// DRAFT DESTINATION: src/background/vault.ts // Phase 1: add createVaultFromRootKey(password, rootKeyHex). // // Integration notes: // - Add `import { computeVaultId } from "../shared/sync-identity";` near the // existing imports if vault.ts does not already import it. // - Add `wrapExistingRootKeyForRecovery` beside generateAndWrapRootKey. // - Add createVaultFromRootKey beside createVault. async function wrapExistingRootKeyForRecovery(rootKeyBytes: Uint8Array): Promise<{ recoverySalt: string; wrappedRootKey: string; wrappedNonce: string; wrappedTag: string; }> { if (rootKeyBytes.length !== 32) { throw new Error("ADOPT_INVALID_ROOT_KEY: expected 32 bytes"); } const words = await rootKeyToWords(rootKeyBytes); const wordMaterial = await wordsToRootKeyMaterial(words); const recoverySalt = crypto.getRandomValues(new Uint8Array(16)); const recoveryKey = await deriveRecoveryKey(wordMaterial, recoverySalt); const nonce = crypto.getRandomValues(new Uint8Array(12)); const encoded = new TextEncoder().encode(hex(rootKeyBytes)); const raw = await crypto.subtle.encrypt( { name: "AES-GCM", iv: nonce, additionalData: new Uint8Array([1]).buffer as ArrayBuffer, tagLength: 128, }, recoveryKey, encoded, ); const rawU8 = new Uint8Array(raw); return { recoverySalt: hex(recoverySalt), wrappedRootKey: hex(rawU8.slice(0, rawU8.length - 16)), wrappedNonce: hex(nonce), wrappedTag: hex(rawU8.slice(rawU8.length - 16)), }; } /** * Creates a local vault from an existing vault root key received through the * SAS-gated adoption protocol. The password is local to this device; it wraps * the adopted root key at rest and does not need to match the source device. */ export async function createVaultFromRootKey( password: string, rootKeyHex: string, ): Promise<{ vaultId: string }> { if (typeof password !== "string" || password.length === 0) { throw new Error("ADOPT_INVALID_PASSWORD"); } if (typeof rootKeyHex !== "string" || !/^[0-9a-f]{64}$/.test(rootKeyHex)) { throw new Error("ADOPT_INVALID_ROOT_KEY: expected 64 lowercase hex chars"); } const rootKeyBytes = unhex(rootKeyHex); if (rootKeyBytes.length !== 32) { rootKeyBytes.fill(0); throw new Error("ADOPT_INVALID_ROOT_KEY: expected 32 bytes"); } try { await ensureMigrated(); const salt = generateSalt(); derivedKey = await deriveKey(password, salt); entries = []; vaultMode = "real"; duressConfigured = false; buildDomainIndexSync(entries); const vaultId = await computeVaultId(rootKeyBytes); const recovery = await wrapExistingRootKeyForRecovery(rootKeyBytes); // Copy into the unlocked-session cache. Do not alias rootKeyBytes because // the finally block below wipes the input buffer. rootKey = new Uint8Array(rootKeyBytes); const plaintext = encodePayload(entries, duressConfigured, duressConfigured ? DURESS_SLOT_SIZE : 0); const { nonce, ciphertext, tag } = await encrypt(derivedKey, plaintext, "slot_a_v1"); const pwRoot = await wrapRootKeyForDerivedKey(derivedKey, rootKey); const slotA: StoredVault = { version: 1, salt: hex(salt), nonce, ciphertext, tag, recovery_salt: recovery.recoverySalt, recovery_wrapped_root_key: recovery.wrappedRootKey, recovery_nonce: recovery.wrappedNonce, recovery_tag: recovery.wrappedTag, ...pwRoot, }; await writeSlot(SLOT_A, slotA); await writeSlot(SLOT_B, generateNoiseBlob(ciphertext.length / 2)); await stampLocalVaultMutation(); touchAutoLock(); return { vaultId }; } finally { rootKeyBytes.fill(0); } }