B2-INTEGRATION.md

7.4 KB · raw

rendered · source

1# B2 — Import Screen: Integration Note23Draft implementation of spec B2 (lines 110–163). Three Kotlin files plus this note,4all under `app/drafts/`. They are written to drop straight into the source tree —5package is already `com.peachpasswords.ui.vault`.67## Files to MOVE into the source set89Move these from `app/drafts/` to10`app/src/main/java/com/peachpasswords/ui/vault/` (package already correct —11`package com.peachpasswords.ui.vault`):1213| Draft file | Destination | Contents |14|---|---|---|15| `CsvImport.kt` | `ui/vault/CsvImport.kt` | Pure Kotlin: `ParsedEntry`, `ImportParseResult`, `CsvImport` (provider detection + CSV/JSON parsers, RFC-4180 splitter) |16| `PeachScriptImport.kt` | `ui/vault/PeachScriptImport.kt` | Pure Kotlin: PeachScript sniff/detect/decrypt/parse |17| `ImportScreen.kt` | `ui/vault/ImportScreen.kt` | Compose UI + `ImportViewModel` |1819`B2-INTEGRATION.md` stays in drafts (or delete after wiring) — it is not source.2021No edits were made to any existing file. `PeachNav.kt`, `PeachAppNavHost.kt`,22`VaultRepository.kt`, `VaultEntry.kt`, `VaultCrypto.kt` were read only.2324## Public composable signature2526```kotlin27@Composable28fun ImportScreen(onBack: () -> Unit)29// (also has a defaulted vm = hiltViewModel() param, like TrashScreen)30```3132## Nav wiring3334`PeachNav.Import` route already exists. In `PeachAppNavHost.kt`, replace the35`Import` route's `SimpleStub` with:3637```kotlin38ImportScreen(onBack = back)39```4041where `back` is the same back lambda the other vault screens use (e.g. how42`TrashScreen(onBack = back)` is wired). No new route, no nav-graph change needed.4344Note: `ImportScreen` also handles internal back navigation itself (preview/error →45input view, sub-view → choose view); it only calls `onBack()` when at the top46choose view or after Done. So a single `onBack=back` is correct.4748## VaultRepository methods used (all already exist — no new API assumed)4950- `suspend fun addEntry(draft: VaultEntry): VaultEntry` — import loops this51  per parsed entry (confirmed: no batch method exists).52- `fun getEntries(): List<VaultEntry>` — used by REPLACE mode to enumerate53  current active entries.54- `suspend fun deleteEntry(id: String)` — REPLACE soft-deletes existing active55  entries before adding the imported set. (Chose soft-delete over a hard wipe so56  a mistaken "Replace" is recoverable from Trash. If the orchestrator wants a true57  replace, swap to `purgeEntry` or `destroyVault` — but that is destructive.)5859`VaultEntry` drafts are built with `id = ""` (repo assigns UUID), `type = "login"`,60and `createdAt/modifiedAt = now` (repo overrides modifiedAt with a monotonic stamp).6162## VaultCrypto API found (quoted from source)6364From `app/src/main/java/com/peachpasswords/vault/VaultCrypto.kt`:6566```kotlin67object VaultCrypto {68    const val ARGON2_MEMORY_KB = 6553669    const val ARGON2_ITERATIONS = 370    const val ARGON2_PARALLELISM = 471    const val ARGON2_HASH_LENGTH = 3272    const val SALT_SIZE = 1673    const val NONCE_SIZE = 1274    const val TAG_LENGTH_BITS = 12875    const val KEY_SIZE = 327677    fun deriveKey(password: String, salt: ByteArray): ByteArray   // Argon2id (BouncyCastle)78    fun encrypt(plaintext: ByteArray, key: ByteArray, aad: String): ByteArray  // nonce(12) || ct||tag79    fun decrypt(data: ByteArray, key: ByteArray, aad: String): ByteArray       // expects nonce(12) prefix80    fun generateSalt(): ByteArray   // 16 bytes81    fun zero(bytes: ByteArray)82}83```8485- KDF: Argon2id, 64 MiB memory, 3 iterations, parallelism 4, 32-byte output.86- AEAD: AES-256-GCM, 128-bit tag, 12-byte random nonce **prepended** to the87  ciphertext (`encrypt` returns `nonce + cipher.doFinal(...)`; `decrypt` reads the88  first 12 bytes as the nonce). AAD is a UTF-8 string argument.8990`PeachScriptImport.decryptAndParse` is implemented end-to-end against this API91(derive → decrypt → parse), since the on-the-wire shape is fully specified by the92header format below. A GCM tag mismatch surfaces as `WrongPassword`.9394## PeachScript header / export format chosen (C2 export MUST mirror this)9596A PeachScript export is a single JSON object carrying a magic marker key97`"peachscript"` (also the format version).9899**Plaintext:**100```json101{102  "peachscript": 1,103  "encrypted": false,104  "entries": [105    { "title": "...", "username": "...", "password": "...",106      "url": "...", "notes": "...", "totp": "..." }107  ]108}109```110111**Encrypted (backup-password protected):**112```json113{114  "peachscript": 1,115  "encrypted": true,116  "kdf": "argon2id",117  "salt": "<base64, 16 bytes = VaultCrypto.SALT_SIZE>",118  "blob": "<base64: 12-byte nonce || AES-256-GCM ciphertext||tag>"119}120```121122- `blob` is exactly `VaultCrypto.encrypt(plaintextJsonBytes, key, AAD)` output123  (nonce prepended). `salt` feeds `VaultCrypto.deriveKey(backupPassword, salt)`.124- Decrypted plaintext is the UTF-8 bytes of the **plaintext-shape** JSON object125  above (with `"encrypted": false`).126- **AAD is the constant string** `"peachscript_export_v1"`127  (`PeachScriptImport.PEACHSCRIPT_AAD`). C2 export must encrypt with this exact128  AAD or the GCM tag check fails and the user sees "Incorrect backup password."129- Detection: `looksLikePeachScript` = trimmed text starts with `{` and contains130  `"peachscript"`. `isEncrypted` = contains `"encrypted": true`.131  Standard base64 (not URL-safe) is used for `salt`/`blob`.132133## CSV / provider detection summary134135`CsvImport.detectProvider(content)` returns one of:136`"Bitwarden CSV"`, `"Bitwarden JSON"`, `"1Password CSV"`, `"Chrome CSV"`,137`"Edge CSV"`, `"Safari CSV"`, `"CSV"` (generic), or `null` (unrecognised).138139- Chrome and Edge share the identical Chromium header140  (`name,url,username,password`) and are indistinguishable from content alone —141  detection returns `"Chrome CSV"`; UI copy says "Chrome/Edge". `parse` treats142  both identically.143- Bitwarden JSON detection is a shape check (`{` + `"items": [`); only login144  items (`type` absent or `== 1`) are imported.145- The CSV parser is a single RFC-4180-ish state machine handling quoted fields,146  embedded commas, doubled quotes (`""`→`"`), and CR/LF inside quotes.147- `detectAndParse` returns a sealed `ImportParseResult` mapping to the spec error148  strings (UNRECOGNISED / CORRUPTED / NO_ENTRIES).149150## Spec error strings (in `ImportScreen.ImportErrors`)151152- Unrecognised PeachScript: "This doesn't look like a PeachScript export."153- Wrong backup password: "Incorrect backup password."154- Corrupted: "This export appears to be corrupted."155- No entries: "No entries found in this export."156- (extras) "Couldn't read that file." / "Unrecognised file format."157158## Assumptions / things for the orchestrator to confirm1591601. REPLACE uses soft-delete (recoverable via Trash). Change to a hard wipe if161   the spec's `RESTORE_ENTRIES` semantics require it.1622. `GetContent()` launched with `"*/*"` (not `.csv,.json`) because some exporters163   report odd MIME types for CSV; the parser sniffs content regardless. Swap to164   `OpenDocument()` with `arrayOf("text/csv","application/json")` if a stricter165   picker is preferred.1663. The minimal hand-rolled JSON scanners in `CsvImport`/`PeachScriptImport` avoid167   adding a JSON dependency to the pure modules. If `kotlinx.serialization` is168   already on the classpath (it is — `VaultEntry` is `@Serializable`), these could169   be swapped for a typed decode later; kept dependency-free here for testability.1704. Entry-point wiring (Lock screen RestoreTabs pills, Settings → Vault → Import171   row) is out of scope for this draft — only the `PeachNav.Import` route swap is172   described. Those entry points just need to navigate to the existing route.