-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsecurity-modal.js
More file actions
305 lines (268 loc) · 15 KB
/
Copy pathsecurity-modal.js
File metadata and controls
305 lines (268 loc) · 15 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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
import { state, events } from './state.js';
import { security } from './security.js';
import { showToast, setLoading } from './utils.js';
import { storeServerKey, storeShareBLocally, unlockSession, hasServerKey, sessionNeedsUnlock } from './job-queue.js';
import { devLog } from './thinking.js';
const PIN_MODAL_HTML = `
<div id="pin-modal" class="fixed inset-0 z-[70] hidden flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/80 backdrop-blur-md" id="pin-backdrop"></div>
<div class="relative w-full max-w-sm bg-gray-900 border border-gray-700 rounded-2xl shadow-2xl p-6 animate-float-in">
<div class="text-center mb-6">
<div class="w-12 h-12 bg-blue-900/30 text-blue-400 rounded-full flex items-center justify-center mx-auto mb-3 text-xl border border-blue-500/20"><i class="fa-solid fa-lock"></i></div>
<h3 id="pin-title" class="text-lg font-semibold text-white">Enter Security PIN</h3>
<p id="pin-subtitle" class="text-sm text-gray-400 mt-1">Unlock your API key to continue</p>
</div>
<div id="pin-setup-fields" class="space-y-4 hidden">
<div><label class="block text-xs font-medium text-gray-500 mb-1">API Key</label><input type="password" id="input-api-key" placeholder="sk-or-..." class="w-full bg-gray-800 border border-gray-700 text-white rounded-lg p-2.5 text-sm focus:ring-blue-500 focus:border-blue-500 font-mono"></div>
<div><label class="block text-xs font-medium text-gray-500 mb-1">Create PIN (4-8 digits)</label><input type="password" id="input-pin-setup" maxlength="8" placeholder="••••" class="w-full bg-gray-800 border border-gray-700 text-white rounded-lg p-2.5 text-center text-lg tracking-[0.5em] focus:ring-blue-500 focus:border-blue-500 font-mono placeholder-gray-600"></div>
<div class="text-[10px] text-gray-500 leading-tight p-2 bg-gray-800/50 rounded border border-gray-700/50"><i class="fa-solid fa-info-circle mr-1"></i> Your key is encrypted locally with this PIN. We cannot recover it if you forget the PIN.</div>
</div>
<div id="pin-entry-fields" class="space-y-4">
<input type="password" id="input-pin-entry" maxlength="8" placeholder="••••" class="w-full bg-gray-800 border border-gray-700 text-white rounded-lg p-3 text-center text-2xl tracking-[0.5em] focus:ring-blue-500 focus:border-blue-500 font-mono placeholder-gray-600 transition-all">
<p id="pin-error" class="text-red-400 text-xs text-center hidden"><i class="fa-solid fa-circle-exclamation mr-1"></i> Incorrect PIN</p>
</div>
<div class="mt-6 flex gap-3">
<button id="btn-cancel-pin" class="flex-1 py-2.5 px-4 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-xl transition-colors text-sm">Cancel</button>
<button id="btn-confirm-pin" class="flex-1 py-2.5 px-4 bg-blue-600 hover:bg-blue-500 text-white rounded-xl shadow-lg shadow-blue-900/20 transition-colors text-sm font-medium">Confirm</button>
</div>
</div>
</div>`;
let callbacks = {
saveSettings: () => {},
updateUI: () => {},
fetchModels: () => {}
};
export function initSecurityModal(providedCallbacks) {
callbacks = { ...callbacks, ...providedCallbacks };
document.body.insertAdjacentHTML('beforeend', PIN_MODAL_HTML);
setupSecurityListeners();
}
function setupSecurityListeners() {
const d = {
pinModal: document.getElementById('pin-modal'),
pinBackdrop: document.getElementById('pin-backdrop'),
btnCancelPin: document.getElementById('btn-cancel-pin'),
btnConfirmPin: document.getElementById('btn-confirm-pin'),
inputPinEntry: document.getElementById('input-pin-entry'),
inputPinSetup: document.getElementById('input-pin-setup'),
inputApiKey: document.getElementById('input-api-key')
};
d.btnCancelPin?.addEventListener('click', closePinModal);
d.pinBackdrop?.addEventListener('click', closePinModal);
d.btnConfirmPin?.addEventListener('click', handlePinConfirm);
[d.inputPinEntry, d.inputPinSetup, d.inputApiKey].forEach(el =>
el?.addEventListener('keydown', e => { if(e.key === 'Enter') handlePinConfirm(); })
);
// Listen for session-needs-unlock event (fired when session exists but API key not in memory)
events.addEventListener('session-needs-unlock', (e) => {
devLog('Session needs unlock event received:', e.detail);
// Don't show the modal automatically - just log it
// The user will see the unlock button in the UI
// Showing a modal on page load is disruptive
});
}
/**
* Check if session needs unlock on page load and show a subtle prompt
* Called from app.js after initialization
*/
export function checkSessionOnLoad() {
if (sessionNeedsUnlock()) {
devLog('Session needs unlock on load - showing notification');
// Show a toast notification instead of blocking modal
showToast('Session expired. Click unlock to continue using background jobs.', 5000);
return true;
}
return false;
}
export function startPinFlow(mode, customTitle) {
state.pinFlow = mode;
const modal = document.getElementById('pin-modal');
modal.classList.remove('hidden');
document.getElementById('pin-error').classList.add('hidden');
document.getElementById('input-pin-entry').value = '';
document.getElementById('input-pin-setup').value = '';
document.getElementById('input-api-key').value = '';
if (mode === 'setup') {
document.getElementById('pin-title').textContent = "Configure OpenRouter Key";
document.getElementById('pin-subtitle').textContent = "Set a PIN to encrypt your API key locally";
document.getElementById('input-api-key').placeholder = "sk-or-...";
document.getElementById('pin-setup-fields').classList.remove('hidden');
document.getElementById('pin-entry-fields').classList.add('hidden');
document.getElementById('input-api-key').focus();
} else if (mode === 'setup-hf') {
document.getElementById('pin-title').textContent = "Configure HuggingFace Key";
document.getElementById('pin-subtitle').textContent = "Set a PIN to encrypt your HuggingFace API key";
document.getElementById('input-api-key').placeholder = "hf_...";
document.getElementById('pin-setup-fields').classList.remove('hidden');
document.getElementById('pin-entry-fields').classList.add('hidden');
document.getElementById('input-api-key').focus();
} else if (mode === 'setup-server-key') {
document.getElementById('pin-title').textContent = "Enable Background Jobs";
document.getElementById('pin-subtitle').textContent = "Your key is split for zero-knowledge security";
document.getElementById('input-api-key').placeholder = "sk-or-... (OpenRouter API Key)";
document.getElementById('pin-setup-fields').classList.remove('hidden');
document.getElementById('pin-entry-fields').classList.add('hidden');
document.getElementById('input-api-key').focus();
} else if (mode === 'unlock-session') {
document.getElementById('pin-title').textContent = customTitle || "Unlock Session";
document.getElementById('pin-subtitle').textContent = "Enter PIN to activate background jobs (2h session)";
document.getElementById('pin-setup-fields').classList.add('hidden');
document.getElementById('pin-entry-fields').classList.remove('hidden');
document.getElementById('input-pin-entry').focus();
} else {
document.getElementById('pin-title').textContent = customTitle || "Enter PIN";
document.getElementById('pin-subtitle').textContent = "Unlock your API key to continue";
document.getElementById('pin-setup-fields').classList.add('hidden');
document.getElementById('pin-entry-fields').classList.remove('hidden');
document.getElementById('input-pin-entry').focus();
}
}
function closePinModal() {
document.getElementById('pin-modal').classList.add('hidden');
state.pinFlow = null;
if (state.pendingPrompt) {
state.pendingPrompt = null;
setLoading(false);
window.dispatchEvent(new Event('loading-state-changed'));
}
}
async function handlePinConfirm() {
const errorEl = document.getElementById('pin-error');
errorEl.classList.add('hidden');
if (state.pinFlow === 'setup') {
const apiKey = document.getElementById('input-api-key').value.trim();
const pin = document.getElementById('input-pin-setup').value.trim();
if (!apiKey.startsWith('sk-or-')) return showPinError("Invalid Key (must start with sk-or-)");
if (pin.length < 4) return showPinError("PIN must be 4+ digits");
try {
const encryptedData = await security.encrypt(apiKey, pin);
localStorage.setItem('openrouter_enc', JSON.stringify(encryptedData));
state.settings.hasKey = true;
state.settings.useOpenRouter = true;
callbacks.saveSettings();
callbacks.updateUI();
closePinModal();
showToast("OpenRouter key saved");
callbacks.fetchModels();
} catch (e) { showPinError("Encryption failed"); }
} else if (state.pinFlow === 'setup-hf') {
const apiKey = document.getElementById('input-api-key').value.trim();
const pin = document.getElementById('input-pin-setup').value.trim();
if (!apiKey.startsWith('hf_')) return showPinError("Invalid Key (must start with hf_)");
if (pin.length < 4) return showPinError("PIN must be 4+ digits");
try {
const encryptedData = await security.encrypt(apiKey, pin);
localStorage.setItem('huggingface_enc', JSON.stringify(encryptedData));
state.settings.hasHfKey = true;
callbacks.saveSettings();
callbacks.updateUI();
closePinModal();
showToast("HuggingFace key saved");
} catch (e) { showPinError("Encryption failed"); }
} else if (state.pinFlow === 'setup-server-key') {
// Server key setup with Shamir's Secret Sharing
const apiKey = document.getElementById('input-api-key').value.trim();
const pin = document.getElementById('input-pin-setup').value.trim();
console.log('[Background Jobs] Starting server key setup...');
console.log('[Background Jobs] API key length:', apiKey.length, 'starts with sk-or-:', apiKey.startsWith('sk-or-'));
console.log('[Background Jobs] PIN length:', pin.length);
if (!apiKey.startsWith('sk-or-')) {
console.log('[Background Jobs] Invalid API key format');
return showPinError("Invalid Key (must start with sk-or-)");
}
if (pin.length < 8) {
console.log('[Background Jobs] PIN too short');
return showPinError("PIN must be 8+ digits for server keys");
}
console.log('[Background Jobs] Validation passed, calling setLoading...');
try {
setLoading(true, "Setting up zero-knowledge key storage...");
console.log('[Background Jobs] setLoading done, calling storeServerKey...');
// 1. Send to server - receives Share B back
const result = await storeServerKey(apiKey, {
trainingOptOut: state.settings.trainingOptOut
});
console.log('[Background Jobs] storeServerKey result:', result);
const { shareB, keyId } = result;
// 2. Encrypt Share B locally with PIN
console.log('[Background Jobs] Storing Share B locally...');
await storeShareBLocally(shareB, pin);
// Clear client-side key state to avoid confusion
// When using server keys, we don't want stale client-side key data
if (security.isUnlocked()) {
console.log('[Background Jobs] Clearing client-side key state (switching to server keys)');
security.lock();
}
state.settings.hasServerKey = true;
state.settings.useBackgroundJobs = true;
callbacks.saveSettings();
callbacks.updateUI();
closePinModal();
setLoading(false);
showToast("Background jobs enabled! Your key is now sharded.");
// Automatically unlock the session
try {
console.log('[Background Jobs] Auto-unlocking session...');
await unlockSession(pin);
showToast("Session unlocked for 2 hours");
} catch (e) {
console.error("[Background Jobs] Auto-unlock failed:", e);
}
} catch (e) {
setLoading(false);
console.error("[Background Jobs] Server key setup failed:", e);
console.error("[Background Jobs] Error details:", e.stack || e);
showPinError(e.message || "Failed to setup server key");
}
} else if (state.pinFlow === 'unlock-session') {
// Unlock session for background jobs
const pin = document.getElementById('input-pin-entry').value.trim();
if (!hasServerKey()) {
return showPinError("No server key configured");
}
try {
setLoading(true, "Unlocking session...");
await unlockSession(pin);
closePinModal();
setLoading(false);
callbacks.updateUI();
showToast("Session unlocked for 2 hours");
// Note: unlockSession() already dispatches 'session-unlocked' event
// Don't dispatch it again here to avoid duplicate API calls
if (state.pendingPrompt) events.dispatchEvent(new CustomEvent('retry-generation'));
} catch (e) {
setLoading(false);
console.error("Session unlock failed:", e);
showPinError(e.message || "Failed to unlock session");
}
} else if (state.pinFlow === 'unlock') {
const pin = document.getElementById('input-pin-entry').value.trim();
const encDataStr = localStorage.getItem('openrouter_enc');
if (!encDataStr) return closePinModal();
try {
const success = await security.decrypt(JSON.parse(encDataStr), pin);
if (success) {
// Also try to unlock HuggingFace key if it exists
const hfEncDataStr = localStorage.getItem('huggingface_enc');
if (hfEncDataStr) {
try {
await security.decryptHf(JSON.parse(hfEncDataStr), pin);
} catch (e) {
// HuggingFace key might have different PIN - that's ok
}
}
closePinModal();
callbacks.updateUI();
showToast("Unlocked");
if (state.pendingPrompt) events.dispatchEvent(new CustomEvent('retry-generation'));
} else {
showPinError("Incorrect PIN");
}
} catch (e) { showPinError("Decryption error"); }
}
}
function showPinError(msg) {
const el = document.getElementById('pin-error');
el.textContent = msg;
el.classList.remove('hidden');
}