123456789101112131415import { beforeEach, describe, expect, it, vi } from "vitest";1617import type { Message, MessageResponse } from "../shared/types";1819type StorageShape = Record<string, unknown>;20type MessageListener = (21 msg: unknown,22 sender: chrome.runtime.MessageSender,23 sendResponse: (response: MessageResponse) => void24) => boolean;2526describe("sync adversarial", () => {27 let storage: StorageShape;28 let sessionStorage: StorageShape;29 let listener: MessageListener | undefined;3031 beforeEach(async () => {32 vi.resetModules();33 vi.useRealTimers();34 storage = { peach_settings: { breachMonitoring: false } };35 sessionStorage = {};36 listener = undefined;3738 vi.stubGlobal(39 "fetch",40 vi.fn(async () => ({ ok: true, status: 200, text: async () => "" }))41 );42 vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false);4344 const chromeMock = {45 runtime: {46 id: "peach-extension",47 onMessage: {48 addListener: vi.fn((fn: MessageListener) => {49 listener = fn;50 }),51 },52 sendMessage: vi.fn(async () => ({ ok: true })),53 },54 tabs: {55 query: vi.fn((_q: unknown, cb?: (tabs: unknown[]) => void) => {56 if (typeof cb === "function") {57 cb([{ id: 1 }]);58 return;59 }60 return Promise.resolve([{ id: 1 }]);61 }),62 sendMessage: vi.fn(async () => ({ ok: true })),63 },64 storage: {65 local: {66 get: vi.fn(async (key?: string | string[]) => {67 if (!key) return storage;68 if (typeof key === "string") return { [key]: storage[key] };69 return key.reduce<Record<string, unknown>>((acc, k) => {70 acc[k] = storage[k];71 return acc;72 }, {});73 }),74 set: vi.fn(async (next: Record<string, unknown>) => {75 Object.assign(storage, next);76 }),77 remove: vi.fn(async (keys: string | string[]) => {78 const list = Array.isArray(keys) ? keys : [keys];79 for (const key of list) delete storage[key];80 }),81 },82 session: {83 get: vi.fn(async (key?: string | string[]) => {84 if (!key) return sessionStorage;85 if (typeof key === "string") return { [key]: sessionStorage[key] };86 return key.reduce<Record<string, unknown>>((acc, k) => {87 acc[k] = sessionStorage[k];88 return acc;89 }, {});90 }),91 set: vi.fn(async (next: Record<string, unknown>) => {92 Object.assign(sessionStorage, next);93 }),94 remove: vi.fn(async (keys: string | string[]) => {95 const list = Array.isArray(keys) ? keys : [keys];96 for (const key of list) delete sessionStorage[key];97 }),98 clear: vi.fn(async () => {99 for (const k of Object.keys(sessionStorage)) delete sessionStorage[k];100 }),101 },102 },103 alarms: {104 clear: vi.fn(),105 create: vi.fn(),106 onAlarm: { addListener: vi.fn() },107 },108 idle: {109 setDetectionInterval: vi.fn(),110 onStateChanged: { addListener: vi.fn() },111 },112 scripting: { executeScript: vi.fn(async () => []) },113 } as unknown as typeof chrome;114115 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;116117 const messages = await import("./messages");118 messages.setupMessageHandler();119 const vault = await import("./vault");120 await vault.ensureSettingsLoaded();121 });122123 async function dispatch(message: Message): Promise<MessageResponse> {124 if (!listener) throw new Error("listener not registered");125 return new Promise<MessageResponse>((resolve) => {126 const keepOpen = listener!(127 message,128 { id: "peach-extension" } as chrome.runtime.MessageSender,129 resolve130 );131 expect(keepOpen).toBe(true);132 });133 }134135 async function setupBlobs() {136 const vault = await import("./vault");137 await vault.createVault("test-pass-12345");138 139 storage.peach_last_modified_at = String(1_700_000_000_000);140 return vault;141 }142143 function makeIncomingBlob(slotsLike?: {144 a?: Record<string, unknown>;145 b?: Record<string, unknown>;146 }): string {147 const a = slotsLike?.a ?? storage.peach_vault_a;148 const b = slotsLike?.b ?? storage.peach_vault_b;149 return JSON.stringify({ a, b });150 }151152 153154 describe("applyIncomingVault timestamp adversarial", () => {155 const olderDeltas = [156 -1, -10, -1000, -60_000, -3_600_000, -86_400_000, -1_000_000_000,157 ];158 for (const delta of olderDeltas) {159 it(`incoming vault with timestamp delta ${delta}ms (older): applied=false`, async () => {160 const vault = await setupBlobs();161 const remoteTs = 1_700_000_000_000 + delta;162 const applied = await vault.applyIncomingVault(163 makeIncomingBlob(),164 remoteTs165 );166 expect(applied).toBe(false);167 });168 }169170 it("same timestamp: applied=false (not equal-overwrite)", async () => {171 const vault = await setupBlobs();172 const applied = await vault.applyIncomingVault(173 makeIncomingBlob(),174 1_700_000_000_000175 );176 expect(applied).toBe(false);177 });178179 const newerDeltas = [1, 10, 1_000, 60_000, 3_600_000, 1_000_000_000];180 for (const delta of newerDeltas) {181 it(`incoming vault delta +${delta}ms (newer): applied=true`, async () => {182 const vault = await setupBlobs();183 const applied = await vault.applyIncomingVault(184 makeIncomingBlob(),185 1_700_000_000_000 + delta186 );187 expect(applied).toBe(true);188 });189 }190191 it("far-future timestamp accepted (no upper bound on remote ts)", async () => {192 const vault = await setupBlobs();193 const applied = await vault.applyIncomingVault(194 makeIncomingBlob(),195 Date.now() * 100196 );197 expect(applied).toBe(true);198 });199 });200201 202203 describe("applyIncomingVault malformed blob handling", () => {204 const malformedBlobs = [205 "",206 "not json",207 "{",208 "[]",209 "null",210 "true",211 "42",212 '{"a": null}',213 '{"a": "not-an-object"}',214 ];215 for (const blob of malformedBlobs) {216 it(`malformed blob ${JSON.stringify(blob).slice(0, 30)} does not poison local vault`, async () => {217 const vault = await setupBlobs();218 const before = storage.peach_vault_a;219 let threw = false;220 try {221 await vault.applyIncomingVault(blob, Date.now() + 100_000);222 } catch {223 threw = true;224 }225 226 227 const reloaded = storage.peach_vault_a;228 if (!threw) {229 230 231 232 233 expect(reloaded).not.toBe(undefined);234 } else {235 expect(reloaded).toBe(before);236 }237 });238 }239 });240241 242243 describe("applyIncomingVault license handling", () => {244 it("existing license is not overwritten by incoming license", async () => {245 const vault = await setupBlobs();246 storage.peach_license = "my-existing-license";247 await vault.applyIncomingVault(248 makeIncomingBlob(),249 Date.now() + 100_000,250 "their-different-license"251 );252 expect(storage.peach_license).toBe("my-existing-license");253 });254255 it("no local license: incoming license is adopted", async () => {256 const vault = await setupBlobs();257 delete storage.peach_license;258 await vault.applyIncomingVault(259 makeIncomingBlob(),260 Date.now() + 100_000,261 "incoming-license"262 );263 expect(storage.peach_license).toBe("incoming-license");264 });265266 it("no incoming license: local license is unchanged", async () => {267 const vault = await setupBlobs();268 storage.peach_license = "local-pro";269 await vault.applyIncomingVault(270 makeIncomingBlob(),271 Date.now() + 100_000272 );273 expect(storage.peach_license).toBe("local-pro");274 });275276 it("empty-string incoming license is treated as no license", async () => {277 const vault = await setupBlobs();278 delete storage.peach_license;279 await vault.applyIncomingVault(280 makeIncomingBlob(),281 Date.now() + 100_000,282 ""283 );284 285 expect(storage.peach_license).toBeUndefined();286 });287 });288289 290291 describe("applyIncomingVault snapshot", () => {292 it("a snapshot is taken before applying a newer incoming vault", async () => {293 const vault = await setupBlobs();294 const historyBefore = await vault.getVaultHistory();295 await vault.applyIncomingVault(296 makeIncomingBlob(),297 Date.now() + 100_000298 );299 const historyAfter = await vault.getVaultHistory();300 expect(historyAfter.length).toBeGreaterThanOrEqual(historyBefore.length);301 });302 });303304 305306 describe("pairing token lifecycle", () => {307 it("START_PAIRING_SERVER yields a 6-digit token", async () => {308 const r = await dispatch({ type: "START_PAIRING_SERVER" });309 expect(r.ok).toBe(true);310 const data = (r as { data: { token: string; expiresAt: number } }).data;311 expect(data.token).toMatch(/^\d{6}$/);312 expect(data.expiresAt).toBeGreaterThan(Date.now());313 });314315 it("STOP_PAIRING_SERVER clears the token", async () => {316 await dispatch({ type: "START_PAIRING_SERVER" });317 const r = await dispatch({ type: "STOP_PAIRING_SERVER" });318 expect(r.ok).toBe(true);319 });320321 it("SCAN_PAIRING_QR returns android_only off mobile", async () => {322 const r = await dispatch({323 type: "SCAN_PAIRING_QR",324 payload: { token: "123456" },325 });326 expect(r.ok).toBe(false);327 expect(r.error).toBe("android_only");328 });329330 it("SCAN_PAIRING_QR with garbage token still returns android_only off mobile", async () => {331 const r = await dispatch({332 type: "SCAN_PAIRING_QR",333 payload: { token: "not-a-token" as unknown as string },334 });335 expect(r.ok).toBe(false);336 });337338 it("ACCEPT_PAIRING always returns ok (free tier)", async () => {339 const r = await dispatch({ type: "ACCEPT_PAIRING", payload: {} });340 expect(r.ok).toBe(true);341 });342343 for (let i = 0; i < 10; i++) {344 it(`generated token #${i} is a fresh 6-digit code`, async () => {345 const r = await dispatch({ type: "START_PAIRING_SERVER" });346 const data = (r as { data: { token: string } }).data;347 expect(data.token).toMatch(/^\d{6}$/);348 });349 }350 });351352 353354 describe("RESPOND_INCOMING_PAIRING adversarial", () => {355 it("no pending request: returns no_pending_request", async () => {356 const r = await dispatch({357 type: "RESPOND_INCOMING_PAIRING",358 payload: { accept: true },359 });360 expect(r.ok).toBe(false);361 expect(r.error).toBe("no_pending_request");362 });363364 it("expired request: getIncomingPairingRequest returns null", async () => {365 await dispatch({366 type: "DEBUG_INJECT_INCOMING_PAIRING",367 payload: {368 deviceName: "fake",369 publicKeyHex: "00".repeat(32),370 },371 });372 373 const sessionRaw = sessionStorage["peach_incoming_pair"] as374 | Record<string, unknown>375 | undefined;376 if (sessionRaw && typeof sessionRaw === "object") {377 (sessionRaw as Record<string, unknown>).expiresAt = Date.now() - 1000;378 }379 const r = await dispatch({380 type: "RESPOND_INCOMING_PAIRING",381 payload: { accept: true },382 });383 384 385 expect(r.ok).toBe(false);386 });387388 it("missing payload returns no_pending or error", async () => {389 const r = await dispatch({390 type: "RESPOND_INCOMING_PAIRING",391 payload: {} as { accept: boolean },392 });393 expect(r).toBeDefined();394 });395 });396397 398399 describe("DEBUG_INJECT_INCOMING_PAIRING adversarial", () => {400 it("missing fields returns missing_payload", async () => {401 const r = await dispatch({402 type: "DEBUG_INJECT_INCOMING_PAIRING",403 payload: {},404 });405 expect(r.ok).toBe(false);406 expect(r.error).toBe("missing_payload");407 });408409 const partials = [410 { deviceName: "x" },411 { publicKeyHex: "y" },412 { deviceName: "", publicKeyHex: "" },413 ];414 for (const [i, p] of partials.entries()) {415 it(`partial payload #${i} returns missing_payload`, async () => {416 const r = await dispatch({417 type: "DEBUG_INJECT_INCOMING_PAIRING",418 payload: p,419 });420 expect(r.ok).toBe(false);421 expect(r.error).toBe("missing_payload");422 });423 }424425 it("valid payload returns ok", async () => {426 const r = await dispatch({427 type: "DEBUG_INJECT_INCOMING_PAIRING",428 payload: {429 deviceName: "test-device",430 publicKeyHex: "ab".repeat(32),431 },432 });433 expect(r.ok).toBe(true);434 });435 });436437 438439 describe("GET_SYNC_STATUS shape", () => {440 it("never synced: status=never", async () => {441 delete storage.peach_last_sync_at;442 const r = await dispatch({ type: "GET_SYNC_STATUS" });443 const data = (r as { data: { status: string } }).data;444 expect(data.status).toBe("never");445 });446447 it("with last_sync_at: status=up_to_date", async () => {448 storage.peach_last_sync_at = String(Date.now());449 const r = await dispatch({ type: "GET_SYNC_STATUS" });450 const data = (r as { data: { status: string } }).data;451 expect(data.status).toBe("up_to_date");452 });453454 it("garbage last_sync_at: status=never", async () => {455 storage.peach_last_sync_at = "not-a-number";456 const r = await dispatch({ type: "GET_SYNC_STATUS" });457 const data = (r as { data: { status: string } }).data;458 expect(data.status).toBe("never");459 });460 });461462 463464 describe("REMOVE_PAIRED_DEVICE adversarial", () => {465 const badIds = ["", " ", "not-a-real-id", "\x00", "a".repeat(1000)];466 for (const deviceId of badIds) {467 it(`unknown deviceId ${JSON.stringify(deviceId.slice(0, 20))} succeeds (idempotent)`, async () => {468 const r = await dispatch({469 type: "REMOVE_PAIRED_DEVICE",470 payload: { deviceId },471 });472 473 expect(r.ok).toBe(true);474 });475 }476 });477478 479480 describe("timestamp boundary sweep", () => {481 for (const baseTs of [482 0,483 1,484 1_000,485 1_700_000_000_000,486 Number.MAX_SAFE_INTEGER - 10,487 ]) {488 it(`base ts ${baseTs}: applies when remote is strictly newer`, async () => {489 const vault = await import("./vault");490 await vault.createVault("test-pass-12345");491 storage.peach_last_modified_at = String(baseTs);492 const applied = await vault.applyIncomingVault(493 makeIncomingBlob(),494 baseTs + 1495 );496 expect(applied).toBe(true);497 });498499 it(`base ts ${baseTs}: rejects when remote is equal`, async () => {500 const vault = await import("./vault");501 await vault.createVault("test-pass-12345");502 storage.peach_last_modified_at = String(baseTs);503 const applied = await vault.applyIncomingVault(504 makeIncomingBlob(),505 baseTs506 );507 expect(applied).toBe(false);508 });509510 it(`base ts ${baseTs}: rejects when remote is strictly older`, async () => {511 const vault = await import("./vault");512 await vault.createVault("test-pass-12345");513 storage.peach_last_modified_at = String(baseTs);514 const applied = await vault.applyIncomingVault(515 makeIncomingBlob(),516 baseTs - 1517 );518 expect(applied).toBe(false);519 });520 }521 });522523 524525 describe("token format adversarial", () => {526 const badFormats = [527 "",528 "12345",529 "1234567",530 "abcdef",531 "12 34 56",532 "<script>",533 "null",534 "undefined",535 "-12345",536 "1.23456",537 "🔢🔢🔢🔢🔢🔢",538 ];539 for (const token of badFormats) {540 it(`SCAN_PAIRING_QR with token ${JSON.stringify(token)} returns android_only`, async () => {541 const r = await dispatch({542 type: "SCAN_PAIRING_QR",543 payload: { token },544 });545 expect(r.ok).toBe(false);546 });547 }548 });549550 551552 describe("runBlobSyncSession safety", () => {553 it("locked vault returns skipped status", async () => {554 const { runBlobSyncSession } = await import("./sync-coordinator");555 556 const result = await runBlobSyncSession(557 "initiator",558 {559 send: vi.fn(async () => {}),560 receive: vi.fn(async () => new Uint8Array()),561 close: vi.fn(async () => {}),562 } as unknown as Parameters<typeof runBlobSyncSession>[1],563 {564 getVaultBlobForSync: async () => null,565 applyIncomingVault: async () => false,566 isVaultUnlocked: () => false,567 }568 );569 expect(result.status).toBe("skipped");570 expect(result.applied).toBe(false);571 });572 });573574 575 576 577 describe("H3/COR-005 sync re-decrypt failure lock", () => {578 async function buildRemoteBlobWithDifferentKey(): Promise<string> {579 const vault = await import("./vault");580 const slotA = await vault.loadSlotA();581 if (!slotA) throw new Error("no slot A");582 583 584 585 const rand = (n: number) =>586 Array.from(crypto.getRandomValues(new Uint8Array(n)))587 .map(b => b.toString(16).padStart(2, "0"))588 .join("");589 const remoteA = {590 ...slotA,591 salt: rand(16),592 nonce: rand(12),593 ciphertext: rand(64),594 tag: rand(16),595 };596 return JSON.stringify({ a: remoteA, b: storage.peach_vault_b });597 }598599 it("vault locks when reloadEntriesAfterSync fails after apply", async () => {600 const vault = await setupBlobs();601 const messages = await import("./messages");602 const deps = messages.buildSyncDeps();603 const remote = await buildRemoteBlobWithDifferentKey();604 605 const remoteTs = 1_700_000_000_000 + 100_000;606 expect(vault.isUnlocked()).toBe(true);607 const applied = await deps.applyIncomingVault(remote, remoteTs);608 expect(applied).toBe(true);609 expect(vault.isUnlocked()).toBe(false);610 });611612 it("stale in-memory entries cannot clobber remote blob after sync", async () => {613 await setupBlobs();614 const messages = await import("./messages");615 const deps = messages.buildSyncDeps();616 const remote = await buildRemoteBlobWithDifferentKey();617 const remoteTs = 1_700_000_000_000 + 100_000;618 await deps.applyIncomingVault(remote, remoteTs);619 620 const r = await dispatch({621 type: "ADD_ENTRY",622 payload: { title: "Ghost", password: "p" },623 });624 expect(r.ok).toBe(false);625 });626627 it("when reloadEntriesAfterSync succeeds, vault stays unlocked", async () => {628 const vault = await setupBlobs();629 const messages = await import("./messages");630 const deps = messages.buildSyncDeps();631 632 633 const slotA = storage.peach_vault_a;634 const sameKeyBlob = JSON.stringify({ a: slotA, b: storage.peach_vault_b });635 const applied = await deps.applyIncomingVault(636 sameKeyBlob,637 1_700_000_000_000 + 50_000,638 );639 expect(applied).toBe(true);640 expect(vault.isUnlocked()).toBe(true);641 });642 });643644 645 646 647 648 describe("sync initiator authentication", () => {649 650 651 652 653 async function simulateResponderHandshake(opts: {654 initiatorKeypair: { secretKey: Uint8Array; publicKey: Uint8Array };655 pairedX25519Hex: string[];656 }): Promise<{ status: "completed" | "rejected"; vaultBlobSent: boolean }> {657 const { generateKeyPair, sharedKey } = await import("@stablelib/x25519");658 const { HMAC } = await import("@stablelib/hmac");659 const { SHA256 } = await import("@stablelib/sha256");660661 662 663 664 const responderKp = generateKeyPair();665 const b64 = (bytes: Uint8Array) => {666 let s = "";667 for (const b of bytes) s += String.fromCharCode(b);668 return btoa(s);669 };670 storage.peach_sync_identity = JSON.stringify({671 priv: b64(responderKp.secretKey),672 pub: b64(responderKp.publicKey),673 });674675 676 storage.peach_paired_devices = pairedX25519Hex(opts.pairedX25519Hex);677678 679 680 const sentFrames: string[] = [];681 (globalThis as unknown as { Capacitor: unknown }).Capacitor = {682 isNativePlatform: () => true,683 Plugins: {684 SyncPlugin: {685 addListener: () => {},686 sendToConnection: async ({ data }: { connectionId: string; data: string }) => {687 sentFrames.push(data);688 },689 },690 },691 };692693 694 vi.resetModules();695 const messages = await import("./messages");696 messages.setupMessageHandler();697 const vault = await import("./vault");698 await vault.ensureSettingsLoaded();699 await vault.createVault("test-pass-sync");700701 702 const sessionPromise = dispatch({703 type: "SYNC_SESSION_STARTED",704 payload: { connectionId: "test-conn" },705 });706707 708 const waitForFrame = async (): Promise<string> => {709 for (let i = 0; i < 200; i++) {710 if (sentFrames.length > 0) return sentFrames.shift()!;711 await new Promise(r => setTimeout(r, 5));712 }713 throw new Error("timeout waiting for responder frame");714 };715 const challengeB64 = await waitForFrame();716 const challengeBytes = base64ToU8(challengeB64);717 const challenge = JSON.parse(new TextDecoder().decode(challengeBytes)) as {718 type: string;719 nonce_hex: string;720 responder_public_key_hex: string;721 };722 if (challenge.type !== "CHALLENGE") throw new Error("expected CHALLENGE");723724 725 726 const responderPub = hexToBytes(challenge.responder_public_key_hex);727 const dh = sharedKey(opts.initiatorKeypair.secretKey, responderPub);728 const mac = new HMAC(SHA256, dh);729 mac.update(new TextEncoder().encode("peach-sync-v1-initiator-auth"));730 mac.update(opts.initiatorKeypair.publicKey);731 mac.update(responderPub);732 mac.update(hexToBytes(challenge.nonce_hex));733 const proof = mac.digest();734735 const proofFrame = JSON.stringify({736 type: "PROOF",737 initiator_public_key_hex: bytesToHexStr(opts.initiatorKeypair.publicKey),738 proof_hex: bytesToHexStr(proof),739 });740 741 const { deliverAndroidSyncFrame } = messages;742 deliverAndroidSyncFrame("test-conn", u8ToBase64(new TextEncoder().encode(proofFrame)));743744 745 746 747 const result = await Promise.race([748 sessionPromise.then(r => ({ kind: "resolved" as const, response: r })),749 new Promise<{ kind: "blob-stage" }>(resolve =>750 setTimeout(() => resolve({ kind: "blob-stage" }), 200)751 ),752 ]);753754 if (result.kind === "resolved") {755 const resp = result.response;756 if (resp.ok === false) {757 return { status: "rejected", vaultBlobSent: false };758 }759 760 761 return { status: "completed", vaultBlobSent: sentFrames.length > 0 };762 }763 764 return { status: "completed", vaultBlobSent: sentFrames.length > 0 };765 }766767 function pairedX25519Hex(keys: string[]): Array<Record<string, unknown>> {768 return keys.map((k, i) => ({769 deviceId: k,770 displayName: `Dev${i}`,771 x25519PublicKey: k,772 pairedAt: 1_700_000_000_000,773 lastSeenAt: 1_700_000_000_000,774 }));775 }776777 function hexToBytes(s: string): Uint8Array {778 const out = new Uint8Array(s.length / 2);779 for (let i = 0; i < s.length; i += 2) out[i / 2] = parseInt(s.slice(i, i + 2), 16);780 return out;781 }782 function bytesToHexStr(b: Uint8Array): string {783 return Array.from(b).map(x => x.toString(16).padStart(2, "0")).join("");784 }785 function base64ToU8(b64: string): Uint8Array {786 const bin = atob(b64);787 const out = new Uint8Array(bin.length);788 for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);789 return out;790 }791 function u8ToBase64(u: Uint8Array): string {792 let s = "";793 for (const b of u) s += String.fromCharCode(b);794 return btoa(s);795 }796797 it("unpaired initiator is rejected — vault blob never sent", async () => {798 const { generateKeyPair } = await import("@stablelib/x25519");799 const initKp = generateKeyPair();800 const result = await simulateResponderHandshake({801 initiatorKeypair: initKp,802 pairedX25519Hex: [],803 });804 expect(result.status).toBe("rejected");805 expect(result.vaultBlobSent).toBe(false);806 });807808 it("paired initiator passes the auth gate", async () => {809 const { generateKeyPair } = await import("@stablelib/x25519");810 const initKp = generateKeyPair();811 const initHex = Array.from(initKp.publicKey)812 .map(x => x.toString(16).padStart(2, "0"))813 .join("");814 const result = await simulateResponderHandshake({815 initiatorKeypair: initKp,816 pairedX25519Hex: [initHex],817 });818 expect(result.status).toBe("completed");819 });820821 it("forged public key in PROOF frame is rejected (proof MAC fails)", async () => {822 const { generateKeyPair } = await import("@stablelib/x25519");823 const realKp = generateKeyPair();824 const realHex = Array.from(realKp.publicKey)825 .map(x => x.toString(16).padStart(2, "0"))826 .join("");827 828 829 830 831 const forgedKp = generateKeyPair();832 const result = await simulateResponderHandshake({833 initiatorKeypair: forgedKp,834 pairedX25519Hex: [realHex], 835 });836 expect(result.status).toBe("rejected");837 expect(result.vaultBlobSent).toBe(false);838 });839 });840});