Skip to content

Commit 1437496

Browse files
committed
feat: trust rin apis in electron edge
1 parent 5bd8292 commit 1437496

File tree

4 files changed

+129
-1
lines changed

4 files changed

+129
-1
lines changed

src-electron/main-cred-ipc.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const { ipcMain } = require('electron');
2+
const crypto = require('crypto');
3+
4+
// Per-window AES trust map (mirrors Tauri's WindowAesTrust)
5+
// Uses webContents.id which persists across page reloads but changes when window is destroyed
6+
const windowTrustMap = new Map(); // webContentsId -> { key, iv }
7+
8+
// Keytar for system keychain (libsecret on Linux, Keychain on macOS, Credential Vault on Windows)
9+
let keytar = null;
10+
try {
11+
keytar = require('keytar');
12+
} catch (e) {
13+
console.warn('keytar not available, credential storage will not work');
14+
}
15+
16+
const PHOENIX_CRED_PREFIX = 'phcode_electron_';
17+
18+
function registerCredIpcHandlers() {
19+
// Trust window AES key - can only be called once per window
20+
ipcMain.handle('trust-window-aes-key', (event, key, iv) => {
21+
const webContentsId = event.sender.id;
22+
23+
if (windowTrustMap.has(webContentsId)) {
24+
throw new Error('Trust has already been established for this window.');
25+
}
26+
27+
// Validate key (64 hex chars = 32 bytes for AES-256)
28+
if (!/^[0-9a-fA-F]{64}$/.test(key)) {
29+
throw new Error('Invalid AES key. Must be 64 hex characters.');
30+
}
31+
// Validate IV (24 hex chars = 12 bytes for AES-GCM)
32+
if (!/^[0-9a-fA-F]{24}$/.test(iv)) {
33+
throw new Error('Invalid IV. Must be 24 hex characters.');
34+
}
35+
36+
windowTrustMap.set(webContentsId, { key, iv });
37+
console.log(`AES trust established for webContents: ${webContentsId}`);
38+
});
39+
40+
// Remove trust - requires matching key/iv
41+
ipcMain.handle('remove-trust-window-aes-key', (event, key, iv) => {
42+
const webContentsId = event.sender.id;
43+
const stored = windowTrustMap.get(webContentsId);
44+
45+
if (!stored) {
46+
throw new Error('No trust established for this window.');
47+
}
48+
if (stored.key !== key || stored.iv !== iv) {
49+
throw new Error('Provided key and IV do not match.');
50+
}
51+
52+
windowTrustMap.delete(webContentsId);
53+
console.log(`AES trust removed for webContents: ${webContentsId}`);
54+
});
55+
56+
// Store credential in system keychain
57+
ipcMain.handle('store-credential', async (event, scopeName, secretVal) => {
58+
if (!keytar) {
59+
throw new Error('keytar module not available.');
60+
}
61+
const service = PHOENIX_CRED_PREFIX + scopeName;
62+
await keytar.setPassword(service, process.env.USER || 'user', secretVal);
63+
});
64+
65+
// Get credential (encrypted with window's AES key)
66+
ipcMain.handle('get-credential', async (event, scopeName) => {
67+
if (!keytar) {
68+
throw new Error('keytar module not available.');
69+
}
70+
71+
const webContentsId = event.sender.id;
72+
const trustData = windowTrustMap.get(webContentsId);
73+
if (!trustData) {
74+
throw new Error('Trust needs to be established first.');
75+
}
76+
77+
const service = PHOENIX_CRED_PREFIX + scopeName;
78+
const credential = await keytar.getPassword(service, process.env.USER || 'user');
79+
if (!credential) {
80+
return null;
81+
}
82+
83+
// Encrypt with AES-256-GCM (same as Tauri)
84+
const keyBytes = Buffer.from(trustData.key, 'hex');
85+
const ivBytes = Buffer.from(trustData.iv, 'hex');
86+
const cipher = crypto.createCipheriv('aes-256-gcm', keyBytes, ivBytes);
87+
let encrypted = cipher.update(credential, 'utf8', 'hex');
88+
encrypted += cipher.final('hex');
89+
const authTag = cipher.getAuthTag().toString('hex');
90+
91+
return encrypted + authTag; // Return ciphertext + authTag as hex string
92+
});
93+
94+
// Delete credential from system keychain
95+
ipcMain.handle('delete-credential', async (event, scopeName) => {
96+
if (!keytar) {
97+
throw new Error('keytar module not available.');
98+
}
99+
const service = PHOENIX_CRED_PREFIX + scopeName;
100+
await keytar.deletePassword(service, process.env.USER || 'user');
101+
});
102+
}
103+
104+
// Clean up trust when window closes
105+
function cleanupWindowTrust(webContentsId) {
106+
if (windowTrustMap.has(webContentsId)) {
107+
windowTrustMap.delete(webContentsId);
108+
console.log(`AES trust auto-removed for closed webContents: ${webContentsId}`);
109+
}
110+
}
111+
112+
module.exports = { registerCredIpcHandlers, cleanupWindowTrust };

src-electron/main.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const fs = require('fs');
44

55
const { registerAppIpcHandlers, terminateAllProcesses } = require('./main-app-ipc');
66
const { registerFsIpcHandlers, getAppDataDir } = require('./main-fs-ipc');
7+
const { registerCredIpcHandlers, cleanupWindowTrust } = require('./main-cred-ipc');
78

89
// In-memory key-value store shared across all windows (mirrors Tauri's put_item/get_all_items)
910
// Used for multi-window storage synchronization
@@ -26,6 +27,10 @@ async function createWindow() {
2627
// Load the test page from the http-server
2728
mainWindow.loadURL('http://localhost:8000/src/');
2829

30+
mainWindow.webContents.on('destroyed', () => {
31+
cleanupWindowTrust(mainWindow.webContents.id);
32+
});
33+
2934
mainWindow.on('closed', () => {
3035
mainWindow = null;
3136
});
@@ -40,6 +45,7 @@ async function gracefulShutdown(exitCode = 0) {
4045
// Register all IPC handlers
4146
registerAppIpcHandlers();
4247
registerFsIpcHandlers();
48+
registerCredIpcHandlers();
4349

4450
/**
4551
* IPC handlers for electronAPI

src-electron/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"productName": "Phoenix Code Experimental Build",
66
"description": "Phoenix Code Experimental Build",
77
"main": "main.js",
8+
"dependencies": {
9+
"keytar": "^7.9.0"
10+
},
811
"devDependencies": {
912
"electron": "^40.0.0"
1013
}

src-electron/preload.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
8080

8181
// Path to src-node for development (../phoenix/src-node)
8282
// Throws if path does not exist
83-
getSrcNodePath: () => ipcRenderer.invoke('get-src-node-path')
83+
getSrcNodePath: () => ipcRenderer.invoke('get-src-node-path'),
84+
85+
// Trust ring / credential APIs
86+
trustWindowAesKey: (key, iv) => ipcRenderer.invoke('trust-window-aes-key', key, iv),
87+
removeTrustWindowAesKey: (key, iv) => ipcRenderer.invoke('remove-trust-window-aes-key', key, iv),
88+
storeCredential: (scopeName, secretVal) => ipcRenderer.invoke('store-credential', scopeName, secretVal),
89+
getCredential: (scopeName) => ipcRenderer.invoke('get-credential', scopeName),
90+
deleteCredential: (scopeName) => ipcRenderer.invoke('delete-credential', scopeName)
8491
});

0 commit comments

Comments
 (0)