0795bfa40488
feat: Expose native session key client (v2.13.20)
- time
- 2026-04-30 14:37 UTC · 6 months ago
- machine
- koh2
- parent
- d429e6e3565f
↓ peach-browser-0795bfa4.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/sync.test.ts+33 −0src/background/sync.ts+28 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.13.19",4+ "version": "2.13.20",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.13.19",4+ "version": "2.13.20",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "2.13.19",3+ "version": "2.13.20",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/sync.test.ts+33 −0
@@ -122,6 +122,39 @@122122 await expect(promise).resolves.toBe("conn-1");123123 });124124 125+ it("decodes a base64url session key for the requested connection", async () => {126+ const chromeMock = installChromeMock();127+ const sync = await import("./sync");128+129+ const promise = sync.getNativeSessionKey("conn-1");130+ expect(chromeMock.port.postMessage).toHaveBeenCalledWith({131+ type: "GET_SESSION_KEY",132+ connection_id: "conn-1",133+ });134+135+ // 32 bytes of 0x07 → URL-safe base64 with no padding.136+ const expected = new Uint8Array(32).fill(7);137+ let binary = "";138+ for (const byte of expected) binary += String.fromCharCode(byte);139+ const key = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");140+141+ chromeMock.emit({ type: "SESSION_KEY", connection_id: "conn-1", key });142+ await expect(promise).resolves.toEqual(expected);143+ });144+145+ it("rejects a session key returned for the wrong connection", async () => {146+ const chromeMock = installChromeMock();147+ const sync = await import("./sync");148+149+ const promise = sync.getNativeSessionKey("conn-1");150+ chromeMock.emit({151+ type: "SESSION_KEY",152+ connection_id: "other",153+ key: "AA".repeat(0),154+ });155+ await expect(promise).rejects.toThrow("unexpected connection");156+ });157+125158 it("rejects pending requests when the native host disconnects", async () => {126159 const chromeMock = installChromeMock();127160 const sync = await import("./sync");src/background/sync.ts+28 −0
@@ -34,6 +34,7 @@3434 }3535 | { type: "DISCONNECT"; connection_id: string }3636 | { type: "SYNC_SEND"; connection_id: string; data: string }37+ | { type: "GET_SESSION_KEY"; connection_id: string }3738 | { type: "STATUS" };3839 3940 type HostPeer = {@@ -67,6 +68,7 @@6768 | { type: "PEER_CONNECTED"; connection_id: string; peer_addr: string }6869 | { type: "DISCONNECTED"; connection_id: string }6970 | { type: "SYNC_RECEIVED"; connection_id: string; data: string }71+ | { type: "SESSION_KEY"; connection_id: string; key: string }7072 | { type: "STATUS"; advertising: boolean; tcp_port?: number | null; connections: string[] }7173 | { type: "ERROR"; connection_id?: string | null; message: string };7274 @@ -343,6 +345,32 @@343345 344346 export function sendSyncData(connectionId: string, data: string): void {345347 sendToHost({ type: "SYNC_SEND", connection_id: connectionId, data });348+}349+350+export async function getNativeSessionKey(connectionId: string): Promise<Uint8Array> {351+ const response = await requestFromHost<Extract<HostResponse, { type: "SESSION_KEY" }>>(352+ { type: "GET_SESSION_KEY", connection_id: connectionId },353+ ["SESSION_KEY"]354+ );355+ if (response.connection_id !== connectionId) {356+ throw new Error("sync native host returned session key for unexpected connection");357+ }358+ const decoded = decodeBase64Url(response.key);359+ if (decoded.length !== 32) {360+ throw new Error(`sync native host returned ${decoded.length}-byte session key (want 32)`);361+ }362+ return decoded;363+}364+365+function decodeBase64Url(value: string): Uint8Array {366+ const padded = value367+ .replace(/-/g, "+")368+ .replace(/_/g, "/")369+ .padEnd(Math.ceil(value.length / 4) * 4, "=");370+ const binary = atob(padded);371+ const out = new Uint8Array(binary.length);372+ for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);373+ return out;346374 }347375 348376 export async function getNativeStatus(): Promise<NativeSyncStatus> {