mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +02:00
fix(desktop):add secure auth ipc bridge
This commit is contained in:
parent
7241a7a894
commit
f481d1360a
3 changed files with 101 additions and 0 deletions
|
|
@ -42,6 +42,11 @@ export const IPC_CHANNELS = {
|
||||||
// Auth token sync across windows
|
// Auth token sync across windows
|
||||||
GET_AUTH_TOKENS: 'auth:get-tokens',
|
GET_AUTH_TOKENS: 'auth:get-tokens',
|
||||||
SET_AUTH_TOKENS: 'auth:set-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
|
// Keyboard shortcut configuration
|
||||||
GET_SHORTCUTS: 'shortcuts:get',
|
GET_SHORTCUTS: 'shortcuts:get',
|
||||||
SET_SHORTCUTS: 'shortcuts:set',
|
SET_SHORTCUTS: 'shortcuts:set',
|
||||||
|
|
|
||||||
86
surfsense_desktop/src/modules/secret-store.ts
Normal file
86
surfsense_desktop/src/modules/secret-store.ts
Normal file
|
|
@ -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<void>;
|
||||||
|
get(key: string): Promise<string | null>;
|
||||||
|
clear(key: string): Promise<void>;
|
||||||
|
isHardwareBacked(): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memoryStore = new Map<string, string>();
|
||||||
|
const storePath = path.join(app.getPath('userData'), 'secrets.enc.json');
|
||||||
|
|
||||||
|
async function readDiskStore(): Promise<Record<string, string>> {
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(storePath, 'utf8');
|
||||||
|
return JSON.parse(raw) as Record<string, string>;
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeDiskStore(data: Record<string, string>): Promise<void> {
|
||||||
|
await fs.mkdir(path.dirname(storePath), { recursive: true });
|
||||||
|
await fs.writeFile(storePath, JSON.stringify(data), { encoding: 'utf8', mode: 0o600 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function canPersistEncryptedSecrets(): Promise<boolean> {
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -83,6 +83,16 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
getAuthTokens: () => ipcRenderer.invoke(IPC_CHANNELS.GET_AUTH_TOKENS),
|
getAuthTokens: () => ipcRenderer.invoke(IPC_CHANNELS.GET_AUTH_TOKENS),
|
||||||
setAuthTokens: (bearer: string, refresh: string) =>
|
setAuthTokens: (bearer: string, refresh: string) =>
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.SET_AUTH_TOKENS, { bearer, refresh }),
|
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
|
// Keyboard shortcut configuration
|
||||||
getShortcuts: () => ipcRenderer.invoke(IPC_CHANNELS.GET_SHORTCUTS),
|
getShortcuts: () => ipcRenderer.invoke(IPC_CHANNELS.GET_SHORTCUTS),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue