From f481d1360a3debbcd863125e75c0e417f1bc1d29 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:55:36 +0530 Subject: [PATCH] fix(desktop):add secure auth ipc bridge --- surfsense_desktop/src/ipc/channels.ts | 5 ++ surfsense_desktop/src/modules/secret-store.ts | 86 +++++++++++++++++++ surfsense_desktop/src/preload.ts | 10 +++ 3 files changed, 101 insertions(+) create mode 100644 surfsense_desktop/src/modules/secret-store.ts diff --git a/surfsense_desktop/src/ipc/channels.ts b/surfsense_desktop/src/ipc/channels.ts index 17daab9a6..43bdce312 100644 --- a/surfsense_desktop/src/ipc/channels.ts +++ b/surfsense_desktop/src/ipc/channels.ts @@ -42,6 +42,11 @@ export const IPC_CHANNELS = { // Auth token sync across windows GET_AUTH_TOKENS: 'auth:get-tokens', SET_AUTH_TOKENS: 'auth:set-tokens', + GET_ACCESS_TOKEN: 'auth:get-access-token', + REFRESH_ACCESS_TOKEN: 'auth:refresh-access-token', + LOGOUT: 'auth:logout', + AUTH_CHANGED: 'auth:changed', + AUTH_START_GOOGLE: 'auth:start-google', // Keyboard shortcut configuration GET_SHORTCUTS: 'shortcuts:get', SET_SHORTCUTS: 'shortcuts:set', diff --git a/surfsense_desktop/src/modules/secret-store.ts b/surfsense_desktop/src/modules/secret-store.ts new file mode 100644 index 000000000..28a1cfc4b --- /dev/null +++ b/surfsense_desktop/src/modules/secret-store.ts @@ -0,0 +1,86 @@ +import { app, safeStorage } from 'electron'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +export interface SecretStore { + set(key: string, value: string): Promise; + get(key: string): Promise; + clear(key: string): Promise; + isHardwareBacked(): Promise; +} + +const memoryStore = new Map(); +const storePath = path.join(app.getPath('userData'), 'secrets.enc.json'); + +async function readDiskStore(): Promise> { + try { + const raw = await fs.readFile(storePath, 'utf8'); + return JSON.parse(raw) as Record; + } catch { + return {}; + } +} + +async function writeDiskStore(data: Record): Promise { + await fs.mkdir(path.dirname(storePath), { recursive: true }); + await fs.writeFile(storePath, JSON.stringify(data), { encoding: 'utf8', mode: 0o600 }); +} + +async function canPersistEncryptedSecrets(): Promise { + try { + if (safeStorage.getSelectedStorageBackend?.() === 'basic_text') { + return false; + } + return await safeStorage.isAsyncEncryptionAvailable(); + } catch { + return false; + } +} + +export const secretStore: SecretStore = { + async set(key, value) { + if (!(await canPersistEncryptedSecrets())) { + memoryStore.set(key, value); + return; + } + + const encrypted = await safeStorage.encryptStringAsync(value); + const data = await readDiskStore(); + data[key] = encrypted.toString('base64'); + await writeDiskStore(data); + }, + + async get(key) { + if (!(await canPersistEncryptedSecrets())) { + return memoryStore.get(key) ?? null; + } + + const data = await readDiskStore(); + const encoded = data[key]; + if (!encoded) return null; + + try { + const decrypted = await safeStorage.decryptStringAsync(Buffer.from(encoded, 'base64')); + if (decrypted.shouldReEncrypt) { + await this.set(key, decrypted.result); + } + return decrypted.result; + } catch { + await this.clear(key); + return null; + } + }, + + async clear(key) { + memoryStore.delete(key); + const data = await readDiskStore(); + if (key in data) { + delete data[key]; + await writeDiskStore(data); + } + }, + + async isHardwareBacked() { + return canPersistEncryptedSecrets(); + }, +}; diff --git a/surfsense_desktop/src/preload.ts b/surfsense_desktop/src/preload.ts index 97232179c..548610f03 100644 --- a/surfsense_desktop/src/preload.ts +++ b/surfsense_desktop/src/preload.ts @@ -83,6 +83,16 @@ contextBridge.exposeInMainWorld('electronAPI', { getAuthTokens: () => ipcRenderer.invoke(IPC_CHANNELS.GET_AUTH_TOKENS), setAuthTokens: (bearer: string, refresh: string) => ipcRenderer.invoke(IPC_CHANNELS.SET_AUTH_TOKENS, { bearer, refresh }), + getAccessToken: () => ipcRenderer.invoke(IPC_CHANNELS.GET_ACCESS_TOKEN), + refreshAccessToken: () => ipcRenderer.invoke(IPC_CHANNELS.REFRESH_ACCESS_TOKEN), + logout: () => ipcRenderer.invoke(IPC_CHANNELS.LOGOUT), + startGoogleOAuth: () => ipcRenderer.invoke(IPC_CHANNELS.AUTH_START_GOOGLE), + onAuthChanged: (callback: (payload: { authed: boolean; accessToken: string | null }) => void) => { + const listener = (_event: Electron.IpcRendererEvent, payload: { authed: boolean; accessToken: string | null }) => + callback(payload); + ipcRenderer.on(IPC_CHANNELS.AUTH_CHANGED, listener); + return () => ipcRenderer.removeListener(IPC_CHANNELS.AUTH_CHANGED, listener); + }, // Keyboard shortcut configuration getShortcuts: () => ipcRenderer.invoke(IPC_CHANNELS.GET_SHORTCUTS),