fix(ext): allow rate_passphrase + is_unlocked from setup tab; add diagnostic logging
Bug: setup tab's zxcvbn meter silently stayed at score=-1 because the
router's isSetup exception only allowed save_setup, so rate_passphrase
got unauthorized_sender. Result: the "create vault" button stayed
disabled forever even with a strong passphrase.
Fix: add a narrow SETUP_ALLOWED set containing save_setup,
rate_passphrase, and is_unlocked (step-4 extension detection). Reject
everything else from the setup tab. Also clean up setup.ts's unlock
call — it was passing the raw 32-byte imageSecret where JPEG bytes with
embedded secret are required; the Rust-side unlock calls imgsecret::
extract internally.
Diagnostic logging across the message path so the next silent failure
speaks up:
- [relicario setup] staged logs through vault-init; console.error
with the failure stage name in the UI banner.
- [relicario setup] rate_passphrase lastError / rejected / threw
branches each log their own warning.
- [relicario router] console.warn on unauthorized_sender (with sender
classification) and unknown_message_type.
- [relicario sw] first-message wasm init announced; per-message
non-ok result logged; thrown errors console.error'd.
Tests: +3 setup-allowlist tests (rate_passphrase accepted, is_unlocked
accepted, fill_credentials + unlock rejected). 55/55 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
244
docs/superpowers/test-runs/2026-04-20-1c-alpha-manual-matrix.md
Normal file
244
docs/superpowers/test-runs/2026-04-20-1c-alpha-manual-matrix.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# Plan 1C-α — Manual Test Matrix
|
||||
|
||||
Walkthrough for validating the extension on both Chrome and Firefox after the six-slice implementation.
|
||||
|
||||
Branch: `feature/typed-items-1c-alpha` @ `3238ef4` (tag candidate: `plan-1c-alpha-complete`)
|
||||
Worktree: `/home/alee/Sources/relicario/.worktrees/typed-items-1c-alpha`
|
||||
|
||||
---
|
||||
|
||||
## Pre-flight
|
||||
|
||||
- [ ] **P1.** Bundles built:
|
||||
```bash
|
||||
cd /home/alee/Sources/relicario/.worktrees/typed-items-1c-alpha/extension
|
||||
bun run build:all
|
||||
```
|
||||
Expected: "compiled with 2 warnings" (WASM size only) for each bundle. `dist/` and `dist-firefox/` populated.
|
||||
|
||||
- [ ] **P2.** Fresh-profile browsers ready (or existing profile's `chrome.storage.local` for this extension cleared). Stale `vaultConfig`/`imageBase64` from the pre-rename `idfoto` era must not persist.
|
||||
|
||||
- [ ] **P3.** Test git repo for the vault is reachable (SSH key / HTTPS PAT working). Use a throwaway repo to avoid polluting your real vault history.
|
||||
|
||||
- [ ] **P4.** Reference image ready (any JPEG; DCT-steg secret is embedded at init time).
|
||||
|
||||
---
|
||||
|
||||
## Loading
|
||||
|
||||
### Chrome
|
||||
- [ ] **L1.** `chrome://extensions` → Developer mode ON → "Load unpacked" → select `extension/dist/`.
|
||||
- [ ] **L2.** Toolbar icon visible (pin if needed).
|
||||
- [ ] **L3.** Click icon → first open triggers setup tab (not a popup-embedded wizard).
|
||||
|
||||
### Firefox
|
||||
- [ ] **L4.** `about:debugging#/runtime/this-firefox` → "Load Temporary Add-on…" → select `extension/dist-firefox/manifest.json`.
|
||||
- [ ] **L5.** Toolbar icon visible.
|
||||
- [ ] **L6.** Click icon → setup tab opens.
|
||||
|
||||
---
|
||||
|
||||
## 11-step core matrix — Chrome
|
||||
|
||||
**Notes column: write what you saw. Check box only when matching expected.**
|
||||
|
||||
### 1. Setup tab opens from popup (audit C1)
|
||||
|
||||
- [ ] **Do:** Fresh install, click toolbar icon.
|
||||
- [ ] **Expected:** `setup.html` opens in a new tab; popup closes immediately; WAR is empty so this MUST work via extension-origin tab, not WAR.
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### 2. zxcvbn gate in setup (audit H3)
|
||||
|
||||
- [ ] **Do:** Type weak passphrase like `password`.
|
||||
- [ ] **Expected:** Submit disabled, bar shows red/orange segments, feedback "Too weak…".
|
||||
- [ ] **Do:** Type stronger phrase until bar fills.
|
||||
- [ ] **Expected:** At score ≥ 3, submit enables, feedback "Strong enough."
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### 3. Setup completes → unlock → list renders
|
||||
|
||||
- [ ] **Do:** Upload reference JPEG, fill vault config (git host/URL/repo/token), submit. Then open popup, enter passphrase, unlock.
|
||||
- [ ] **Expected:** Manifest decrypts client-side. Empty list view appears with toolbar (search, + New, sync, lock, ⚙).
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### 4. Add Login with TOTP (typed-item wire format)
|
||||
|
||||
- [ ] **Do:** "+ New" → Login form. Fill:
|
||||
- title: `GitHub`
|
||||
- url: `https://github.com`
|
||||
- username: your handle
|
||||
- password: click "gen" (uses `DEFAULT_PASSWORD_REQUEST` — 20 chars, safe symbols)
|
||||
- totp: `JBSWY3DPEHPK3PXP` (well-known base32 test vector)
|
||||
- Save.
|
||||
- [ ] **Expected:** Row appears with 🔑 icon + title + favorite star position.
|
||||
- [ ] **Expected (CLI cross-check, optional):** From main worktree:
|
||||
```bash
|
||||
relicario list
|
||||
relicario get "GitHub" --show
|
||||
```
|
||||
Should show the same item. TOTP secret should decode identically.
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### 5. TOFU origin-ack prompt (audit C4 first half)
|
||||
|
||||
- [ ] **Do:** Navigate to `https://github.com/login`. Click the blue `id` icon next to the password field.
|
||||
- [ ] **Expected:** Closed Shadow DOM hint appears ("First autofill on github.com / Open relicario to confirm"). In DevTools, verify `document.querySelector('[data-rel]')` finds the host but `.shadowRoot` is `null` (closed mode).
|
||||
- [ ] **Expected:** No credentials fill on this click.
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### 6. Confirm origin + autofill fills correctly
|
||||
|
||||
- [ ] **Do:** Open popup (on the github.com tab). Look for a pending-ack prompt OR (α behavior) just confirm manually: any `get_credentials` call after the hostname is acked in `VaultSettings.autofill_origin_acks` will return credentials.
|
||||
- Simplest α path: click the item in the popup list, click "autofill" button. This uses the popup-captured tab state path (audit M5).
|
||||
- [ ] **Expected:** Username + password fields fill. On React/Vue sites, the native-setter trick fires input+change events.
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### 7. Multiple candidates → picker
|
||||
|
||||
- [ ] **Do:** Add a second Login for github.com with a different username. Back on `github.com/login`, click the `id` icon.
|
||||
- [ ] **Expected:** Picker shows both titles. Click one → fills that set.
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### 8. Capture prompt → `capture_save_login` flow (Slice 5 critical-fix)
|
||||
|
||||
- [ ] **Do:** Go to a site not in your vault. Fill signup form (or real trial). Submit.
|
||||
- [ ] **Expected:** Capture prompt appears inside closed Shadow DOM. No stable element IDs — running `document.querySelector('#relicario-save-btn')` in the page console returns `null`.
|
||||
- [ ] **Do:** Click "Save" in the prompt.
|
||||
- [ ] **Expected:** ✓ Saved confirmation; prompt dismisses. Open popup → item present in list with the new hostname as title.
|
||||
- [ ] **CRITICAL:** If "Save" silently fails, the `capture_save_login` content-callable handler is broken — file a bug before proceeding.
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### 9. Edit Login → password rotates; field history captured
|
||||
|
||||
- [ ] **Do:** Select the GitHub item → edit → change password → save.
|
||||
- [ ] **Expected:** Detail view shows new password on reveal. List's "modified" time updates.
|
||||
- [ ] **Expected (CLI cross-check):**
|
||||
```bash
|
||||
relicario get "GitHub" --show
|
||||
# confirm field_history now has entry for the old password
|
||||
```
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### 10. Delete Login → soft-delete
|
||||
|
||||
- [ ] **Do:** Select an item → "trash" → confirm.
|
||||
- [ ] **Expected:** Row disappears from list immediately. Popup list filters `trashed_at !== undefined`.
|
||||
- [ ] **Expected (CLI cross-check):** `relicario list --trashed` shows the item.
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### 11. Lock → re-unlock
|
||||
|
||||
- [ ] **Do:** Click "lock" in the toolbar. Try to open the popup again.
|
||||
- [ ] **Expected:** Unlock screen. Session handle was cleared in WASM (not just JS).
|
||||
- [ ] **Do:** Re-unlock.
|
||||
- [ ] **Expected:** Same list (including the item from step 10 still in trash, invisible).
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
---
|
||||
|
||||
## 11-step core matrix — Firefox
|
||||
|
||||
Re-run 1–11 on Firefox. Critical Firefox-only check: the background script runs as a **persistent script** (not MV3 service worker); WASM loads via `initDefault(wasmUrl)` not `initSync`. Anything broken here that works in Chrome indicates WASM-loading drift.
|
||||
|
||||
- [ ] **FF1–FF11.** Re-run the 11 steps above. Summarize anomalies:
|
||||
- **Notes:** ___
|
||||
|
||||
---
|
||||
|
||||
## Security probes (bonus)
|
||||
|
||||
Open DevTools on any page (not extension origin) and try to defeat the router:
|
||||
|
||||
### SP1. Content-script-originated popup-only message
|
||||
|
||||
- [ ] **Do:** In a page console (not popup DevTools):
|
||||
```js
|
||||
chrome.runtime.sendMessage({ type: 'unlock', passphrase: 'guess' }, console.log)
|
||||
```
|
||||
- [ ] **Expected:** `{ ok: false, error: 'unauthorized_sender' }` (audit C2).
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### SP2. Cross-origin `get_credentials` attempt
|
||||
|
||||
- [ ] **Do:** Pick an item id from the popup (e.g., via popup DevTools: `copy(currentState.selectedId)`). Go to a **different-origin** page's console:
|
||||
```js
|
||||
chrome.runtime.sendMessage({ type: 'get_credentials', id: '<the-id>' }, console.log)
|
||||
```
|
||||
- [ ] **Expected:** `{ ok: false, error: 'origin_mismatch' }` (audit C4). No item data leaks.
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### SP3. Closed Shadow DOM verification
|
||||
|
||||
- [ ] **Do:** Trigger the capture prompt (step 8). In the page console:
|
||||
```js
|
||||
const hosts = document.querySelectorAll('[data-rel]');
|
||||
for (const h of hosts) console.log(h, h.shadowRoot); // shadowRoot should be null
|
||||
console.log(document.querySelector('#relicario-save-btn')); // should be null
|
||||
console.log(document.querySelector('.relicario-capture')); // should be null
|
||||
```
|
||||
- [ ] **Expected:** All `shadowRoot` values are `null`; no stable selectors match (audit C3).
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
### SP4. Captured-tab navigation during fill (audit M5)
|
||||
|
||||
- [ ] **Do:** Open popup on `https://github.com/login`. Select a github item, click "autofill", but BEFORE the fill lands, rapidly navigate the github tab to `https://example.com`.
|
||||
- [ ] **Expected:** No credentials typed on example.com. SW rejects with `tab_navigated`; if somehow the message reaches the content script, `fill.ts` re-checks `expectedHost` and rejects with `origin_changed`.
|
||||
- [ ] **Notes:** ___ (this one's hard to time; skip if not easily reproducible)
|
||||
|
||||
### SP5. WAR probe
|
||||
|
||||
- [ ] **Do:** In a page console on any site:
|
||||
```js
|
||||
fetch('chrome-extension://<your-extension-id>/setup.html').catch(e => console.log('blocked:', e))
|
||||
```
|
||||
- [ ] **Expected:** Blocked (either CORS error or net::ERR). WAR is empty, so no resource is web-accessible. `<all_urls>` pages cannot reach setup.html.
|
||||
- [ ] **Notes:** ___
|
||||
|
||||
---
|
||||
|
||||
## Final acceptance
|
||||
|
||||
- [ ] **A1.** `cargo test --workspace` green (should still be 151+ Rust tests).
|
||||
- [ ] **A2.** `cd extension && bun run test` green (should be 52 passing — 11 base32 + 41 router).
|
||||
- [ ] **A3.** `cd extension && bun run build` green (Chrome bundle).
|
||||
- [ ] **A4.** `cd extension && bun run build:firefox` green (Firefox bundle).
|
||||
- [ ] **A5.** Lint greps clean:
|
||||
```bash
|
||||
git grep -n 'innerHTML\|insertAdjacentHTML' extension/src/content/ # zero hits
|
||||
git grep -n 'idfoto' extension/ # zero hits
|
||||
git grep -n '@ts-nocheck' extension/src/ # zero hits
|
||||
```
|
||||
- [ ] **A6.** WAR empty:
|
||||
```bash
|
||||
grep -A2 web_accessible_resources extension/manifest.json # []
|
||||
grep -A2 web_accessible_resources extension/manifest.firefox.json # []
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sign-off
|
||||
|
||||
- [ ] **All 11 core-matrix steps pass on Chrome**
|
||||
- [ ] **All 11 core-matrix steps pass on Firefox**
|
||||
- [ ] **All 5 security probes pass (or SP4 skipped, others pass)**
|
||||
- [ ] **All 6 final acceptance checks pass**
|
||||
- [ ] **Ready to tag `plan-1c-alpha-complete` and decide on merge path**
|
||||
|
||||
### Findings / issues
|
||||
|
||||
Use this space to log anything weird:
|
||||
|
||||
```
|
||||
(fill in as you go)
|
||||
```
|
||||
|
||||
### Decision
|
||||
|
||||
- [ ] Merge straight to `main`
|
||||
- [ ] Open a PR first for review
|
||||
- [ ] Need rework on: ___
|
||||
|
||||
---
|
||||
|
||||
*Generated 2026-04-20 — source: spec `2026-04-20-relicario-extension-1c-alpha-design.md` §5.4, plan `2026-04-20-relicario-extension-1c-alpha.md` Task 27.*
|
||||
@@ -46,11 +46,25 @@ const state: RouterState = {
|
||||
chrome.runtime.onMessage.addListener(
|
||||
(request: Request, sender: chrome.runtime.MessageSender, sendResponse: (r: Response) => void) => {
|
||||
(async () => {
|
||||
if (!state.wasm) state.wasm = await initWasm();
|
||||
if (!state.wasm) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[relicario sw] initializing WASM on first message');
|
||||
state.wasm = await initWasm();
|
||||
}
|
||||
return route(request, state, sender);
|
||||
})()
|
||||
.then(sendResponse)
|
||||
.catch((err: Error) => sendResponse({ ok: false, error: err.message }));
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[relicario sw] ${request.type} -> error:`, r.error);
|
||||
}
|
||||
sendResponse(r);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[relicario sw] ${request.type} threw:`, err);
|
||||
sendResponse({ ok: false, error: err.message });
|
||||
});
|
||||
return true; // async response
|
||||
},
|
||||
);
|
||||
|
||||
@@ -309,10 +309,32 @@ describe('fill_credentials captured-tab verification', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- save_setup exception scope: setup tab is ONLY allowed save_setup ---
|
||||
// --- setup-tab exception scope ---
|
||||
//
|
||||
// Setup is allowed a narrow subset of popup-only messages:
|
||||
// - save_setup (final wire-up)
|
||||
// - rate_passphrase (zxcvbn meter during passphrase entry)
|
||||
// - is_unlocked (step-4 extension detection)
|
||||
// Everything else popup-only must be rejected from setup.
|
||||
|
||||
describe('save_setup exception scope', () => {
|
||||
it('rejects fill_credentials from the setup tab (setup can only save_setup)', async () => {
|
||||
describe('setup tab exception scope', () => {
|
||||
it('accepts rate_passphrase from the setup tab (zxcvbn meter)', async () => {
|
||||
const state = makeState();
|
||||
const res = await route(
|
||||
{ type: 'rate_passphrase', passphrase: 'correct horse battery staple parapet' },
|
||||
state,
|
||||
makeSetupSender(),
|
||||
);
|
||||
expect(res).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it('accepts is_unlocked from the setup tab (step-4 detection)', async () => {
|
||||
const state = makeState();
|
||||
const res = await route({ type: 'is_unlocked' }, state, makeSetupSender());
|
||||
expect(res).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it('rejects fill_credentials from the setup tab (outside the allowlist)', async () => {
|
||||
const state = makeState();
|
||||
const res = await route(
|
||||
{
|
||||
@@ -326,6 +348,16 @@ describe('save_setup exception scope', () => {
|
||||
);
|
||||
expect(res).toEqual({ ok: false, error: 'unauthorized_sender' });
|
||||
});
|
||||
|
||||
it('rejects unlock from the setup tab (outside the allowlist)', async () => {
|
||||
const state = makeState();
|
||||
const res = await route(
|
||||
{ type: 'unlock', passphrase: 'hunter2' },
|
||||
state,
|
||||
makeSetupSender(),
|
||||
);
|
||||
expect(res).toEqual({ ok: false, error: 'unauthorized_sender' });
|
||||
});
|
||||
});
|
||||
|
||||
// --- isContent rejects unknown sender.id ---
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/// to popup-only or content-callable handlers. Unauthorized senders are
|
||||
/// rejected with { ok: false, error: 'unauthorized_sender' }.
|
||||
|
||||
import type { Request, Response } from '../../shared/messages';
|
||||
import type { PopupMessage, Request, Response } from '../../shared/messages';
|
||||
import { POPUP_ONLY_TYPES, CONTENT_CALLABLE_TYPES } from '../../shared/messages';
|
||||
import type { Manifest } from '../../shared/types';
|
||||
import type { GitHost } from '../git-host';
|
||||
@@ -16,6 +16,16 @@ export interface RouterState {
|
||||
wasm: any;
|
||||
}
|
||||
|
||||
/// Popup-only messages the setup tab is also allowed to send.
|
||||
/// - save_setup: wires vault config + image into chrome.storage.local at end of init.
|
||||
/// - rate_passphrase: drives the zxcvbn strength meter during passphrase entry.
|
||||
/// - is_unlocked: setup step-4 pings the extension to detect "save config to extension" availability.
|
||||
const SETUP_ALLOWED: ReadonlySet<PopupMessage['type']> = new Set<PopupMessage['type']>([
|
||||
'save_setup',
|
||||
'rate_passphrase',
|
||||
'is_unlocked',
|
||||
]);
|
||||
|
||||
export async function route(
|
||||
msg: Request,
|
||||
state: RouterState,
|
||||
@@ -32,17 +42,29 @@ export async function route(
|
||||
&& sender.id === chrome.runtime.id;
|
||||
|
||||
if (POPUP_ONLY_TYPES.has(msg.type as never)) {
|
||||
// save_setup gets one exception: allowed from the setup tab too.
|
||||
if (!(isPopup || (msg.type === 'save_setup' && isSetup))) {
|
||||
if (!(isPopup || (isSetup && SETUP_ALLOWED.has(msg.type as PopupMessage['type'])))) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[relicario router] rejected popup-only message from wrong sender', {
|
||||
type: msg.type, senderUrl, isPopup, isSetup, isContent,
|
||||
});
|
||||
return { ok: false, error: 'unauthorized_sender' };
|
||||
}
|
||||
return popupOnly.handle(msg as never, state, sender);
|
||||
}
|
||||
|
||||
if (CONTENT_CALLABLE_TYPES.has(msg.type as never)) {
|
||||
if (!isContent) return { ok: false, error: 'unauthorized_sender' };
|
||||
if (!isContent) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[relicario router] rejected content-only message from wrong sender', {
|
||||
type: msg.type, senderUrl, isPopup, isSetup, isContent,
|
||||
frameId: sender.frameId, senderId: sender.id,
|
||||
});
|
||||
return { ok: false, error: 'unauthorized_sender' };
|
||||
}
|
||||
return contentCallable.handle(msg as never, state, sender);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[relicario router] unknown message type', { type: (msg as { type: string }).type });
|
||||
return { ok: false, error: 'unknown_message_type' };
|
||||
}
|
||||
|
||||
@@ -81,11 +81,22 @@ function ratePassphrase(passphrase: string): Promise<number> {
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: 'rate_passphrase', passphrase },
|
||||
(response: { ok: boolean; data?: { score: number; guesses_log10: number }; error?: string }) => {
|
||||
if (chrome.runtime.lastError || !response?.ok) { resolve(-1); return; }
|
||||
if (chrome.runtime.lastError) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[relicario setup] rate_passphrase lastError:', chrome.runtime.lastError);
|
||||
resolve(-1); return;
|
||||
}
|
||||
if (!response?.ok) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[relicario setup] rate_passphrase rejected by SW:', response);
|
||||
resolve(-1); return;
|
||||
}
|
||||
resolve(response.data?.score ?? -1);
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[relicario setup] rate_passphrase threw:', err);
|
||||
resolve(-1);
|
||||
}
|
||||
});
|
||||
@@ -428,66 +439,85 @@ function attachStep3(): void {
|
||||
state.error = null;
|
||||
render();
|
||||
|
||||
// Structured logging so silent failures become visible in DevTools.
|
||||
// eslint-disable-next-line no-console
|
||||
const log = (stage: string, detail?: unknown) => console.log(`[relicario setup] ${stage}`, detail ?? '');
|
||||
|
||||
let stage = 'init';
|
||||
try {
|
||||
stage = 'load wasm';
|
||||
log(stage);
|
||||
const w = await loadWasm();
|
||||
|
||||
// 1. Generate 32-byte image secret.
|
||||
stage = 'generate image secret';
|
||||
log(stage);
|
||||
const imageSecret = new Uint8Array(32);
|
||||
crypto.getRandomValues(imageSecret);
|
||||
|
||||
// 2. Embed secret into carrier JPEG.
|
||||
stage = 'embed image secret';
|
||||
log(stage, { carrierBytes: state.carrierImageBytes.byteLength });
|
||||
state.referenceImageBytes = new Uint8Array(
|
||||
w.embed_image_secret(state.carrierImageBytes, imageSecret),
|
||||
);
|
||||
log('embedded', { referenceBytes: state.referenceImageBytes.byteLength });
|
||||
|
||||
// 3. Generate 32-byte salt + KDF params.
|
||||
stage = 'generate salt';
|
||||
const salt = new Uint8Array(32);
|
||||
crypto.getRandomValues(salt);
|
||||
const paramsJson = '{"argon2_m":65536,"argon2_t":3,"argon2_p":4}';
|
||||
|
||||
// 4. Derive a session handle via the typed-item unlock API.
|
||||
// (Single-shot master_key derivation is no longer exposed; the
|
||||
// handle is the only in-JS reference to the master key.)
|
||||
const handle = w.unlock(state.passphrase, imageSecret, salt, paramsJson);
|
||||
stage = 'derive session handle';
|
||||
log(stage);
|
||||
// unlock() takes JPEG bytes with embedded secret (it extracts internally),
|
||||
// not the raw 32-byte secret.
|
||||
const handle = w.unlock(state.passphrase, state.referenceImageBytes, salt, paramsJson);
|
||||
log('handle acquired');
|
||||
|
||||
// 5. Encrypt an empty schema-v2 manifest.
|
||||
stage = 'encrypt empty manifest';
|
||||
log(stage);
|
||||
const manifestJson = '{"schema_version":2,"items":{}}';
|
||||
const encryptedManifest = w.manifest_encrypt(handle, manifestJson);
|
||||
log('manifest encrypted', { bytes: encryptedManifest.length });
|
||||
|
||||
// 6. Push vault files via git API.
|
||||
stage = 'push vault files';
|
||||
log(stage);
|
||||
const hostUrl = state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl;
|
||||
const host = createGitHost(state.hostType, hostUrl, state.repoPath, state.apiToken);
|
||||
|
||||
log('write .relicario/salt');
|
||||
await host.writeFile('.relicario/salt', salt, 'init: vault salt');
|
||||
|
||||
log('write .relicario/params.json');
|
||||
const paramsBytes = new TextEncoder().encode(paramsJson);
|
||||
await host.writeFile('.relicario/params.json', paramsBytes, 'init: KDF parameters');
|
||||
|
||||
log('write .relicario/devices.json');
|
||||
const devicesJson = '{"devices":[]}';
|
||||
const devicesBytes = new TextEncoder().encode(devicesJson);
|
||||
await host.writeFile('.relicario/devices.json', devicesBytes, 'init: device registry');
|
||||
|
||||
log('write manifest.enc');
|
||||
await host.writeFile(
|
||||
'manifest.enc',
|
||||
new Uint8Array(encryptedManifest),
|
||||
'init: encrypted manifest',
|
||||
);
|
||||
|
||||
// 7. Release the handle — the SW's own unlock will re-derive.
|
||||
stage = 'release handle';
|
||||
w.lock(handle);
|
||||
|
||||
// 8. Advance to step 4.
|
||||
log('vault created — advancing to step 4');
|
||||
state.creating = false;
|
||||
state.step = 4;
|
||||
state.error = null;
|
||||
|
||||
// Detect extension.
|
||||
detectExtension();
|
||||
|
||||
render();
|
||||
} catch (err: unknown) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[relicario setup] vault creation FAILED during "${stage}":`, err);
|
||||
state.creating = false;
|
||||
state.error = `Vault creation failed: ${err instanceof Error ? err.message : String(err)}`;
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
state.error = `Vault creation failed at "${stage}": ${detail}`;
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user