# VAULT-ADOPTION Integration Contract Source of truth for integrating `/Users/june/Downloads/peach-vault-adoption-brief.md`. Where this contract and an individual draft disagree, this contract wins. ## Scope Draft only. No production source was modified in this pass. Integrate in browser first, then coordinate Android. `peach-sync` has no adoption work. Current browser drift from the brief: - Brief base says v4.0.3; current dev is v4.0.13. - `PAIR_IDENTITY` already supports optional `vault_id` and Pro `license`. - Sync pairing is already usable by non-Pro devices; at least one device needs Pro and license transfer already rides normal pairing. ## Files in `src/drafts/VAULT-ADOPTION/` | Draft file | Destination / role | |---|---| | `README.md` | Draft index; keep or trash after integration. | | `vault-createVaultFromRootKey.ts` | Code hunks for `src/background/vault.ts`. | | `vault-createVaultFromRootKey.test.ts` | Destination `src/background/vault-createVaultFromRootKey.test.ts`; T-A01..T-A05. | | `messages-CREATE_VAULT_FROM_ROOT_KEY.ts` | Code hunks for `src/shared/types.ts` and `src/background/messages.ts`. | | `messages-CREATE_VAULT_FROM_ROOT_KEY.test.ts` | Destination `src/background/messages.create-vault-from-root-key.test.ts`; T-A06..T-A09. | | `chirp-adopt.ts` | Destination `src/shared/chirp-adopt.ts`. | | `chirp-adopt.test.ts` | Destination `src/shared/chirp-adopt.test.ts`; T-A10..T-A15. | | `AdoptionConsentModal.tsx` | Destination `src/popup/components/AdoptionConsentModal.tsx` plus CSS hunk to `src/popup/popup.css`. | | `AdoptionConsentModal.test.tsx` | Destination `src/popup/components/AdoptionConsentModal.test.tsx`; T-A16..T-A20. | | `onboarding-adopt-path.tsx` | Destination `src/popup/views/AdoptVaultView.tsx` plus hunks for `Onboarding.tsx`, `Lock.tsx`, `App.tsx`. | | `onboarding-adopt-path.test.tsx` | Destination `src/popup/views/AdoptVaultView.test.tsx` plus one contracts test; T-A21..T-A25. | | `chirp-pair-adopt-source.ts` | Hunk for `src/popup/chirp-pair.ts`; requires a supporting responder-open-frame export from `src/shared/chirp-handshake.ts`. | | `chirp-pair-adopt-source.test.ts` | Destination `src/popup/chirp-pair.adopt-source.test.ts`; T-A26..T-A29. | | `INTEGRATION-CONTRACT.md` | This file. | ## Integration Order Follow phases 1 -> 2 -> 3 -> 4 -> 5 -> 6. Save each phase separately. 1. **Phase 1: vault root-key adoption** - Add `wrapExistingRootKeyForRecovery` and `createVaultFromRootKey` to `src/background/vault.ts`. - Import `computeVaultId`. - Run `npx tsc --noEmit`, relevant vault tests, then save. 2. **Phase 2: message handler** - Add `CREATE_VAULT_FROM_ROOT_KEY` to `MessageType`. - Add handler beside `CREATE_VAULT`. - Handler is vault-less allowed and returns `{ ok: true, vaultId }`. - Run message tests and save. 3. **Phase 3: adoption sub-protocol** - Add `src/shared/chirp-adopt.ts`. - Add tests T-A10..T-A15. - Important correction from the brief: receiver computes and checks `computeVaultId(root_key_hex)` before creating the vault. The brief's original order would allow partial local creation before detecting a mismatch. 4. **Phase 4: source consent UI** - Add `AdoptionConsentModal`. - Add amber CSS to `popup.css`. - Wire the modal through the source pairing UI event path. - Re-auth uses `VERIFY_MAIN_PASSWORD` or the existing `vault.verifyMainPassword` background path before allowing `grantAdoption`. 5. **Phase 5: receiver onboarding** - Add `"adopting"` to `App.tsx` view state. - Add `AdoptVaultView`. - Add final onboarding action: `I already have Peach ->`. - Receiver password is collected before chirp. Use `CREATE_VAULT_FROM_ROOT_KEY` only after SAS confirm and `ADOPT_GRANT` validation. 6. **Phase 6: source routing** - Refactor `chirp-handshake.ts` so responder can inspect the first encrypted JSON frame as `PAIR_IDENTITY` or `ADOPT_REQUEST`. - Normal pairing remains unchanged for `PAIR_IDENTITY`. - `ADOPT_REQUEST` runs `grantAdoption` only after SAS and consent pass. 7. **Version/build save** - Bump browser version from current dev version by at least PATCH. - Run `npm run build` after `tsc`/vitest. ## New Message Types Add to `src/shared/types.ts`: ```ts | "CREATE_VAULT_FROM_ROOT_KEY" ``` Payload: ```ts { password: string; rootKeyHex: string; } ``` Response: ```ts { ok: true; vaultId: string } | { ok: false; error: string } ``` Suggested error strings: - `invalid_payload` - `ADOPT_INVALID_PASSWORD` - `ADOPT_INVALID_ROOT_KEY: expected 64 lowercase hex chars` - `ADOPT_VAULT_ID_MISMATCH` ## New Frame Types Add in `src/shared/chirp-adopt.ts`: ```ts ADOPT_REQUEST ADOPT_GRANT ADOPT_IDENTITY ``` Do not overload `PAIR_IDENTITY`. Adoption is a sub-protocol that runs on the same Noise/SAS channel after chirp, with its own two-phase persistence rules. ## New View State Add to `App.tsx`: ```ts | "adopting" ``` `"adopting"` is vault-less allowed. If adoption succeeds: - Refresh `HAS_VAULT`. - Mark onboarding complete. - Open the normal unlocked app shell. - Trigger an immediate sync attempt if a paired source was persisted. ## Estimated Test Count 29 specs: - T-A01..T-A05: vault creation from adopted root key. - T-A06..T-A09: message handler. - T-A10..T-A15: adoption sub-protocol. - T-A16..T-A20: source consent modal. - T-A21..T-A25: receiver onboarding path. - T-A26..T-A29: source routing. ## Cross-Platform KAT Extensions Add a fixed adoption KAT shared with Android: - Fixed chirp ephemeral inputs. - Fixed source root key: 32 bytes. - Fixed source identity keys. - Expected SAS. - Expected `vault_id`. - Expected frame sequence: `ADOPT_REQUEST -> ADOPT_GRANT -> ADOPT_IDENTITY`. The KAT should prove Android and browser serialize the same snake_case frame fields and derive the same vault id from the same root key bytes. ## Android Work Required Do not implement in this browser draft, but the Android integration pass needs: - Pairing protocol layer: add `ADOPT_REQUEST`, `ADOPT_GRANT`, `ADOPT_IDENTITY`. - Vault layer: create local vault from imported root key with local password. - Source consent UI: warning modal with master-password re-auth. - Receiver onboarding: "I already have Peach" path, local password first. - License transfer: send/activate optional `license` in adoption frames, matching browser's normal pairing transfer behavior. - Tests mirroring T-A01..T-A29 plus the cross-platform KAT. Likely files in peach-android (names may differ): - pairing/chirp protocol module - vault repository / crypto storage module - onboarding Compose screens - source consent Compose dialog - paired-device repository - license activation path ## Security Checklist - [x] `root_key_hex` is validated as exactly 64 lowercase hex chars before use. - [x] Decoded root key bytes are zeroed after `createVaultFromRootKey` returns. - [x] Drafts never log `root_key_hex`. - [x] `ADOPT_GRANT` is sent only after the regular SAS gate. - [x] `ADOPT_GRANT` is sent only after source master-password re-auth succeeds. - [x] Source does not persist the receiver until `ADOPT_IDENTITY` is received. - [x] Receiver aborts before creating a vault if `vault_id` does not match the granted root key. - [x] Receiver stays vault-less on invalid root key, unexpected frame, SAS cancel, source consent reject, and vault-id mismatch. - [x] `ADOPT_REQUEST` from a peer without adoption support results in an unexpected-frame/error path, not a grant. - [x] Consent modal copy says "copies your entire vault including all passwords and Pro". ## Known Integration Corrections - The brief suggests `rootKeyHex.split('').fill('0')`; that does not zero the original immutable string. Integrate byte-buffer zeroing and keep string scope narrow. - `createVaultFromRootKey` must set `rootKey = new Uint8Array(rootKeyBytes)`, not `rootKey = rootKeyBytes`, because the input buffer is wiped in `finally`. - The current `runPairingResponder` cannot handle adoption as-is because it decodes the first encrypted frame as `PAIR_IDENTITY`. Refactor around an open-frame responder primitive, then rebuild normal pairing on top.