vault-createVaultFromRootKey.ts

3.6 KB · raw

1// DRAFT DESTINATION: src/background/vault.ts2// Phase 1: add createVaultFromRootKey(password, rootKeyHex).3//4// Integration notes:5// - Add `import { computeVaultId } from "../shared/sync-identity";` near the6//   existing imports if vault.ts does not already import it.7// - Add `wrapExistingRootKeyForRecovery` beside generateAndWrapRootKey.8// - Add createVaultFromRootKey beside createVault.910async function wrapExistingRootKeyForRecovery(rootKeyBytes: Uint8Array): Promise<{11  recoverySalt: string;12  wrappedRootKey: string;13  wrappedNonce: string;14  wrappedTag: string;15}> {16  if (rootKeyBytes.length !== 32) {17    throw new Error("ADOPT_INVALID_ROOT_KEY: expected 32 bytes");18  }1920  const words = await rootKeyToWords(rootKeyBytes);21  const wordMaterial = await wordsToRootKeyMaterial(words);22  const recoverySalt = crypto.getRandomValues(new Uint8Array(16));23  const recoveryKey = await deriveRecoveryKey(wordMaterial, recoverySalt);24  const nonce = crypto.getRandomValues(new Uint8Array(12));25  const encoded = new TextEncoder().encode(hex(rootKeyBytes));26  const raw = await crypto.subtle.encrypt(27    {28      name: "AES-GCM",29      iv: nonce,30      additionalData: new Uint8Array([1]).buffer as ArrayBuffer,31      tagLength: 128,32    },33    recoveryKey,34    encoded,35  );36  const rawU8 = new Uint8Array(raw);37  return {38    recoverySalt: hex(recoverySalt),39    wrappedRootKey: hex(rawU8.slice(0, rawU8.length - 16)),40    wrappedNonce: hex(nonce),41    wrappedTag: hex(rawU8.slice(rawU8.length - 16)),42  };43}4445/**46 * Creates a local vault from an existing vault root key received through the47 * SAS-gated adoption protocol. The password is local to this device; it wraps48 * the adopted root key at rest and does not need to match the source device.49 */50export async function createVaultFromRootKey(51  password: string,52  rootKeyHex: string,53): Promise<{ vaultId: string }> {54  if (typeof password !== "string" || password.length === 0) {55    throw new Error("ADOPT_INVALID_PASSWORD");56  }57  if (typeof rootKeyHex !== "string" || !/^[0-9a-f]{64}$/.test(rootKeyHex)) {58    throw new Error("ADOPT_INVALID_ROOT_KEY: expected 64 lowercase hex chars");59  }6061  const rootKeyBytes = unhex(rootKeyHex);62  if (rootKeyBytes.length !== 32) {63    rootKeyBytes.fill(0);64    throw new Error("ADOPT_INVALID_ROOT_KEY: expected 32 bytes");65  }6667  try {68    await ensureMigrated();6970    const salt = generateSalt();71    derivedKey = await deriveKey(password, salt);72    entries = [];73    vaultMode = "real";74    duressConfigured = false;75    buildDomainIndexSync(entries);7677    const vaultId = await computeVaultId(rootKeyBytes);78    const recovery = await wrapExistingRootKeyForRecovery(rootKeyBytes);7980    // Copy into the unlocked-session cache. Do not alias rootKeyBytes because81    // the finally block below wipes the input buffer.82    rootKey = new Uint8Array(rootKeyBytes);8384    const plaintext = encodePayload(entries, duressConfigured, duressConfigured ? DURESS_SLOT_SIZE : 0);85    const { nonce, ciphertext, tag } = await encrypt(derivedKey, plaintext, "slot_a_v1");86    const pwRoot = await wrapRootKeyForDerivedKey(derivedKey, rootKey);8788    const slotA: StoredVault = {89      version: 1,90      salt: hex(salt),91      nonce,92      ciphertext,93      tag,94      recovery_salt: recovery.recoverySalt,95      recovery_wrapped_root_key: recovery.wrappedRootKey,96      recovery_nonce: recovery.wrappedNonce,97      recovery_tag: recovery.wrappedTag,98      ...pwRoot,99    };100    await writeSlot(SLOT_A, slotA);101    await writeSlot(SLOT_B, generateNoiseBlob(ciphertext.length / 2));102    await stampLocalVaultMutation();103    touchAutoLock();104105    return { vaultId };106  } finally {107    rootKeyBytes.fill(0);108  }109}110