diff --git a/extension/src/service-worker/vault.ts b/extension/src/service-worker/vault.ts index 29df581..d12cb89 100644 --- a/extension/src/service-worker/vault.ts +++ b/extension/src/service-worker/vault.ts @@ -21,6 +21,32 @@ function requireWasm(): any { return wasm; } +const DEFAULT_PARAMS_JSON = '{"argon2_m":65536,"argon2_t":3,"argon2_p":4}'; + +/// Register this device on the remote (devices.json) and persist the vault +/// config + reference image locally so future unlocks work. Shared by the +/// create and attach flows — both finish with this identical tail. +async function registerDeviceAndPersistConfig( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + w: any, + git: GitHost, + config: VaultConfig, + referenceImageBytes: Uint8Array, + deviceName: string, +): Promise { + const keys = w.register_device(deviceName) as { signing_public_key: string }; + await devices.addDevice(git, { + name: deviceName, + public_key: keys.signing_public_key, + added_at: Math.floor(Date.now() / 1000), + }); + await chrome.storage.local.set({ + vaultConfig: config, + imageBase64: uint8ArrayToBase64(referenceImageBytes), + device_name: deviceName, + }); +} + export async function handleCreateVault( msg: { config: VaultConfig; passphrase: string; carrierImageBytes: ArrayBuffer; deviceName: string }, state: PopupState, @@ -35,35 +61,25 @@ export async function handleCreateVault( const salt = new Uint8Array(32); crypto.getRandomValues(salt); - const paramsJson = '{"argon2_m":65536,"argon2_t":3,"argon2_p":4}'; - handle = w.unlock(msg.passphrase, referenceImageBytes, salt, paramsJson); + // Capture the unlock result in a non-null binding for the in-scope ops; + // `handle` stays the ownership tracker the finally block cleans up. + const h: SessionHandle = w.unlock(msg.passphrase, referenceImageBytes, salt, DEFAULT_PARAMS_JSON); + handle = h; - const encryptedManifest = new Uint8Array(w.manifest_encrypt(handle, '{"schema_version":2,"items":{}}')); - const settingsJson = w.default_vault_settings_json(); - const encryptedSettings = new Uint8Array(w.settings_encrypt(handle, settingsJson)); + const encryptedManifest = new Uint8Array(w.manifest_encrypt(h, '{"schema_version":2,"items":{}}')); + const encryptedSettings = new Uint8Array(w.settings_encrypt(h, w.default_vault_settings_json())); const { config } = msg; const git = createGitHost(config.hostType, config.hostUrl, config.repoPath, config.apiToken); await git.writeFileCreateOnly('.relicario/salt', salt, 'init: vault salt'); - await git.writeFileCreateOnly('.relicario/params.json', new TextEncoder().encode(paramsJson), 'init: KDF parameters'); + await git.writeFileCreateOnly('.relicario/params.json', new TextEncoder().encode(DEFAULT_PARAMS_JSON), 'init: KDF parameters'); await git.writeFileCreateOnly('manifest.enc', encryptedManifest, 'init: encrypted manifest'); await git.writeFileCreateOnly('settings.enc', encryptedSettings, 'init: encrypted settings'); - const keys = w.register_device(msg.deviceName) as { signing_public_key: string; deploy_public_key: string }; - await devices.addDevice(git, { - name: msg.deviceName, - public_key: keys.signing_public_key, - added_at: Math.floor(Date.now() / 1000), - }); - - await chrome.storage.local.set({ - vaultConfig: config, - imageBase64: uint8ArrayToBase64(referenceImageBytes), - device_name: msg.deviceName, - }); + await registerDeviceAndPersistConfig(w, git, config, referenceImageBytes, msg.deviceName); // SW now owns the unlocked session — keeps the handle alive (enables recoveryQrAvailable). - session.setCurrent(handle); + session.setCurrent(h); state.gitHost = git; state.manifest = { schema_version: 2, items: {} } as Manifest; handle = null; // ownership transferred — do NOT lock-and-free in finally @@ -92,28 +108,21 @@ export async function handleAttachVault( const { config } = msg; const git = createGitHost(config.hostType, config.hostUrl, config.repoPath, config.apiToken); - const meta = await fetchVaultMeta(git); - const encryptedManifest = await git.readFile('manifest.enc'); + // The vault metadata and manifest are independent read-only GETs — fan out. + const [meta, encryptedManifest] = await Promise.all([ + fetchVaultMeta(git), + git.readFile('manifest.enc'), + ]); - handle = w.unlock(msg.passphrase, referenceImageBytes, meta.salt, meta.paramsJson); + const h: SessionHandle = w.unlock(msg.passphrase, referenceImageBytes, meta.salt, meta.paramsJson); + handle = h; // manifest_decrypt verifies the passphrase + reference image — throws on AEAD failure. - const manifest = w.manifest_decrypt(handle, encryptedManifest) as Manifest; + const manifest = w.manifest_decrypt(h, encryptedManifest) as Manifest; - const keys = w.register_device(msg.deviceName) as { signing_public_key: string; deploy_public_key: string }; - await devices.addDevice(git, { - name: msg.deviceName, - public_key: keys.signing_public_key, - added_at: Math.floor(Date.now() / 1000), - }); - - await chrome.storage.local.set({ - vaultConfig: config, - imageBase64: uint8ArrayToBase64(referenceImageBytes), - device_name: msg.deviceName, - }); + await registerDeviceAndPersistConfig(w, git, config, referenceImageBytes, msg.deviceName); // SW now owns the unlocked session — transfer ownership to the session. - session.setCurrent(handle); + session.setCurrent(h); state.gitHost = git; state.manifest = manifest; handle = null; // ownership transferred — do NOT lock-and-free in finally