-
-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathmain-cred-ipc.js
More file actions
151 lines (129 loc) · 6.27 KB
/
main-cred-ipc.js
File metadata and controls
151 lines (129 loc) · 6.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
const { ipcMain } = require('electron');
const crypto = require('crypto');
const { assertTrusted } = require('./ipc-security');
// Per-window AES trust map (mirrors Tauri's WindowAesTrust)
// Uses webContents.id which persists across page reloads but changes when window is destroyed
const windowTrustMap = new Map(); // webContentsId -> { key, iv }
// Keytar for system keychain (libsecret on Linux, Keychain on macOS, Credential Vault on Windows)
let keytar = null;
try {
keytar = require('keytar');
} catch (e) {
console.warn('keytar not available, credential storage will not work');
}
const PHOENIX_CRED_PREFIX = 'phcode_electron_';
function registerCredIpcHandlers() {
// Trust window AES key - can only be called once per page load
ipcMain.handle('trust-window-aes-key', (event, key, iv) => {
assertTrusted(event);
const webContentsId = event.sender.id;
if (windowTrustMap.has(webContentsId)) {
throw new Error('Trust has already been established for this window.');
}
// Validate key (64 hex chars = 32 bytes for AES-256)
if (!/^[0-9a-fA-F]{64}$/.test(key)) {
throw new Error('Invalid AES key. Must be 64 hex characters.');
}
// Validate IV (24 hex chars = 12 bytes for AES-GCM)
if (!/^[0-9a-fA-F]{24}$/.test(iv)) {
throw new Error('Invalid IV. Must be 24 hex characters.');
}
windowTrustMap.set(webContentsId, { key, iv });
// Lazy require to avoid circular dependency
const { getWindowLabel } = require('./main-window-ipc');
console.log(`AES trust established for window: ${getWindowLabel(webContentsId)} (webContentsId: ${webContentsId})`);
});
// Remove trust - requires matching key/iv
ipcMain.handle('remove-trust-window-aes-key', (event, key, iv) => {
assertTrusted(event);
const webContentsId = event.sender.id;
const stored = windowTrustMap.get(webContentsId);
if (!stored) {
// Match Tauri's error message
throw new Error('No trust association found for this window.');
}
if (stored.key !== key || stored.iv !== iv) {
throw new Error('Provided key and IV do not match.');
}
windowTrustMap.delete(webContentsId);
const { getWindowLabel } = require('./main-window-ipc');
console.log(`AES trust removed for window: ${getWindowLabel(webContentsId)} (webContentsId: ${webContentsId})`);
});
// Special marker for empty string credentials (keytar doesn't allow empty passwords)
// Using a unique string that's unlikely to be a real credential value
const EMPTY_CREDENTIAL_MARKER = '___PHCODE_EMPTY_CREDENTIAL_MARKER___';
// Store credential in system keychain
ipcMain.handle('store-credential', async (event, scopeName, secretVal) => {
assertTrusted(event);
if (!keytar) {
throw new Error('keytar module not available.');
}
const service = PHOENIX_CRED_PREFIX + scopeName;
// Handle empty strings by storing a special marker (keytar requires non-empty passwords)
// Check for empty string, null, or undefined - all become the marker
const isEmpty = secretVal === '' || secretVal === null || secretVal === undefined;
const valueToStore = isEmpty ? EMPTY_CREDENTIAL_MARKER : secretVal;
await keytar.setPassword(service, process.env.USER || 'user', valueToStore);
});
// Get credential (encrypted with window's AES key)
ipcMain.handle('get-credential', async (event, scopeName) => {
assertTrusted(event);
if (!keytar) {
throw new Error('keytar module not available.');
}
const webContentsId = event.sender.id;
const trustData = windowTrustMap.get(webContentsId);
if (!trustData) {
// Match Tauri's error message
throw new Error('Trust needs to be first established for this window to get or set credentials.');
}
const service = PHOENIX_CRED_PREFIX + scopeName;
const account = process.env.USER || 'user';
let credential = await keytar.getPassword(service, account);
if (credential === null || credential === undefined) {
return null;
}
// Convert empty credential marker back to empty string
if (credential === EMPTY_CREDENTIAL_MARKER) {
credential = '';
}
// Encrypt with AES-256-GCM (same as Tauri)
const keyBytes = Buffer.from(trustData.key, 'hex');
const ivBytes = Buffer.from(trustData.iv, 'hex');
const cipher = crypto.createCipheriv('aes-256-gcm', keyBytes, ivBytes);
let encrypted = cipher.update(credential, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
// For empty credentials, encrypted will be empty string, but authTag will still be present
const result = encrypted + authTag;
return result;
});
// Delete credential from system keychain
ipcMain.handle('delete-credential', async (event, scopeName) => {
assertTrusted(event);
if (!keytar) {
throw new Error('keytar module not available.');
}
const service = PHOENIX_CRED_PREFIX + scopeName;
const deleted = await keytar.deletePassword(service, process.env.USER || 'user');
// Match Tauri's behavior: throw if credential didn't exist
if (!deleted) {
throw new Error('No matching entry found in secure storage');
}
});
}
// Clean up trust when window closes
function cleanupWindowTrust(webContentsId, windowLabel) {
if (windowTrustMap.has(webContentsId)) {
windowTrustMap.delete(webContentsId);
console.log(`AES trust auto-removed for closed window: ${windowLabel} (webContentsId: ${webContentsId})`);
}
}
// Clear trust on navigation (page reload) - allows fresh trust to be established after reload
function clearTrustOnNavigation(webContentsId, windowLabel) {
if (windowTrustMap.has(webContentsId)) {
windowTrustMap.delete(webContentsId);
console.log(`AES trust cleared for navigation in window: ${windowLabel} (webContentsId: ${webContentsId})`);
}
}
module.exports = { registerCredIpcHandlers, cleanupWindowTrust, clearTrustOnNavigation };