diff --git a/extension/src/setup/setup.ts b/extension/src/setup/setup.ts index 20b38fe..8c0fa6c 100644 --- a/extension/src/setup/setup.ts +++ b/extension/src/setup/setup.ts @@ -9,6 +9,7 @@ import { createGitHost, uint8ArrayToBase64 } from '../service-worker/git-host'; import { probeVault } from './probe'; import type { VaultConfig } from '../shared/types'; +import type { SessionHandle } from 'relicario-wasm'; // --- WASM module (loaded dynamically) --- @@ -47,7 +48,7 @@ interface WizardState { passphraseVisible: boolean; confirmVisible: boolean; referenceImageBytes: Uint8Array | null; - verifiedHandle: number | null; + verifiedHandle: SessionHandle | null; creating: boolean; attaching: boolean; error: string | null; @@ -323,14 +324,151 @@ function attachStep0(): void { }); } -// --- Step 3 (attach variant) — placeholder; filled in Task 8 --- +// --- Step 3 (attach variant) --- function renderStep3Attach(): string { - return '

Step 3b (placeholder)

'; + const p = state.passphrase; + const pType = state.passphraseVisible ? 'text' : 'password'; + const pToggle = state.passphraseVisible ? 'hide' : 'show'; + const hasImage = !!state.referenceImageBytesAttach; + const gateDisabled = state.attaching || !p || !hasImage; + + return ` +
+

attach this device

+

+ Use your existing passphrase and reference image to attach this browser + to your vault. We'll verify both before registering this device. +

+ +
+ +
+ + ${hasImage + ? '

reference image loaded

' + : '

click to select your reference JPEG

'} +
+

+ The reference image is the JPEG you saved when you first created this vault — + not the original photo. It has the 256-bit secret embedded. +

+
+ +
+ +
+ + +
+
+ +
+ + +
+
+ `; } function attachStep3Attach(): void { - /* filled in Task 8 */ + const refDrop = document.getElementById('ref-drop')!; + const refInput = document.getElementById('ref-input') as HTMLInputElement; + + refDrop.addEventListener('click', () => refInput.click()); + refInput.addEventListener('change', () => { + const file = refInput.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + state.referenceImageBytesAttach = new Uint8Array(reader.result as ArrayBuffer); + state.error = null; + render(); + }; + reader.readAsArrayBuffer(file); + }); + + const passInput = document.getElementById('passphrase') as HTMLInputElement | null; + passInput?.addEventListener('input', (e) => { + state.passphrase = (e.target as HTMLInputElement).value; + const btn = document.getElementById('attach-btn') as HTMLButtonElement | null; + if (btn) btn.disabled = state.attaching || !state.passphrase || !state.referenceImageBytesAttach; + }); + + document.getElementById('eye-btn')?.addEventListener('click', () => { + state.passphraseVisible = !state.passphraseVisible; + if (passInput) passInput.type = state.passphraseVisible ? 'text' : 'password'; + const btn = document.getElementById('eye-btn'); + if (btn) btn.textContent = state.passphraseVisible ? 'hide' : 'show'; + passInput?.focus(); + }); + + document.getElementById('back-btn')?.addEventListener('click', () => { + state.step = 2; + state.error = null; + render(); + }); + + document.getElementById('attach-btn')?.addEventListener('click', async () => { + if (!state.referenceImageBytesAttach || !state.passphrase) return; + state.attaching = true; + state.error = null; + render(); + + const log = (stage: string, detail?: unknown) => console.log(`[relicario setup] ${stage}`, detail ?? ''); + let stage = 'init'; + let handle: SessionHandle | null = null; + try { + stage = 'load wasm'; + log(stage); + const w = await loadWasm(); + + stage = 'fetch vault metadata'; + log(stage); + const hostUrl = state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl; + const host = createGitHost(state.hostType, hostUrl, state.repoPath, state.apiToken); + const [salt, paramsBytes, encryptedManifest] = await Promise.all([ + host.readFile('.relicario/salt'), + host.readFile('.relicario/params.json'), + host.readFile('manifest.enc'), + ]); + const paramsJson = new TextDecoder().decode(paramsBytes); + + stage = 'derive session handle'; + log(stage); + handle = w.unlock(state.passphrase, state.referenceImageBytesAttach, salt, paramsJson); + + stage = 'decrypt manifest'; + log(stage); + // Throws if AEAD verification fails — wrong passphrase or wrong image. + w.manifest_decrypt(handle, encryptedManifest); + + log('attach verified — advancing'); + state.verifiedHandle = handle; + state.attaching = false; + state.step = 4; + state.error = null; + render(); + } catch (err: unknown) { + console.error(`[relicario setup] attach FAILED during "${stage}":`, err); + state.attaching = false; + // Lock any partial handle to avoid leaking key material. + if (handle !== null) { + try { (await loadWasm()).lock(handle); } catch { /* best effort */ } + } + state.verifiedHandle = null; + const detail = err instanceof Error ? err.message : String(err); + // Stage-aware copy: if we got past 'fetch', this is a credential failure. + if (stage === 'derive session handle' || stage === 'decrypt manifest') { + state.error = 'Could not decrypt vault — wrong passphrase or reference image.'; + } else { + state.error = `Attach failed at "${stage}": ${detail}`; + } + render(); + } + }); } // --- Step 1: Choose Host ---