# B2 — Import Screen: Integration Note Draft implementation of spec B2 (lines 110–163). Three Kotlin files plus this note, all under `app/drafts/`. They are written to drop straight into the source tree — package is already `com.peachpasswords.ui.vault`. ## Files to MOVE into the source set Move these from `app/drafts/` to `app/src/main/java/com/peachpasswords/ui/vault/` (package already correct — `package com.peachpasswords.ui.vault`): | Draft file | Destination | Contents | |---|---|---| | `CsvImport.kt` | `ui/vault/CsvImport.kt` | Pure Kotlin: `ParsedEntry`, `ImportParseResult`, `CsvImport` (provider detection + CSV/JSON parsers, RFC-4180 splitter) | | `PeachScriptImport.kt` | `ui/vault/PeachScriptImport.kt` | Pure Kotlin: PeachScript sniff/detect/decrypt/parse | | `ImportScreen.kt` | `ui/vault/ImportScreen.kt` | Compose UI + `ImportViewModel` | `B2-INTEGRATION.md` stays in drafts (or delete after wiring) — it is not source. No edits were made to any existing file. `PeachNav.kt`, `PeachAppNavHost.kt`, `VaultRepository.kt`, `VaultEntry.kt`, `VaultCrypto.kt` were read only. ## Public composable signature ```kotlin @Composable fun ImportScreen(onBack: () -> Unit) // (also has a defaulted vm = hiltViewModel() param, like TrashScreen) ``` ## Nav wiring `PeachNav.Import` route already exists. In `PeachAppNavHost.kt`, replace the `Import` route's `SimpleStub` with: ```kotlin ImportScreen(onBack = back) ``` where `back` is the same back lambda the other vault screens use (e.g. how `TrashScreen(onBack = back)` is wired). No new route, no nav-graph change needed. Note: `ImportScreen` also handles internal back navigation itself (preview/error → input view, sub-view → choose view); it only calls `onBack()` when at the top choose view or after Done. So a single `onBack=back` is correct. ## VaultRepository methods used (all already exist — no new API assumed) - `suspend fun addEntry(draft: VaultEntry): VaultEntry` — import loops this per parsed entry (confirmed: no batch method exists). - `fun getEntries(): List` — used by REPLACE mode to enumerate current active entries. - `suspend fun deleteEntry(id: String)` — REPLACE soft-deletes existing active entries before adding the imported set. (Chose soft-delete over a hard wipe so a mistaken "Replace" is recoverable from Trash. If the orchestrator wants a true replace, swap to `purgeEntry` or `destroyVault` — but that is destructive.) `VaultEntry` drafts are built with `id = ""` (repo assigns UUID), `type = "login"`, and `createdAt/modifiedAt = now` (repo overrides modifiedAt with a monotonic stamp). ## VaultCrypto API found (quoted from source) From `app/src/main/java/com/peachpasswords/vault/VaultCrypto.kt`: ```kotlin object VaultCrypto { const val ARGON2_MEMORY_KB = 65536 const val ARGON2_ITERATIONS = 3 const val ARGON2_PARALLELISM = 4 const val ARGON2_HASH_LENGTH = 32 const val SALT_SIZE = 16 const val NONCE_SIZE = 12 const val TAG_LENGTH_BITS = 128 const val KEY_SIZE = 32 fun deriveKey(password: String, salt: ByteArray): ByteArray // Argon2id (BouncyCastle) fun encrypt(plaintext: ByteArray, key: ByteArray, aad: String): ByteArray // nonce(12) || ct||tag fun decrypt(data: ByteArray, key: ByteArray, aad: String): ByteArray // expects nonce(12) prefix fun generateSalt(): ByteArray // 16 bytes fun zero(bytes: ByteArray) } ``` - KDF: Argon2id, 64 MiB memory, 3 iterations, parallelism 4, 32-byte output. - AEAD: AES-256-GCM, 128-bit tag, 12-byte random nonce **prepended** to the ciphertext (`encrypt` returns `nonce + cipher.doFinal(...)`; `decrypt` reads the first 12 bytes as the nonce). AAD is a UTF-8 string argument. `PeachScriptImport.decryptAndParse` is implemented end-to-end against this API (derive → decrypt → parse), since the on-the-wire shape is fully specified by the header format below. A GCM tag mismatch surfaces as `WrongPassword`. ## PeachScript header / export format chosen (C2 export MUST mirror this) A PeachScript export is a single JSON object carrying a magic marker key `"peachscript"` (also the format version). **Plaintext:** ```json { "peachscript": 1, "encrypted": false, "entries": [ { "title": "...", "username": "...", "password": "...", "url": "...", "notes": "...", "totp": "..." } ] } ``` **Encrypted (backup-password protected):** ```json { "peachscript": 1, "encrypted": true, "kdf": "argon2id", "salt": "", "blob": "" } ``` - `blob` is exactly `VaultCrypto.encrypt(plaintextJsonBytes, key, AAD)` output (nonce prepended). `salt` feeds `VaultCrypto.deriveKey(backupPassword, salt)`. - Decrypted plaintext is the UTF-8 bytes of the **plaintext-shape** JSON object above (with `"encrypted": false`). - **AAD is the constant string** `"peachscript_export_v1"` (`PeachScriptImport.PEACHSCRIPT_AAD`). C2 export must encrypt with this exact AAD or the GCM tag check fails and the user sees "Incorrect backup password." - Detection: `looksLikePeachScript` = trimmed text starts with `{` and contains `"peachscript"`. `isEncrypted` = contains `"encrypted": true`. Standard base64 (not URL-safe) is used for `salt`/`blob`. ## CSV / provider detection summary `CsvImport.detectProvider(content)` returns one of: `"Bitwarden CSV"`, `"Bitwarden JSON"`, `"1Password CSV"`, `"Chrome CSV"`, `"Edge CSV"`, `"Safari CSV"`, `"CSV"` (generic), or `null` (unrecognised). - Chrome and Edge share the identical Chromium header (`name,url,username,password`) and are indistinguishable from content alone — detection returns `"Chrome CSV"`; UI copy says "Chrome/Edge". `parse` treats both identically. - Bitwarden JSON detection is a shape check (`{` + `"items": [`); only login items (`type` absent or `== 1`) are imported. - The CSV parser is a single RFC-4180-ish state machine handling quoted fields, embedded commas, doubled quotes (`""`→`"`), and CR/LF inside quotes. - `detectAndParse` returns a sealed `ImportParseResult` mapping to the spec error strings (UNRECOGNISED / CORRUPTED / NO_ENTRIES). ## Spec error strings (in `ImportScreen.ImportErrors`) - Unrecognised PeachScript: "This doesn't look like a PeachScript export." - Wrong backup password: "Incorrect backup password." - Corrupted: "This export appears to be corrupted." - No entries: "No entries found in this export." - (extras) "Couldn't read that file." / "Unrecognised file format." ## Assumptions / things for the orchestrator to confirm 1. REPLACE uses soft-delete (recoverable via Trash). Change to a hard wipe if the spec's `RESTORE_ENTRIES` semantics require it. 2. `GetContent()` launched with `"*/*"` (not `.csv,.json`) because some exporters report odd MIME types for CSV; the parser sniffs content regardless. Swap to `OpenDocument()` with `arrayOf("text/csv","application/json")` if a stricter picker is preferred. 3. The minimal hand-rolled JSON scanners in `CsvImport`/`PeachScriptImport` avoid adding a JSON dependency to the pure modules. If `kotlinx.serialization` is already on the classpath (it is — `VaultEntry` is `@Serializable`), these could be swapped for a typed decode later; kept dependency-free here for testability. 4. Entry-point wiring (Lock screen RestoreTabs pills, Settings → Vault → Import row) is out of scope for this draft — only the `PeachNav.Import` route swap is described. Those entry points just need to navigate to the existing route.