From c66bee5cf028fa94fb79cfaf9d44f40ce1da553f Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 10 Jun 2026 14:00:34 +0200 Subject: [PATCH] age crypto via kage, verified against the CCTV scrypt vectors Milestone 6. kage 0.4.0 over Jagged: Android-first (explicitly supports API 26, matching minSdk), Kotlin, ships its own primitives through BouncyCastle instead of relying on JCA ChaCha20-Poly1305 which Android only provides from API 28. AgeBackupCrypto sits behind the same BackupCrypto interface as the JCE baseline; age is now first in all() so new encrypted backups are written as binary age v1 files, decryptable anywhere with 'age -d' (decision #7). Old MDS1/JCE backups keep decrypting via format auto-detection. The C2SP/CCTV scrypt vector subset (19 files) is vendored into test resources: both success vectors decrypt with matching payload SHA-256, all 17 failure vectors are rejected. Co-Authored-By: Claude Fable 5 --- app/build.gradle.kts | 1 + .../meddetsamme/backup/AgeBackupCrypto.kt | 55 ++++++++++ .../naiv/meddetsamme/backup/BackupCrypto.kt | 4 +- .../meddetsamme/backup/AgeBackupCryptoTest.kt | 46 +++++++++ .../backup/CctvScryptVectorTest.kt | 95 ++++++++++++++++++ app/src/test/resources/cctv/armor_scrypt | 12 +++ app/src/test/resources/cctv/scrypt | Bin 0 -> 340 bytes app/src/test/resources/cctv/scrypt_bad_tag | Bin 0 -> 358 bytes app/src/test/resources/cctv/scrypt_double | 13 +++ .../test/resources/cctv/scrypt_extra_argument | Bin 0 -> 335 bytes .../test/resources/cctv/scrypt_long_file_key | Bin 0 -> 377 bytes app/src/test/resources/cctv/scrypt_no_match | Bin 0 -> 264 bytes .../resources/cctv/scrypt_not_canonical_body | Bin 0 -> 332 bytes .../resources/cctv/scrypt_not_canonical_salt | Bin 0 -> 273 bytes app/src/test/resources/cctv/scrypt_salt_long | Bin 0 -> 278 bytes .../test/resources/cctv/scrypt_salt_missing | 9 ++ app/src/test/resources/cctv/scrypt_salt_short | Bin 0 -> 267 bytes app/src/test/resources/cctv/scrypt_uppercase | Bin 0 -> 267 bytes .../test/resources/cctv/scrypt_work_factor_23 | 10 ++ .../resources/cctv/scrypt_work_factor_missing | Bin 0 -> 270 bytes .../cctv/scrypt_work_factor_negative | Bin 0 -> 274 bytes .../cctv/scrypt_work_factor_overflow | Bin 0 -> 290 bytes .../resources/cctv/scrypt_work_factor_wrong | Bin 0 -> 267 bytes .../resources/cctv/scrypt_work_factor_zero | Bin 0 -> 272 bytes gradle/libs.versions.toml | 2 + 25 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/no/naiv/meddetsamme/backup/AgeBackupCrypto.kt create mode 100644 app/src/test/java/no/naiv/meddetsamme/backup/AgeBackupCryptoTest.kt create mode 100644 app/src/test/java/no/naiv/meddetsamme/backup/CctvScryptVectorTest.kt create mode 100644 app/src/test/resources/cctv/armor_scrypt create mode 100644 app/src/test/resources/cctv/scrypt create mode 100644 app/src/test/resources/cctv/scrypt_bad_tag create mode 100644 app/src/test/resources/cctv/scrypt_double create mode 100644 app/src/test/resources/cctv/scrypt_extra_argument create mode 100644 app/src/test/resources/cctv/scrypt_long_file_key create mode 100644 app/src/test/resources/cctv/scrypt_no_match create mode 100644 app/src/test/resources/cctv/scrypt_not_canonical_body create mode 100644 app/src/test/resources/cctv/scrypt_not_canonical_salt create mode 100644 app/src/test/resources/cctv/scrypt_salt_long create mode 100644 app/src/test/resources/cctv/scrypt_salt_missing create mode 100644 app/src/test/resources/cctv/scrypt_salt_short create mode 100644 app/src/test/resources/cctv/scrypt_uppercase create mode 100644 app/src/test/resources/cctv/scrypt_work_factor_23 create mode 100644 app/src/test/resources/cctv/scrypt_work_factor_missing create mode 100644 app/src/test/resources/cctv/scrypt_work_factor_negative create mode 100644 app/src/test/resources/cctv/scrypt_work_factor_overflow create mode 100644 app/src/test/resources/cctv/scrypt_work_factor_wrong create mode 100644 app/src/test/resources/cctv/scrypt_work_factor_zero diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5582296..bfa15ec 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,6 +62,7 @@ dependencies { implementation(libs.work.runtime) implementation(libs.okhttp) implementation(libs.kotlinx.serialization.json) + implementation(libs.kage) testImplementation(libs.junit) } diff --git a/app/src/main/java/no/naiv/meddetsamme/backup/AgeBackupCrypto.kt b/app/src/main/java/no/naiv/meddetsamme/backup/AgeBackupCrypto.kt new file mode 100644 index 0000000..ffff681 --- /dev/null +++ b/app/src/main/java/no/naiv/meddetsamme/backup/AgeBackupCrypto.kt @@ -0,0 +1,55 @@ +package no.naiv.meddetsamme.backup + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import kage.Age +import kage.crypto.scrypt.ScryptIdentity +import kage.crypto.scrypt.ScryptRecipient + +/** + * age (age-encryption.org/v1) in scrypt/passphrase mode via kage — the target + * format (brief decision #7): backups are decryptable on any machine with + * plain `age -d`, so this app can die without taking the data with it. + * + * kage is verified in unit tests against the age Community Cryptography Test + * Vectors (C2SP/CCTV) before being trusted here. + */ +object AgeBackupCrypto : BackupCrypto { + + private val BINARY_MAGIC = "age-encryption.org/v1".toByteArray(Charsets.US_ASCII) + private val ARMOR_MAGIC = "-----BEGIN AGE ENCRYPTED FILE-----".toByteArray(Charsets.US_ASCII) + + /** age default work factor (2^18) — same cost `age -p` uses. */ + private const val WORK_FACTOR = 18 + + override val formatId = "age" + + override fun matches(data: ByteArray): Boolean = + data.startsWith(BINARY_MAGIC) || data.startsWith(ARMOR_MAGIC) + + override fun encrypt(plaintext: ByteArray, passphrase: CharArray): ByteArray { + val out = ByteArrayOutputStream() + Age.encryptStream( + listOf(ScryptRecipient(passphrase.toUtf8Bytes(), WORK_FACTOR)), + ByteArrayInputStream(plaintext), + out, + ) + return out.toByteArray() + } + + override fun decrypt(ciphertext: ByteArray, passphrase: CharArray): ByteArray { + val out = ByteArrayOutputStream() + // kage throws kage.errors.* on wrong passphrase or malformed input. + Age.decryptStream( + listOf(ScryptIdentity(passphrase.toUtf8Bytes())), + ByteArrayInputStream(ciphertext), + out, + ) + return out.toByteArray() + } + + private fun ByteArray.startsWith(prefix: ByteArray): Boolean = + size >= prefix.size && copyOfRange(0, prefix.size).contentEquals(prefix) + + private fun CharArray.toUtf8Bytes(): ByteArray = String(this).toByteArray(Charsets.UTF_8) +} diff --git a/app/src/main/java/no/naiv/meddetsamme/backup/BackupCrypto.kt b/app/src/main/java/no/naiv/meddetsamme/backup/BackupCrypto.kt index 01c50bb..0686950 100644 --- a/app/src/main/java/no/naiv/meddetsamme/backup/BackupCrypto.kt +++ b/app/src/main/java/no/naiv/meddetsamme/backup/BackupCrypto.kt @@ -27,8 +27,8 @@ interface BackupCrypto { fun matches(data: ByteArray): Boolean companion object { - /** All known formats, preferred encryption format first. */ - fun all(): List = listOf(JceBackupCrypto) + /** All known formats, preferred encryption format first: age is the target. */ + fun all(): List = listOf(AgeBackupCrypto, JceBackupCrypto) fun detect(data: ByteArray): BackupCrypto? = all().firstOrNull { it.matches(data) } } diff --git a/app/src/test/java/no/naiv/meddetsamme/backup/AgeBackupCryptoTest.kt b/app/src/test/java/no/naiv/meddetsamme/backup/AgeBackupCryptoTest.kt new file mode 100644 index 0000000..3561836 --- /dev/null +++ b/app/src/test/java/no/naiv/meddetsamme/backup/AgeBackupCryptoTest.kt @@ -0,0 +1,46 @@ +package no.naiv.meddetsamme.backup + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test + +class AgeBackupCryptoTest { + + private val plaintext = """{"version":1,"medications":[]}""".toByteArray() + private val passphrase = "korrekt hest batteri stift".toCharArray() + + @Test + fun `round trip with correct passphrase`() { + val ct = AgeBackupCrypto.encrypt(plaintext, passphrase) + assertArrayEquals(plaintext, AgeBackupCrypto.decrypt(ct, passphrase)) + } + + @Test + fun `output is a binary age v1 file`() { + val ct = AgeBackupCrypto.encrypt(plaintext, passphrase) + assertTrue(ct.decodeToString(0, 21) == "age-encryption.org/v1") + } + + @Test + fun `wrong passphrase throws`() { + val ct = AgeBackupCrypto.encrypt(plaintext, passphrase) + assertThrows(Exception::class.java) { + AgeBackupCrypto.decrypt(ct, "feil passord".toCharArray()) + } + } + + @Test + fun `format detection prefers age and routes correctly`() { + val ageCt = AgeBackupCrypto.encrypt(plaintext, passphrase) + val jceCt = JceBackupCrypto.encrypt(plaintext, passphrase) + assertEquals(AgeBackupCrypto, BackupCrypto.detect(ageCt)) + assertEquals(JceBackupCrypto, BackupCrypto.detect(jceCt)) + assertFalse(JceBackupCrypto.matches(ageCt)) + assertFalse(AgeBackupCrypto.matches(jceCt)) + // age first = the format new backups are written in (the brief's target). + assertEquals(AgeBackupCrypto, BackupCrypto.all().first()) + } +} diff --git a/app/src/test/java/no/naiv/meddetsamme/backup/CctvScryptVectorTest.kt b/app/src/test/java/no/naiv/meddetsamme/backup/CctvScryptVectorTest.kt new file mode 100644 index 0000000..37592c5 --- /dev/null +++ b/app/src/test/java/no/naiv/meddetsamme/backup/CctvScryptVectorTest.kt @@ -0,0 +1,95 @@ +package no.naiv.meddetsamme.backup + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.security.MessageDigest +import kage.Age +import kage.crypto.scrypt.ScryptIdentity +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Test + +/** + * Runs kage against the age Community Cryptography Test Vectors + * (github.com/C2SP/CCTV, `age/testdata`, scrypt subset) — the brief's gate + * for trusting the age implementation with medication data. + * + * Vector format: "key: value" header lines, blank line, then the raw age file. + * expect: success → decrypt must succeed AND the payload SHA-256 must match; + * anything else (header failure / no match / …) → decrypt must throw. + */ +class CctvScryptVectorTest { + + private val successVectors = listOf("scrypt", "armor_scrypt") + private val failureVectors = listOf( + "scrypt_bad_tag", "scrypt_double", "scrypt_extra_argument", + "scrypt_long_file_key", "scrypt_no_match", "scrypt_not_canonical_body", + "scrypt_not_canonical_salt", "scrypt_salt_long", "scrypt_salt_missing", + "scrypt_salt_short", "scrypt_uppercase", "scrypt_work_factor_23", + "scrypt_work_factor_missing", "scrypt_work_factor_negative", + "scrypt_work_factor_overflow", "scrypt_work_factor_wrong", + "scrypt_work_factor_zero", + ) + + @Test + fun `success vectors decrypt and payload hashes match`() { + for (name in successVectors) { + val v = load(name) + assertEquals("$name: expect header", "success", v.headers["expect"]) + val out = ByteArrayOutputStream() + Age.decryptStream( + listOf(ScryptIdentity(v.headers.getValue("passphrase").toByteArray())), + ByteArrayInputStream(v.body), + out, + ) + val hash = MessageDigest.getInstance("SHA-256").digest(out.toByteArray()) + .joinToString("") { "%02x".format(it) } + assertEquals("$name: payload hash", v.headers["payload"], hash) + } + } + + @Test + fun `failure vectors are all rejected`() { + for (name in failureVectors) { + val v = load(name) + val passphrase = v.headers["passphrase"] ?: "password" + try { + val out = ByteArrayOutputStream() + Age.decryptStream( + listOf(ScryptIdentity(passphrase.toByteArray())), + ByteArrayInputStream(v.body), + out, + ) + fail("$name (expect: ${v.headers["expect"]}) decrypted but must be rejected") + } catch (_: Exception) { + // expected — any rejection is a pass for non-success vectors + } + } + } + + private class Vector(val headers: Map, val body: ByteArray) + + private fun load(name: String): Vector { + val bytes = checkNotNull(javaClass.classLoader.getResourceAsStream("cctv/$name")) { + "missing test resource cctv/$name" + }.readBytes() + + // Header/body split at the first blank line; body is binary, so no + // string round-trip for it. + var split = -1 + for (i in 0 until bytes.size - 1) { + if (bytes[i] == '\n'.code.toByte() && bytes[i + 1] == '\n'.code.toByte()) { + split = i; break + } + } + check(split > 0) { "$name: no blank line found" } + + val headers = String(bytes, 0, split, Charsets.UTF_8).lines() + .filter { it.isNotBlank() } + .associate { line -> + val (k, v) = line.split(": ", limit = 2) + k to v + } + return Vector(headers, bytes.copyOfRange(split + 2, bytes.size)) + } +} diff --git a/app/src/test/resources/cctv/armor_scrypt b/app/src/test/resources/cctv/armor_scrypt new file mode 100644 index 0000000..51e4092 --- /dev/null +++ b/app/src/test/resources/cctv/armor_scrypt @@ -0,0 +1,12 @@ +expect: success +payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab +file key: 59454c4c4f57205355424d4152494e45 +passphrase: password +armored: yes + +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdCByRjAvTndibFVISFRwZ1Fn +UnBlNUNRIDEwCmdVakV5bUZLTVZYUUVLZE1NSEwyNG9ZZXhqRTNUSUMwTzB6R1Nx +SjJhVVkKLS0tIElPWGlRWVN0a29UMW12WlcydEZPcVpkaFJWdmo1OGVnQUJ4L3NX +ZlpRYmMKGzXG5ofdANo6w3msn3QsIf0YWhuePe1znRSsappQEk24Ztg= +-----END AGE ENCRYPTED FILE----- diff --git a/app/src/test/resources/cctv/scrypt b/app/src/test/resources/cctv/scrypt new file mode 100644 index 0000000000000000000000000000000000000000..57dd08f0e7b2312db772e8f24d786de3bb3da187 GIT binary patch literal 340 zcmW-aJ5Iwe06>{DA_k-`W+&8f?8b=%5>zcMC6x5vY>w@uKbkZSZNmk)0}^n79)KN* zGaw{ZHjaQqL2r3_%aiLw^2`D1mUF3;mau#lGvNSem?6O!LQaujkV4b4h|L7Ep~YxG zOIX^LkksIiQf5#DONiruln@F9Vu2QpXA(?h?f_zAg87z^SO^l65R98^`mbIE{)+L(@-y`fnhu!s68`n00HlP7--hE0Lt^ z0Rw7LcT&l3stva{@G1@Ay0uG$Djud`l0UyTq}^bulvuP59c-CJZFaoKl^^+>G)ec?rkOy K*SqdZ`2GvoZEjrv literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_bad_tag b/app/src/test/resources/cctv/scrypt_bad_tag new file mode 100644 index 0000000000000000000000000000000000000000..b47512f0d17028b2b684b53248d07d3f5ce27d14 GIT binary patch literal 358 zcmXYp%}&BV6op;S(zq~TGw!XlWhP1!O-KO?Sox>uZfK{|AKIzy2$UD_9Zb|0-~rs5 z_zcFxtvesV#6rxu$@y~cJ%_FmY6}g<97aUg9&r6Rg(1x~h^q)AyO4`dRT)<>Mk;cU zf>l&S6k(8%EK9tUWK@G&Uh>odHjg427mdQBQ1?hNs@&uAT&c(ylKWyE3*RQf=P?w7 z!L1dqGw$S&yF1S8G*1M~1c}!K`WalNJZ1n8MkN~Wjz#)}r>wkCfOG->18Gw(UoU6# zv0;o7)@Or+V!aO)8L)BC$fIV*oJ{+Tj$@j}m5TTcT?LKGsHMwYd3`y&YpY}y0!fnK zFs<=$+LAHhp|YU1UgP@Yj9EdwT{o scrypt rF0/NwblUHHTpgQgRpe5CQ 10 +gUjEymFKMVXQEKdMMHL24oYexjE3TIC0O0zGSqJ2aUY +-> scrypt GzXG5ofdANo6w3msn3QsIQ 10 +OveITuwxakv7k2oLnioNYF4Bhgz9KZ36pb098wDoAv8 +--- a5d+4Ay1evJhoDskIzuTZV9bBgKk4573VZNfuoWJDPE +îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf \ No newline at end of file diff --git a/app/src/test/resources/cctv/scrypt_extra_argument b/app/src/test/resources/cctv/scrypt_extra_argument new file mode 100644 index 0000000000000000000000000000000000000000..3fa92ee932400a1646428ead4d88330775c1b4e2 GIT binary patch literal 335 zcmW-ZO-{l<9K~JF(zq~TGp@DLmKhCAbO97mEK;E;?xyX(A6PnNrV8Z(-oZq@00(ey z;u(yITX!D8#7f>h> ziBB}DlR5!HQO2aqBEcjW_&?9N@R7%-Q%K9-JOb>N-2uUndfboG5c6P-WyA#FSmKoD z*kdVAV~-^Wu@IDUD~c?SdAiRoz5@|?eH=K+O)BzmPVm7 zF^g%tYmKgLv+G+{=UgXz42y|b8=f}R3w3!in4Rg&86!neu-npHUbOV7Xv~tvcvm{z z)Z%@&H*!pSsrs>2Tn*Y*6)cG=^!@wcYva>+D^@T0-p=p#P38IMM?P(>CXfA1>oxfJ E0|%pVqW}N^ literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_long_file_key b/app/src/test/resources/cctv/scrypt_long_file_key new file mode 100644 index 0000000000000000000000000000000000000000..669c22257c94386469dff68ab80cb1152adf8856 GIT binary patch literal 377 zcmXwyOG?8~7=<0*fg2FvcdRjw-kZ`2f)!04Qq(qD>L^YAB+cuFoAePk-~x0e;s$gD zb|Qjp1KgE-6Ei}v+e<0$D*eLYC&Trs-FC3S;oT$Niml$(JO zPTWq|)%2lBBuRozleTqtkQtG>=Y;d4fcM%;G&POmK&}iX-KwTH;!c0Yxk=X8>&Mmm g%3J-Vc(~8EHZFG7uWy$(%g5){&%4EwShT*r0fGa7TmS$7 literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_no_match b/app/src/test/resources/cctv/scrypt_no_match new file mode 100644 index 0000000000000000000000000000000000000000..3d090e184d3eb69325430b9eebd2a5cc51ab1bd9 GIT binary patch literal 264 zcmYeTC`e5%u~Nv(SIA8)NzUL(%gjkt$WE=aQZThNF*QjB!ZcHJBLh=oQ&STolN1v} zQzH{glT;H^u7bqk;)0B##Nt#dh4P~OymT(E#Pn3%)V$=P%7T*2{5-w9J?+cNuP39j;t6zeGpzuSAseJlnU$b4Au<%?c3m I-H~- ziaexX6*Un>7-S^Rvrv$nYVg0WxbT3>)0DF6w+Jb8DtCq;WiIz37C`Rra2^stVU$D0 z3+NKYS>%!g01{BC+KMtOBF;{^23M5H_`m#ChTCLpG9j7x6xH3tV ztAQOdUJm>7MV$5Hp}sMWT@iyCe*b>>+WXYr%I!;Wa`byRtvz4-f?xygELTSMPDR=?zV4_~22XJrV z8H|ZrcOJpSN50K3AJjScrHbdk0^m4gaeBi6g>eda0gZ}jhfw1G7ScmSqHRivLINUF zg>(oIigH#I`JA%?RQ%_4#sh>{1Olvl-sEMRtqw9CiIt3m6I}d1%)63!ww|SKzwhMH zB%0x#~;*`iEpW-p0VML}C1_2QvD7OTK6dB^k$)S|DScOP5t>PxeED);xkcduHH$6v+6 N&L+9Hw$118?FS4yTw(wK literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_salt_long b/app/src/test/resources/cctv/scrypt_salt_long new file mode 100644 index 0000000000000000000000000000000000000000..66e37f93776172a1fb5c876b22631b8021e4f4cf GIT binary patch literal 278 zcmW;AJx_x`9Ds2Lr&~Ci+9p8m{233O;c@!l8y)bflFIB4`_>Dz(pNh-JG2K z3JxYN?k27s9Q+Etyj#!B<9)Ta^Ys?KXitmp S*RA#K=K8_Bvp=ny>+K&g_FwJ* literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_salt_missing b/app/src/test/resources/cctv/scrypt_salt_missing new file mode 100644 index 0000000..880b0e1 --- /dev/null +++ b/app/src/test/resources/cctv/scrypt_salt_missing @@ -0,0 +1,9 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +passphrase: password + +age-encryption.org/v1 +-> scrypt 10 +W0mMthyhNJOV3debCwkQcUlNx/i6Ss/A07aQCrG5Gcw +--- 1QsPcEbBSylfP4apakJqtDBJMrpd81rPuSLTCvdZx6E +¬]?7åPqÓ¦ F—¹ •Â÷õÛ®è zŒ(rŠóÎ| \ No newline at end of file diff --git a/app/src/test/resources/cctv/scrypt_salt_short b/app/src/test/resources/cctv/scrypt_salt_short new file mode 100644 index 0000000000000000000000000000000000000000..a95f4608584930ff0805b2ba268f2b61c258e0e2 GIT binary patch literal 267 zcmW;AJ5Iwe9Ds3FydWk97GEIE%PuqnQdJUKwMqab1+gGE{>P1)hjB{UWMg9N$R)S{ z0}~S50g0{R96aaSFE=z62gQK(+XeETUUm=Z`}gD5&ZqO%Ja6O4X4BdFdjZdGKeUI} GgZ&@;hFTy1 literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_uppercase b/app/src/test/resources/cctv/scrypt_uppercase new file mode 100644 index 0000000000000000000000000000000000000000..88bd1e424ddf209cb05b398d03304301ce5f87c8 GIT binary patch literal 267 zcmW;AzfOZd0Dy7l8;yfWmvdI+pm%9Xq6>wB6a@>7?RFjqEf%-}O7R7J2NU%L`T*`G zK7%oFb@LHS{F86_<>PC?S*^jmfElgX9K?$yhgZDSAn9X**>5rFsfct5!77fhLR8$x z91|dDRf%~?E3UymH$@o%Koc(UoRzJp7e#(pl!<(!0O=Tx{~s((B%d~y%h0j{k@(5E z;6(SKf)I6t9rF5l_p$Y^y|kOB`e5&Sch-44`l=pwHtD^$ I?LNnEKS`!p9{>OV literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_work_factor_23 b/app/src/test/resources/cctv/scrypt_work_factor_23 new file mode 100644 index 0000000..4d286be --- /dev/null +++ b/app/src/test/resources/cctv/scrypt_work_factor_23 @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +passphrase: password +comment: work factor is very high, would take a long time to compute + +age-encryption.org/v1 +-> scrypt rF0/NwblUHHTpgQgRpe5CQ 23 +qW9eVsT0NVb/Vswtw8kPIxUnaYmm9Px1dYmq2+4+qZA +--- 38TpQMxQRRNMfmYYpBX6DDrPx4/QY5UmJnhPyVoX/cw +¬]?7åPqÓ¦ F—¹ •Â÷õÛ®è zŒ(rŠóÎ| \ No newline at end of file diff --git a/app/src/test/resources/cctv/scrypt_work_factor_missing b/app/src/test/resources/cctv/scrypt_work_factor_missing new file mode 100644 index 0000000000000000000000000000000000000000..62c14e21f010214bda0625ebeb5f33dd255b10ba GIT binary patch literal 270 zcmW;AOHPA89Ds4xvotPDx*6As1v(Q7iEb)Q8(Xy4`si+eKa}#C4p6`gcn1^p0zH6x z6VG5w+`97!CO-0Qe)*v0;1&j60P{d#pM`NL0Qq4IID*E&R40_Uzxh;CW!j;XsKg@* zRf$f3P{diD^MznJ82HasCOm{#01~9GXm}B3>0u^<_F6&GG0y)V7Ja#GRZct`45mDo z1U3iSn;>Peidoj1@hG^x8SZD(3z;ico#)m*1Hc!RD-uLdj^?3A^KkRIl L_vhQ=XaDU7oUvT3 literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_work_factor_negative b/app/src/test/resources/cctv/scrypt_work_factor_negative new file mode 100644 index 0000000000000000000000000000000000000000..eaf540005c414b593dfc01787a2beb0a9e9ad058 GIT binary patch literal 274 zcmW;AO-_SA7=U5dvotPDx*7K(1D#0=iEa?At}d0|z6SE)Dx*9VRRH%@Okpdd~GUqP#4rcg$`-vZW?3F|UOW$IIj z$yA|087kbeEF$ifpyEF_IrkBALl7YIcw1C)o*m>o6l)0yN4Wfdm>Wd2n#WmX$R6HA%ZwSHU<*NtfR=d_AM=@HP)t7ejRPFD5?=Cx!hhOEx N&L+8^Y@5%)+Ydx`Tsi;% literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_work_factor_overflow b/app/src/test/resources/cctv/scrypt_work_factor_overflow new file mode 100644 index 0000000000000000000000000000000000000000..03aeb0306dbe370b096a837d4a65ed151f4e4841 GIT binary patch literal 290 zcmW-aO-_SA7(iXm(zr0`X55RwFe8*iliCzPL~SXibVuQX{xcnFTY3TSV4_~22XJrV z8H|ZrcOJpSYToAMZ62&R#B&q(Axc2tG#ZTW1fX;<20Vh&#H>mgjklysCkoRTql6|@ zWrS8KP=68#UKoEc~uBTFarPIy}|GTFbHGGX+)H+t>Z)&WHJ0 bZl33d2S5AW%G1er@wm4cKHThjFX{U)-iKcF literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_work_factor_wrong b/app/src/test/resources/cctv/scrypt_work_factor_wrong new file mode 100644 index 0000000000000000000000000000000000000000..209e1f6302ebc55c17d4b399b11d3c2e0ea682d3 GIT binary patch literal 267 zcmW;AOG?8)0DxiFvlJIXH{)7k9-T2wKsTX}1Y1l@p}I>alXo)HNiX?aGzEy#A2J8O^#YmB7?Xs9pCO(hd*y9vi zE>snuCaWS~)|}_@=E^=~{-wgz*}61t`lBpe4!mI|T8@;`8v;p^U}_YTbn0|rzOeKK zadSD-3*n4QE85udxke&qHSSI_e_(?ie*L`r*m*Z!y6sbQbnv}@-FrO!svq{Y+5N@t I^f`L_0p?3ut^fc4 literal 0 HcmV?d00001 diff --git a/app/src/test/resources/cctv/scrypt_work_factor_zero b/app/src/test/resources/cctv/scrypt_work_factor_zero new file mode 100644 index 0000000000000000000000000000000000000000..9d8c2789748d57166bfa04e515328190ec234455 GIT binary patch literal 272 zcmYeTC`e5%u~NuLO-xBGQbRtPcOeH zUBAqbOV>`J7%ZkxfsSmkRF&GRFG=w9H?Nxl^&YqTAAzS?Hd*m=<1!~ z>+9iTWRf45T9M^y9OCJ0;BQdn9$e^Ulo%SxrK_u}kfv*{mL95Y80F$$P;Bay zXkwaMWf|bCpIemepOa)@X5p3=;TvqhC2e}_S^HgvTULiF*UT@`QT!_rB|XpfZSh=@ OHCeL)gnW0T-2ecb+gnWl literal 0 HcmV?d00001 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed94c2c..a97398e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ work = "2.11.2" okhttp = "5.4.0" coroutines = "1.11.0" serializationJson = "1.11.0" +kage = "0.4.0" junit = "4.13.2" [libraries] @@ -33,6 +34,7 @@ work-runtime = { group = "androidx.work", name = "work-runtime", version.ref = " okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serializationJson" } +kage = { group = "com.github.android-password-store", name = "kage", version.ref = "kage" } junit = { group = "junit", name = "junit", version.ref = "junit" } [plugins]