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 <noreply@anthropic.com>
This commit is contained in:
parent
8f5b18cf68
commit
c66bee5cf0
25 changed files with 245 additions and 2 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -27,8 +27,8 @@ interface BackupCrypto {
|
|||
fun matches(data: ByteArray): Boolean
|
||||
|
||||
companion object {
|
||||
/** All known formats, preferred encryption format first. */
|
||||
fun all(): List<BackupCrypto> = listOf(JceBackupCrypto)
|
||||
/** All known formats, preferred encryption format first: age is the target. */
|
||||
fun all(): List<BackupCrypto> = listOf(AgeBackupCrypto, JceBackupCrypto)
|
||||
|
||||
fun detect(data: ByteArray): BackupCrypto? = all().firstOrNull { it.matches(data) }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue