fix(extension): binary-safe message transport + carrier-image guard
chrome.runtime.sendMessage serializes with a JSON-like algorithm that drops ArrayBuffer/TypedArray payloads — they arrive as empty objects, so a carrier JPEG reached the service worker as 0 bytes and failed with the opaque WASM error "no SOF marker found in JPEG". This broke extension vault creation. - shared/message-binary.ts: encodeBinary/decodeBinary deep-walk a message tree and base64-envelope every binary buffer so it survives the channel (chunked base64 to avoid stack overflow on multi-MB buffers); plain number[] untouched. - Wire encode/decode at all four message boundaries: popup, service-worker onMessage listener (decode request, encode response), setup swSend, vault postToServiceWorker. - handleCreateVault: magic-byte guard classifies empty / non-JPEG carriers into actionable errors (carrier_image_empty / carrier_image_not_jpeg) instead of the opaque WASM error; error-copy.ts adds user-facing text; setup errors now route through lookupErrorCopy. - router SETUP_ALLOWED: add create_vault / attach_vault / generate_recovery_qr — the Phase-3 setup wizard sends these from the setup page and they were being rejected as unauthorized_sender. - Tests: message-binary round-trip suite (7) + carrier-guard (2) + setup allowlist (3). 435/435 vitest, build:all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
This commit is contained in:
@@ -116,7 +116,9 @@ function makeState(wasm: ReturnType<typeof makeWasm>): PopupState {
|
||||
const BASE_MSG = {
|
||||
config: { hostType: 'gitea' as const, hostUrl: 'https://g', repoPath: 'u/v', apiToken: 't' },
|
||||
passphrase: 'pw',
|
||||
carrierImageBytes: new Uint8Array([0, 0, 0]).buffer,
|
||||
// Must begin with the JPEG SOI magic (0xFF 0xD8) to pass handleCreateVault's
|
||||
// pre-embed format guard; the WASM embed itself is mocked.
|
||||
carrierImageBytes: new Uint8Array([0xff, 0xd8, 0x00]).buffer,
|
||||
deviceName: 'Dev',
|
||||
};
|
||||
|
||||
@@ -214,6 +216,30 @@ describe('handleCreateVault', () => {
|
||||
// Ownership was NOT transferred — setCurrent must NOT have been called.
|
||||
expect(setCurrent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a non-JPEG carrier with carrier_image_not_jpeg before touching WASM', async () => {
|
||||
const wasm = makeWasm();
|
||||
const state = makeState(wasm);
|
||||
// PNG magic — must be classified, not handed to embed_image_secret.
|
||||
const png = { ...BASE_MSG, carrierImageBytes: new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer };
|
||||
|
||||
const resp = await vault.handleCreateVault(png, state);
|
||||
|
||||
expect(resp).toEqual({ ok: false, error: 'carrier_image_not_jpeg' });
|
||||
expect(wasm.embed_image_secret).not.toHaveBeenCalled();
|
||||
expect(wasm.unlock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects an empty carrier with carrier_image_empty', async () => {
|
||||
const wasm = makeWasm();
|
||||
const state = makeState(wasm);
|
||||
const empty = { ...BASE_MSG, carrierImageBytes: new ArrayBuffer(0) };
|
||||
|
||||
const resp = await vault.handleCreateVault(empty, state);
|
||||
|
||||
expect(resp).toEqual({ ok: false, error: 'carrier_image_empty' });
|
||||
expect(wasm.embed_image_secret).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// --- attach_vault ---
|
||||
|
||||
Reference in New Issue
Block a user