diff --git a/libs/database/src/main/java/org/fdroid/repo/KnownRepos.kt b/libs/database/src/main/java/org/fdroid/repo/KnownRepos.kt new file mode 100644 index 000000000..34c2fe7bf --- /dev/null +++ b/libs/database/src/main/java/org/fdroid/repo/KnownRepos.kt @@ -0,0 +1,18 @@ +package org.fdroid.repo + +/** + * A map from canonical repo URL to lower-case fingerprint of this repo. + * When adding new repos here, please test that adding the repo still works. + */ +internal val knownRepos = mapOf( + "https://apt.izzysoft.de/fdroid/repo" to + "3bf0d6abfeae2f401707b6d966be743bf0eee49c2561b9ba39073711f628937a", + "https://archive.newpipe.net/fdroid/repo" to + "e2402c78f9b97c6c89e97db914a2751fda1d02fe2039cc0897a462bdb57e7501", + "https://briarproject.org/fdroid/repo" to + "1fb874bee7276d28ecb2c9b06e8a122ec4bcb4008161436ce474c257cbf49bd6", + "https://guardianproject.info/fdroid/repo" to + "b7c2eefd8dac7806af67dfcd92eb18126bc08312a7f2d6f3862e46013c7a6135", + "https://microg.org/fdroid/repo" to + "9bd06727e62796c0130eb6dab39b73157451582cbd138e86c468acc395d14165", +) diff --git a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt index 8e6bdcd4f..61dd6d933 100644 --- a/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt +++ b/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt @@ -146,6 +146,12 @@ internal class RepoAdder( val receiver = object : RepoPreviewReceiver { override fun onRepoReceived(repo: Repository) { receivedRepo = repo + if (repo.address in knownRepos) { + val knownFingerprint = knownRepos[repo.address] + if (knownFingerprint != repo.fingerprint) throw SigningException( + "Known fingerprint different from given one: ${repo.fingerprint}" + ) + } fetchResult = getFetchResult(nUri.uri.toString(), repo) addRepoState.value = Fetching(receivedRepo, apps.toList(), fetchResult) } diff --git a/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt b/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt index e552a68d6..47e97752c 100644 --- a/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt +++ b/libs/database/src/test/java/org/fdroid/repo/RepoAdderTest.kt @@ -36,8 +36,11 @@ import org.fdroid.download.NotFoundException import org.fdroid.download.getDigestInputStream import org.fdroid.fdroid.DigestInputStream import org.fdroid.index.IndexFormatVersion +import org.fdroid.index.IndexParser.json import org.fdroid.index.SigningException import org.fdroid.index.TempFileProvider +import org.fdroid.index.v2.IndexV2 +import org.fdroid.index.v2.RepoV2 import org.fdroid.repo.AddRepoError.ErrorType.INVALID_FINGERPRINT import org.fdroid.repo.AddRepoError.ErrorType.INVALID_INDEX import org.fdroid.repo.AddRepoError.ErrorType.IO_ERROR @@ -474,6 +477,157 @@ internal class RepoAdderTest { } } + @Test + fun testWrongKnownFingerprint() = runTest { + val url = "https://example.org/repo" + testMinRepoPreview(url) { state2 -> + assertTrue(state2 is AddRepoError, "$state2") + assertEquals(INVALID_FINGERPRINT, state2.errorType) + val e = assertIs(state2.exception) + assertTrue(e.message!!.contains("Known fingerprint different")) + } + } + + @Test + fun testWrongKnownFingerprintWithGivenFingerprint() = runTest { + val url = "https://example.org/repo?fingerprint=${VerifierConstants.FINGERPRINT}" + testMinRepoPreview("https://example.org/repo", url) { state2 -> + assertTrue(state2 is AddRepoError, "$state2") + assertEquals(INVALID_FINGERPRINT, state2.errorType) + val e = assertIs(state2.exception) + assertTrue(e.message!!.contains("Known fingerprint different")) + } + } + + private suspend fun testMinRepoPreview( + repoAddress: String, + url: String = repoAddress, + onSecondState: (AddRepoState) -> Unit, + ) { + val jarFile = folder.newFile() + val repoV2 = RepoV2( + address = "https://briarproject.org/fdroid/repo", + timestamp = 42L, + ) + val indexV2 = IndexV2(repo = repoV2) + val index = json.encodeToString(IndexV2.serializer(), indexV2).toByteArray() + val indexStream = DigestInputStream(ByteArrayInputStream(index), digest) + + every { tempFileProvider.createTempFile() } returns jarFile + every { + downloaderFactory.create( + repo = match { + it.address == repoAddress && it.formatVersion == IndexFormatVersion.TWO + }, + uri = Uri.parse("$repoAddress/entry.jar"), + indexFile = any(), + destFile = jarFile, + ) + } returns downloader + every { downloader.download() } answers { + jarFile.outputStream().use { outputStream -> + assets.open("diff-empty-min/entry.jar").use { inputStream -> + inputStream.copyTo(outputStream) + } + } + } + coEvery { + httpManager.getDigestInputStream(match { + it.indexFile.name == "../index-min-v2.json" && + it.mirrors.size == 1 && it.mirrors[0].baseUrl == repoAddress + }) + } returns indexStream + every { + digest.digest() // sha256 from entry.json + } returns "851ecda085ed53adab25f761a9dbf4c09d59e5bff9c9d5530814d56445ae30f2".decodeHex() + + repoAdder.addRepoState.test { + assertIs(awaitItem()) + + repoAdder.fetchRepository(url = url, proxy = null) + + val state1 = awaitItem() + assertIs(state1) + assertNull(state1.repo) + assertTrue(state1.apps.isEmpty()) + assertFalse(state1.canAdd) + + val state2 = awaitItem() + onSecondState(state2) + } + } + + @Test + fun testKnownFingerprintIsAccepted() = runTest { + val repoAddress = "https://guardianproject.info/fdroid/repo" + val fingerprint = knownRepos[repoAddress] + val url = "https://example.org/repo?fingerprint=$fingerprint" + + val jarFile = folder.newFile() + val repoV2 = RepoV2( + address = repoAddress, + timestamp = 42L, + ) + val indexV2 = IndexV2(repo = repoV2) + val index = json.encodeToString(IndexV2.serializer(), indexV2).toByteArray() + val indexStream = DigestInputStream(ByteArrayInputStream(index), digest) + + every { tempFileProvider.createTempFile() } returns jarFile + every { + downloaderFactory.create( + repo = match { + it.address == "https://example.org/repo" && + it.formatVersion == IndexFormatVersion.TWO + }, + uri = Uri.parse("https://example.org/repo/entry.jar"), + indexFile = any(), + destFile = jarFile, + ) + } returns downloader + every { downloader.download() } answers { + jarFile.outputStream().use { outputStream -> + assets.open("guardianproject_entry.jar").use { inputStream -> + inputStream.copyTo(outputStream) + } + } + } + coEvery { + httpManager.getDigestInputStream(match { + it.indexFile.name == "/index-v2.json" && + it.mirrors.size == 1 && it.mirrors[0].baseUrl == "https://example.org/repo" + }) + } returns indexStream + every { + digest.digest() // sha256 from entry.json + } returns "cd925cdc31c88e8509bd64e62f7680d8dbffe2643990f62404acfda71e538906".decodeHex() + + // repo not in DB + every { repoDao.getRepository(any()) } returns null + + repoAdder.addRepoState.test { + assertIs(awaitItem()) + + repoAdder.fetchRepository(url = url, proxy = null) + + val state1 = awaitItem() + assertIs(state1) + assertNull(state1.repo) + assertTrue(state1.apps.isEmpty()) + assertFalse(state1.canAdd) + + val state2 = awaitItem() + assertIs(state2) + assertEquals(repoAddress, state2.repo?.address) + assertTrue(state2.canAdd) + assertFalse(state2.done) + + val state3 = awaitItem() + assertIs(state3) + assertTrue(state3.canAdd) + assertTrue(state3.done) + } + } + @Test fun testFallbackToV1() = runTest { val url = "https://example.org/repo/" diff --git a/libs/sharedTest/src/main/assets/guardianproject_entry.jar b/libs/sharedTest/src/main/assets/guardianproject_entry.jar new file mode 100644 index 000000000..c0d99a7bd Binary files /dev/null and b/libs/sharedTest/src/main/assets/guardianproject_entry.jar differ