12345678910async 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}44454647484950export 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 81 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