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,72 @@
|
|||
package no.naiv.meddetsamme.backup
|
||||
|
||||
import android.content.Context
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import no.naiv.meddetsamme.data.MedDatabase
|
||||
import no.naiv.meddetsamme.settings.SettingsStore
|
||||
|
||||
/**
|
||||
* The one entry point for export, import and auto-backup — all three share
|
||||
* [BackupSerializer] and [BackupCrypto] by construction (brief decision #6).
|
||||
*/
|
||||
class BackupManager(private val context: Context) {
|
||||
|
||||
private val db get() = MedDatabase.get(context)
|
||||
|
||||
/** Plaintext JSON export (caller decides whether/how to encrypt). */
|
||||
suspend fun exportJson(): String {
|
||||
val dao = db.backupDao()
|
||||
return BackupSerializer.export(dao.allMedications(), dao.allDoseTimes(), dao.allDoseLogs())
|
||||
}
|
||||
|
||||
/** Export encrypted with a *typed* passphrase (never stored — manual path). */
|
||||
suspend fun exportEncrypted(passphrase: CharArray): ByteArray =
|
||||
BackupCrypto.all().first().encrypt(exportJson().toByteArray(Charsets.UTF_8), passphrase)
|
||||
|
||||
/**
|
||||
* Import from file content: encrypted (format auto-detected) or plain JSON.
|
||||
* Replace-all; the caller MUST re-arm alarms afterwards.
|
||||
*/
|
||||
suspend fun import(data: ByteArray, passphrase: CharArray?): Int {
|
||||
val crypto = BackupCrypto.detect(data)
|
||||
val json = when {
|
||||
crypto != null -> {
|
||||
requireNotNull(passphrase) { "Denne filen er kryptert — passordfrase kreves" }
|
||||
crypto.decrypt(data, passphrase).toString(Charsets.UTF_8)
|
||||
}
|
||||
else -> data.toString(Charsets.UTF_8)
|
||||
}
|
||||
val file = BackupSerializer.parse(json)
|
||||
BackupSerializer.restore(db, file)
|
||||
return file.medications.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Unattended backup → S3. Encrypts with the STORED passphrase if one is
|
||||
* configured (protects against bucket compromise only — see SettingsStore),
|
||||
* otherwise uploads plaintext JSON by explicit user choice.
|
||||
* Distinct timestamped objects; retention is the bucket's lifecycle rule's
|
||||
* problem, not ours.
|
||||
*/
|
||||
suspend fun runAutoBackup(): String {
|
||||
val settings = SettingsStore(context)
|
||||
val s3 = checkNotNull(settings.s3Client()) { "Backup er ikke konfigurert" }
|
||||
|
||||
val json = exportJson().toByteArray(Charsets.UTF_8)
|
||||
val passphrase = settings.autoBackupPassphrase
|
||||
val (body, suffix) = if (passphrase.isNullOrEmpty()) {
|
||||
json to "json"
|
||||
} else {
|
||||
val crypto = BackupCrypto.all().first()
|
||||
crypto.encrypt(json, passphrase.toCharArray()) to "json.${crypto.formatId}"
|
||||
}
|
||||
|
||||
val stamp = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'")
|
||||
.withZone(ZoneOffset.UTC).format(Instant.now())
|
||||
val key = "meddetsamme/backup-$stamp.$suffix"
|
||||
s3.put(key, body)
|
||||
return key
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue