C2-INTEGRATION.md

7.1 KB · raw

rendered · source

1# C2 — Export Screen: Integration Note23Draft implementation of spec C2 (lines 289–337). Three Kotlin files plus this4note, all under `app/drafts/`. Package is already `com.peachpasswords.ui.vault`,5so they drop straight into the source tree. No existing file was edited6(PeachNav.kt, PeachAppNavHost.kt, VaultRepository.kt, VaultCrypto.kt,7VaultEntry.kt, ImportScreen.kt, RecoveryWordsScreen.kt, theme files were read8only).910## Files to MOVE into the source set1112| Draft file | Destination | Contents |13|---|---|---|14| `PeachScriptExport.kt` | `ui/vault/PeachScriptExport.kt` | Pure Kotlin: `buildPlaintext` / `buildEncrypted`, hand-rolled JSON with escaping |15| `CsvExport.kt` | `ui/vault/CsvExport.kt` | Pure Kotlin: `toCsv` + `encodeField` (RFC-4180 + formula-injection guard) |16| `ExportScreen.kt` | `ui/vault/ExportScreen.kt` | Compose UI + `ExportViewModel` + `scorePassword`/`strengthLabel` |1718`C2-INTEGRATION.md` stays in drafts (or delete after wiring) — it is not source.1920## Public composable signature2122```kotlin23@Composable24fun ExportScreen(onBack: () -> Unit)25// (also has a defaulted vm = hiltViewModel() param, like ImportScreen / TrashScreen)26```2728## Nav wiring (one line)2930`PeachNav.Export` route already exists. In `PeachAppNavHost.kt`, replace the31`Export` route's `SimpleStub` with:3233```kotlin34ExportScreen(onBack = back)35```3637where `back` is the same back lambda the other vault screens use (e.g. how38`ImportScreen(onBack = back)` is wired). No new route, no nav-graph change.3940`ExportScreen` handles its own internal back: a sub-view (PeachScript / CSV)41returns to the choose view; the choose view calls `onBack()`. A single42`onBack=back` is correct.4344## Strength scorer thresholds chosen4546`scorePassword(pw): Int` returns 0–4 (local, dependency-free, no network):4748- +1 if length ≥ 849- +1 if length ≥ 1250- +1 if ≥ 3 of {lowercase, uppercase, digit, symbol} present51- +1 if all 4 classes present52- empty → 0, clamped to 0..45354Labels: 0 very weak · 1 weak · 2 fair · 3 good · 4 strong.5556The C2 gate is **score ≥ 3 ("good")**. `[Generate backup]` is disabled until57`score ≥ 3` AND the two password fields match AND the vault is non-empty. When58a non-empty password scores < 3 the breach-colour error59`"Backup password too weak (score < 3)"` is shown (spec line 306). The 4-segment60bar fills `score` segments and tints to the accent once "good".6162## Round-trip confirmation (buildEncrypted ⇄ PeachScriptImport)6364`PeachScriptImport.decryptAndParse(buildEncrypted(es, pw), pw)` returns65`Success` with the same six fields per entry. Reasoning, field by field against66the pinned format in `B2-INTEGRATION.md` and the importer source:67681. **Envelope shape.** `buildEncrypted` emits69   `{"peachscript":1,"encrypted":true,"kdf":"argon2id","salt":"<b64>","blob":"<b64>"}`.70   The importer reads `salt` and `blob` via `jsonRawString` (a `"key":"..."`71   regex) — key names and JSON-string shape match exactly.722. **Base64.** Both sides use `java.util.Base64` standard (non-URL-safe) codec:73   `getEncoder()` here, `getDecoder()` there. Salt is 16 bytes74   (`VaultCrypto.generateSalt()` = `SALT_SIZE`); blob is `nonce(12) || ct||tag`,75   so `blob.size > NONCE_SIZE` — passes the importer's length guards.763. **KDF.** `salt` feeds `VaultCrypto.deriveKey(pw, salt)` on both sides77   (Argon2id, identical params from the shared `VaultCrypto` constants).784. **AEAD + AAD.** `blob` is exactly `VaultCrypto.encrypt(plaintextBytes, key,79   PeachScriptImport.PEACHSCRIPT_AAD)`. The importer calls80   `VaultCrypto.decrypt(blob, key, PEACHSCRIPT_AAD)` with the **same constant**81   AAD (`"peachscript_export_v1"`). `encrypt` prepends the 12-byte nonce;82   `decrypt` strips the first 12 bytes as the nonce. The GCM tag therefore83   verifies, so wrong-password / tamper paths surface as84   `DecryptResult.WrongPassword` exactly as designed.855. **Inner plaintext.** The decrypted bytes are86   `buildPlaintext(es)` =87   `{"peachscript":1,"encrypted":false,"entries":[{...}]}`. The importer's88   `parsePlaintext` uses `extractArrayBody("entries")` +89   `splitTopLevelObjects` + `jsonString` for `title/username/password/url/90   notes/totp` — the exact keys this writer emits.916. **Escaping.** `buildPlaintext` escapes `\`, `"`, and control chars (`\n \r92   \t \b \f`, `\uXXXX` for the rest). The importer's `unescapeJson` reverses93   `\n \t \r \" \\ \/ \uXXXX`, so any value round-trips. (We never emit `\/`;94   forward slash is left literal, which is valid JSON and read back verbatim.)9596The only normalisation is field projection (see assumptions): empty strings97become `null` on the way back because the importer's `jsonString` maps `""` →98`null`, and `parsePlaintext` drops entries whose `title` is blank. So entries99with a non-blank title and otherwise-null optional fields round-trip to the same100`ParsedEntry` shape.101102## VaultRepository methods used (all already exist — no new API assumed)103104- `fun getEntries(): List<VaultEntry>` — returns active (non-deleted) entries105  (verified: `entryList.filter { it.deletedAt == null }`). Used for both the106  count/empty-vault gate and the export payload.107108`ExportViewModel` injects `VaultRepository` + `@ApplicationContext Context`109(for SAF `openOutputStream`), exactly like `ImportViewModel`.110111## SAF / clipboard patterns112113- PeachScript save: `CreateDocument("*/*")`, suggested name `peach-backup.peach`114  (mirrors `RecoveryWordsScreen`'s `CreateDocument` save pattern). `"*/*"` is115  used because there is no registered MIME type for `.peach`.116- CSV save: `CreateDocument("text/csv")`, suggested name `peach-export.csv`.117- Copy to clipboard uses `ClipboardManager` /118  `ClipData.newPlainText("peach backup", json)` — same approach as119  RecoveryWordsScreen's "copy all words".120121## Assumptions / things for the orchestrator to confirm1221231. **`email` → `username` fold.** PeachScript and the CSV `username` column have124   no `email` field, and the importer reads only six keys. When `username` is125   blank we emit `email` in its place (so a webmail login's address is not lost).126   If the orchestrator prefers email dropped, change the `e.username ?: e.email`127   fallbacks in both `PeachScriptExport.appendEntry` and `CsvExport.toCsv`.1282. **Only the six PeachScript fields are exported.** Card/passkey/tags/history129   fields are intentionally NOT in PeachScript or CSV (neither format nor the130   B2 importer model them). A full-fidelity vault backup would need a richer131   format — out of scope for C2, which targets interop with the B2 importer.1323. **Empty-vault gate** is enforced in the UI (`entryCount() == 0` disables the133   export button and shows `"Your vault is empty — nothing to export."`). The134   spec says "Empty vault" error if 0 entries; exact copy can be tuned.1354. **Strength gate copy** uses the spec's literal string136   `"Backup password too weak (score < 3)"` in breach colour.1375. **CSV MIME** is `text/csv` for the picker (stricter than Import's `"*/*"`,138   since we control the extension on write). Swap to `"*/*"` if a device's SAF139   provider rejects `text/csv`.1406. Entry-point wiring (Settings → Vault card → "Export" row) is out of scope for141   this draft — only the `PeachNav.Export` route swap is described.142```