# C2 — Export Screen: Integration Note Draft implementation of spec C2 (lines 289–337). Three Kotlin files plus this note, all under `app/drafts/`. Package is already `com.peachpasswords.ui.vault`, so they drop straight into the source tree. No existing file was edited (PeachNav.kt, PeachAppNavHost.kt, VaultRepository.kt, VaultCrypto.kt, VaultEntry.kt, ImportScreen.kt, RecoveryWordsScreen.kt, theme files were read only). ## Files to MOVE into the source set | Draft file | Destination | Contents | |---|---|---| | `PeachScriptExport.kt` | `ui/vault/PeachScriptExport.kt` | Pure Kotlin: `buildPlaintext` / `buildEncrypted`, hand-rolled JSON with escaping | | `CsvExport.kt` | `ui/vault/CsvExport.kt` | Pure Kotlin: `toCsv` + `encodeField` (RFC-4180 + formula-injection guard) | | `ExportScreen.kt` | `ui/vault/ExportScreen.kt` | Compose UI + `ExportViewModel` + `scorePassword`/`strengthLabel` | `C2-INTEGRATION.md` stays in drafts (or delete after wiring) — it is not source. ## Public composable signature ```kotlin @Composable fun ExportScreen(onBack: () -> Unit) // (also has a defaulted vm = hiltViewModel() param, like ImportScreen / TrashScreen) ``` ## Nav wiring (one line) `PeachNav.Export` route already exists. In `PeachAppNavHost.kt`, replace the `Export` route's `SimpleStub` with: ```kotlin ExportScreen(onBack = back) ``` where `back` is the same back lambda the other vault screens use (e.g. how `ImportScreen(onBack = back)` is wired). No new route, no nav-graph change. `ExportScreen` handles its own internal back: a sub-view (PeachScript / CSV) returns to the choose view; the choose view calls `onBack()`. A single `onBack=back` is correct. ## Strength scorer thresholds chosen `scorePassword(pw): Int` returns 0–4 (local, dependency-free, no network): - +1 if length ≥ 8 - +1 if length ≥ 12 - +1 if ≥ 3 of {lowercase, uppercase, digit, symbol} present - +1 if all 4 classes present - empty → 0, clamped to 0..4 Labels: 0 very weak · 1 weak · 2 fair · 3 good · 4 strong. The C2 gate is **score ≥ 3 ("good")**. `[Generate backup]` is disabled until `score ≥ 3` AND the two password fields match AND the vault is non-empty. When a non-empty password scores < 3 the breach-colour error `"Backup password too weak (score < 3)"` is shown (spec line 306). The 4-segment bar fills `score` segments and tints to the accent once "good". ## Round-trip confirmation (buildEncrypted ⇄ PeachScriptImport) `PeachScriptImport.decryptAndParse(buildEncrypted(es, pw), pw)` returns `Success` with the same six fields per entry. Reasoning, field by field against the pinned format in `B2-INTEGRATION.md` and the importer source: 1. **Envelope shape.** `buildEncrypted` emits `{"peachscript":1,"encrypted":true,"kdf":"argon2id","salt":"","blob":""}`. The importer reads `salt` and `blob` via `jsonRawString` (a `"key":"..."` regex) — key names and JSON-string shape match exactly. 2. **Base64.** Both sides use `java.util.Base64` standard (non-URL-safe) codec: `getEncoder()` here, `getDecoder()` there. Salt is 16 bytes (`VaultCrypto.generateSalt()` = `SALT_SIZE`); blob is `nonce(12) || ct||tag`, so `blob.size > NONCE_SIZE` — passes the importer's length guards. 3. **KDF.** `salt` feeds `VaultCrypto.deriveKey(pw, salt)` on both sides (Argon2id, identical params from the shared `VaultCrypto` constants). 4. **AEAD + AAD.** `blob` is exactly `VaultCrypto.encrypt(plaintextBytes, key, PeachScriptImport.PEACHSCRIPT_AAD)`. The importer calls `VaultCrypto.decrypt(blob, key, PEACHSCRIPT_AAD)` with the **same constant** AAD (`"peachscript_export_v1"`). `encrypt` prepends the 12-byte nonce; `decrypt` strips the first 12 bytes as the nonce. The GCM tag therefore verifies, so wrong-password / tamper paths surface as `DecryptResult.WrongPassword` exactly as designed. 5. **Inner plaintext.** The decrypted bytes are `buildPlaintext(es)` = `{"peachscript":1,"encrypted":false,"entries":[{...}]}`. The importer's `parsePlaintext` uses `extractArrayBody("entries")` + `splitTopLevelObjects` + `jsonString` for `title/username/password/url/ notes/totp` — the exact keys this writer emits. 6. **Escaping.** `buildPlaintext` escapes `\`, `"`, and control chars (`\n \r \t \b \f`, `\uXXXX` for the rest). The importer's `unescapeJson` reverses `\n \t \r \" \\ \/ \uXXXX`, so any value round-trips. (We never emit `\/`; forward slash is left literal, which is valid JSON and read back verbatim.) The only normalisation is field projection (see assumptions): empty strings become `null` on the way back because the importer's `jsonString` maps `""` → `null`, and `parsePlaintext` drops entries whose `title` is blank. So entries with a non-blank title and otherwise-null optional fields round-trip to the same `ParsedEntry` shape. ## VaultRepository methods used (all already exist — no new API assumed) - `fun getEntries(): List` — returns active (non-deleted) entries (verified: `entryList.filter { it.deletedAt == null }`). Used for both the count/empty-vault gate and the export payload. `ExportViewModel` injects `VaultRepository` + `@ApplicationContext Context` (for SAF `openOutputStream`), exactly like `ImportViewModel`. ## SAF / clipboard patterns - PeachScript save: `CreateDocument("*/*")`, suggested name `peach-backup.peach` (mirrors `RecoveryWordsScreen`'s `CreateDocument` save pattern). `"*/*"` is used because there is no registered MIME type for `.peach`. - CSV save: `CreateDocument("text/csv")`, suggested name `peach-export.csv`. - Copy to clipboard uses `ClipboardManager` / `ClipData.newPlainText("peach backup", json)` — same approach as RecoveryWordsScreen's "copy all words". ## Assumptions / things for the orchestrator to confirm 1. **`email` → `username` fold.** PeachScript and the CSV `username` column have no `email` field, and the importer reads only six keys. When `username` is blank we emit `email` in its place (so a webmail login's address is not lost). If the orchestrator prefers email dropped, change the `e.username ?: e.email` fallbacks in both `PeachScriptExport.appendEntry` and `CsvExport.toCsv`. 2. **Only the six PeachScript fields are exported.** Card/passkey/tags/history fields are intentionally NOT in PeachScript or CSV (neither format nor the B2 importer model them). A full-fidelity vault backup would need a richer format — out of scope for C2, which targets interop with the B2 importer. 3. **Empty-vault gate** is enforced in the UI (`entryCount() == 0` disables the export button and shows `"Your vault is empty — nothing to export."`). The spec says "Empty vault" error if 0 entries; exact copy can be tuned. 4. **Strength gate copy** uses the spec's literal string `"Backup password too weak (score < 3)"` in breach colour. 5. **CSV MIME** is `text/csv` for the picker (stricter than Import's `"*/*"`, since we control the extension on write). Swap to `"*/*"` if a device's SAF provider rejects `text/csv`. 6. Entry-point wiring (Settings → Vault card → "Export" row) is out of scope for this draft — only the `PeachNav.Export` route swap is described. ```