Backup: versioned JSON serializer, SigV4 + S3 client, JCE crypto, daily worker
Milestone 5. One BackupManager entry point so export, import and auto-backup share the same serializer and crypto by construction. Explicit DTOs decouple the backup contract from the Room schema. JCE baseline (PBKDF2-HMAC-SHA256 600k → AES-256-GCM) behind the BackupCrypto interface with format auto-detection on decrypt; age lands behind the same interface in milestone 6. SigV4 signer verified against AWS's published documentation example signature. Settings in SharedPreferences encrypted under a non-exportable AndroidKeyStore key — replaces the now fully deprecated androidx security-crypto with the same construction (~70 lines, zero deps); threat-model comments state plainly that the stored auto-backup passphrase only protects against bucket compromise. WorkManager daily periodic job, inexact by design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
46a5d7e98e
commit
8f5b18cf68
14 changed files with 892 additions and 0 deletions
|
|
@ -0,0 +1,54 @@
|
|||
package no.naiv.meddetsamme.backup
|
||||
|
||||
import javax.crypto.AEADBadTagException
|
||||
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 JceBackupCryptoTest {
|
||||
|
||||
private val plaintext = """{"version":1,"medications":[]}""".toByteArray()
|
||||
private val passphrase = "korrekt hest batteri stift".toCharArray()
|
||||
|
||||
@Test
|
||||
fun `round trip with correct passphrase`() {
|
||||
val ct = JceBackupCrypto.encrypt(plaintext, passphrase)
|
||||
assertArrayEquals(plaintext, JceBackupCrypto.decrypt(ct, passphrase))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wrong passphrase fails authentication, not garbage output`() {
|
||||
val ct = JceBackupCrypto.encrypt(plaintext, passphrase)
|
||||
assertThrows(AEADBadTagException::class.java) {
|
||||
JceBackupCrypto.decrypt(ct, "feil passord".toCharArray())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tampered ciphertext fails authentication`() {
|
||||
val ct = JceBackupCrypto.encrypt(plaintext, passphrase)
|
||||
ct[ct.size - 5] = (ct[ct.size - 5].toInt() xor 1).toByte()
|
||||
assertThrows(AEADBadTagException::class.java) {
|
||||
JceBackupCrypto.decrypt(ct, passphrase)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `format detection by magic`() {
|
||||
val ct = JceBackupCrypto.encrypt(plaintext, passphrase)
|
||||
assertTrue(JceBackupCrypto.matches(ct))
|
||||
assertFalse(JceBackupCrypto.matches(plaintext))
|
||||
assertEquals(JceBackupCrypto, BackupCrypto.detect(ct))
|
||||
assertEquals(null, BackupCrypto.detect(plaintext))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fresh salt and IV every time`() {
|
||||
val a = JceBackupCrypto.encrypt(plaintext, passphrase)
|
||||
val b = JceBackupCrypto.encrypt(plaintext, passphrase)
|
||||
assertFalse(a.contentEquals(b)) // deterministic encryption would leak equality
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue