From 6ac7f652ad370a7e8c59e1dcdcd1cf76f5b0a35e Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 13 Oct 2023 12:10:59 -0300 Subject: [PATCH] [db] verify fingerprint of known repositories if the user tries to add a repository at a known address with a different fingerprint than what we have on file, we'll refuse to add the repo --- .../main/java/org/fdroid/repo/KnownRepos.kt | 18 ++ .../main/java/org/fdroid/repo/RepoAdder.kt | 6 + .../java/org/fdroid/repo/RepoAdderTest.kt | 154 ++++++++++++++++++ .../src/main/assets/guardianproject_entry.jar | Bin 0 -> 2999 bytes 4 files changed, 178 insertions(+) create mode 100644 libs/database/src/main/java/org/fdroid/repo/KnownRepos.kt create mode 100644 libs/sharedTest/src/main/assets/guardianproject_entry.jar 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 0000000000000000000000000000000000000000..c0d99a7bd5e7a627e3cd84d77f138579621ce37f GIT binary patch literal 2999 zcmZ{m2{aViAIAq_48~e`M%J>gWi5NgGL2m#OSUi~jeSdGZWSyC^ zM#`Qg#-6cc{Po^B@BLrrynD~@{?57Qe$V|a_niANh0&bi1ONa)07lW&dLSb8M=%ut z0HX&0KqtFyKK{rM8HC>*pZ6m$<9RJ6y6Lil@_6hjvT=2sz}%ptE{GaLoxjgovaVqa z!)deD*JgABg5j4!iT-eQQHBm@@xz}GX-$lAO}!Vb@36D~3s`?(=d>eUSrisVU5 zZ&?%3AjN!HQO~38p_)dXceqU@hY(d_1q*s8Kq7Jw{=}rDXyp6YVYvDDQB|!1bS~0g z25!y2CEF=)Ir0S-izl3us<@euA%C%rXcwz~fPpN09IL;MuyCRphTsOmUI`zq+BYyp z^Fa2z=O3>pqFnjAl}(TY@|7g=tP^kJLYUBLzUm2R<$j7mSgMq^a_VBZH7*qOlX**e zii5$-(CyDNsoqRJ=mI|q?O+`$38ED)B!aK)nq=($==UgBk;Wiy-pW)^WFwo=EO4NX znms~u{AlsT+GK);!0c{o1Tje9Ec%$r^soK^|LTuj&*QtVPx?YUG3FD4>se|`8yZ7p z zEtR0Sy9{FVkET&BEl%8+Y$T1<miFt|kj%$(%(=O@5awbyY z>|q3mFjM)^M17p!HLXkocXpBwwz&zHm%sO0diP(qWq6)EJsJi80Q~Rt%q_G>tPVAY z4*Bboa|mO&Oty3{bB%s)5%F)=WF~x3^O-9U-V7*7GCh$sk6k~8)6B=OHUEo_kACmf zc9d|$2SFqZa(1d^>dw^m9))J{XkukQyyYyKys{bEns0KSb|N~-vr2kQ(f}i-@{slk z{q-vJO;(^Rg8q12Um@D^9?Z<7-I_R}5^v@zun0xWJ zCv+m!U)lE|Xh3HHE4|Z_Ocf0enocALl&ymAIs~QYFk&gZ>_^}d(_DD40lE;DDRqqa49ea1r2ufAq zJ}QCfYu*V68k=N4x_tnT_AD9@Kf@oS&SRTav|hDA6l=fj=1ZPv>Ie!L(*^O#4k*%> zJDS;pYy(6%`iv&8X5S!6J+%&wqS&HA3ttpOw|$YpjQ(>7f8Fv~zui!io3I~AiWn*(hF>De>R!#l3a|A7-p*}uizjCj zTitH|DZjk=+EzpKqq{g4;YyPf)vg}`eTG~auI@ z0m%T5qw^JewdJtIH$k~(B&=KNpv*11=T|;psM0X=E~Jc6 zYF#azg4S@`9%2V@B^c9is$W}z|BYh#&#LpveLj|5i%TKB4pZR{%6KbM#k_ZQ%z!m3xFN?P>mfLEPm{+*|!Lse8IX!{{S z`meFE(=E(aB(4K;rJJkTvjb+W$&}*pHSAe@OS1w2RZpzRt#-(trq-K3l+!I-S4|h` zCufwJs1qJ*FFwWLKM%*o&Ea^|;Hb0&^w0h52ST6b6IiTm0u{)QvZXv2M2#Na9d8iC zhTX&*HtrV6PnoPj_ZKo3u3QQ6|a1;&Esy*2^jNifagnnV16hsH8$h z|EMyhlpury5T>O9qUw1(N-kQOqi^pO>hdkK>bCm1N!~DKA@BDOEg|5~Zj3KyH{PqA zebblhEE+xJnt69U1!;R`mvv{yiFV1Vm;srOjXd)IqX^n=lw1%W?QY_NFv(xxy4K7V zlA@ZNi&#=A3JWs$}vibxWvrlxQe4nKnLH?2sWHsOR;t^3$7ezD8rwLpKpu1T1EDiDbPAK+_p6(}+x+Fp=*Tm3dS5GV3*LjOTWSPTf+p#9k5=%G2 zfo=N>+wN2+{_w!=7rdRhBku1@D9apD-V4>`@@RGNhM3}LZl6FF>&xQg{osrU5DD`R=(&)> za0LjEoukgkJ!P}r{KKb~J&u1f2lr!>0K~~>>~&(lNbzsxfNL8YLiH>xW#G_lIKKZX zOj>4u873`3#DAzUmm9t?{cZN!77P!sH8&tgikA-eOAtTIeFN?K4l61s4h8gHrAJhm zi!p!=YBnU531W;tSdjfn3{0}4yC?}?!%`+g@w-e)aw&KQBpL`a@~f$$%>e>TVN}$d zH2+PkCjtIBa8bY9Km7Wq?pOT(K~jIIeys&3{QGx~zqsmm>F<>Ck2I3{U(yq1`Ca>a hG5^tWp78eH*ZyB=O<}ZjzfPSxnVToR>KB&-{s-z7QBnW^ literal 0 HcmV?d00001