B2-INTEGRATION.md

7.4 KB · raw

rendered · source

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

@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:

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)

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:

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)
}

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:

{
  "peachscript": 1,
  "encrypted": false,
  "entries": [
    { "title": "...", "username": "...", "password": "...",
      "url": "...", "notes": "...", "totp": "..." }
  ]
}

Encrypted (backup-password protected):

{
  "peachscript": 1,
  "encrypted": true,
  "kdf": "argon2id",
  "salt": "<base64, 16 bytes = VaultCrypto.SALT_SIZE>",
  "blob": "<base64: 12-byte nonce || AES-256-GCM ciphertext||tag>"
}

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).

Spec error strings (in ImportScreen.ImportErrors)

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.