Skip to content

Commit 12eadff

Browse files
authored
Merge pull request #57 from drips-stellar/fix/issue-13-harden-api-key-handling
fix(security): harden API key handling with security warning and ephemeral storage
2 parents 3eb6a40 + 52184aa commit 12eadff

3 files changed

Lines changed: 122 additions & 4 deletions

File tree

public/index.html

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,25 @@
615615
.main { height: auto; overflow: visible; }
616616
.g3, .g4 { grid-template-columns: 1fr 1fr; }
617617
}
618+
619+
/* ═══ SECURITY MODAL ═══ */
620+
#security-modal {
621+
animation: fadeIn 0.2s ease-out;
622+
}
623+
624+
@keyframes fadeIn {
625+
from { opacity: 0; }
626+
to { opacity: 1; }
627+
}
628+
629+
#security-modal > div {
630+
animation: slideUp 0.3s ease-out;
631+
}
632+
633+
@keyframes slideUp {
634+
from { transform: translateY(20px); opacity: 0; }
635+
to { transform: translateY(0); opacity: 1; }
636+
}
618637
</style>
619638
</head>
620639
<body>
@@ -756,14 +775,15 @@ <h1>StellarMind</h1>
756775
<div class="card-h"><h2>🧠 Claude AI Configuration</h2><span class="tag" id="key-status-tag">Checking...</span></div>
757776
<div class="card-b">
758777
<div id="api-key-config">
759-
<p class="lbl" style="margin-bottom:10px;">Configure your Anthropic API key to enable real AI responses (starts with sk-ant-). If not set, the system uses high-quality fallback agents.</p>
778+
<p class="lbl" style="margin-bottom:10px;color:var(--text-secondary);">Configure your Anthropic API key to enable real AI responses (starts with sk-ant-). Your key is stored in server memory only and never persisted.</p>
760779
<div class="task-row">
761780
<div class="task-input-w">
762781
<input type="password" id="api-key-input" class="task-input" placeholder="sk-ant-..." />
763782
</div>
764-
<button class="btn" onclick="saveApiKey()" id="btn-save-key">Update Key</button>
783+
<button class="btn" onclick="showSecurityWarning()" id="btn-save-key">Update Key</button>
765784
</div>
766785
<p class="sc-sub" id="key-hint" style="margin-top:10px;">Current: Loading...</p>
786+
<p class="sc-sub" style="margin-top:8px;color:var(--text-muted);font-size:10px;">💡 Tip: Your key is ephemeral and cleared when the server restarts. For production, use environment variables.</p>
767787
</div>
768788
</div>
769789
</div>
@@ -778,6 +798,34 @@ <h1>StellarMind</h1>
778798
</div>
779799
</div>
780800

801+
<!-- ═══ SECURITY WARNING MODAL ═══ -->
802+
<div id="security-modal" role="dialog" aria-modal="true" aria-labelledby="security-modal-title" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:100;align-items:center;justify-content:center;">
803+
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:24px;max-width:420px;box-shadow:0 20px 60px rgba(0,0,0,0.5);">
804+
<h3 id="security-modal-title" style="font-size:16px;font-weight:700;margin-bottom:12px;color:var(--red);">🔐 API Key Security Warning</h3>
805+
<div style="font-size:12px;color:var(--text-secondary);line-height:1.7;margin-bottom:16px;">
806+
<p style="margin-bottom:10px;"><strong>Your API key will be:</strong></p>
807+
<ul style="margin-left:16px;margin-bottom:10px;">
808+
<li>✓ Sent over HTTPS only</li>
809+
<li>✓ Stored in server memory (session-only)</li>
810+
<li>✓ Never persisted to disk or logs</li>
811+
<li>✓ Cleared when server restarts</li>
812+
</ul>
813+
<p style="margin-bottom:10px;"><strong>Your API key will NOT be:</strong></p>
814+
<ul style="margin-left:16px;margin-bottom:10px;">
815+
<li>✗ Stored in browser storage (localStorage/sessionStorage)</li>
816+
<li>✗ Sent to third parties</li>
817+
<li>✗ Logged or exposed in error messages</li>
818+
<li>✗ Persisted across server restarts</li>
819+
</ul>
820+
<p style="margin-top:10px;color:var(--amber);"><strong>⚠️ Only submit your key if you trust this server.</strong></p>
821+
</div>
822+
<div style="display:flex;gap:10px;justify-content:flex-end;">
823+
<button class="btn" style="background:var(--bg-input);color:var(--text-primary);padding:9px 16px;font-size:11px;" onclick="closeSecurityModal()">Cancel</button>
824+
<button class="btn" style="padding:9px 16px;font-size:11px;" onclick="confirmApiKeySubmission()">I Understand, Submit Key</button>
825+
</div>
826+
</div>
827+
</div>
828+
781829
<!-- ═══ PAGE: TRANSACTIONS ═══ -->
782830
<div class="page" id="page-transactions">
783831
<div class="card">
@@ -1256,6 +1304,72 @@ <h1>StellarMind</h1>
12561304

12571305
document.getElementById('task-input').addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); runOrchestration(); }});
12581306

1307+
// ─── Security Modal Functions ───
1308+
function showSecurityWarning() {
1309+
const key = document.getElementById('api-key-input').value.trim();
1310+
if (!key) {
1311+
alert('Please enter an API key first');
1312+
return;
1313+
}
1314+
if (!key.startsWith('sk-ant-')) {
1315+
alert('Please enter a valid Anthropic API key starting with sk-ant-');
1316+
return;
1317+
}
1318+
const modal = document.getElementById('security-modal');
1319+
modal.style.display = 'flex';
1320+
1321+
// Escape key handler — stored on modal so closeSecurityModal can remove it
1322+
modal._escHandler = function(e) {
1323+
if (e.key === 'Escape') closeSecurityModal();
1324+
};
1325+
document.addEventListener('keydown', modal._escHandler);
1326+
1327+
// Focus trap: collect focusable elements inside the modal
1328+
modal._trapHandler = function(e) {
1329+
if (e.key !== 'Tab') return;
1330+
const focusable = Array.from(modal.querySelectorAll(
1331+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
1332+
)).filter(el => !el.disabled);
1333+
if (!focusable.length) return;
1334+
const first = focusable[0];
1335+
const last = focusable[focusable.length - 1];
1336+
if (e.shiftKey) {
1337+
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
1338+
} else {
1339+
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
1340+
}
1341+
};
1342+
document.addEventListener('keydown', modal._trapHandler);
1343+
1344+
// Focus the Cancel button (first actionable control)
1345+
const cancelBtn = modal.querySelector('button');
1346+
if (cancelBtn) cancelBtn.focus();
1347+
}
1348+
1349+
function closeSecurityModal() {
1350+
const modal = document.getElementById('security-modal');
1351+
modal.style.display = 'none';
1352+
1353+
// Remove Escape and focus-trap handlers
1354+
if (modal._escHandler) {
1355+
document.removeEventListener('keydown', modal._escHandler);
1356+
modal._escHandler = null;
1357+
}
1358+
if (modal._trapHandler) {
1359+
document.removeEventListener('keydown', modal._trapHandler);
1360+
modal._trapHandler = null;
1361+
}
1362+
1363+
// Return focus to the API key input
1364+
const input = document.getElementById('api-key-input');
1365+
if (input) input.focus();
1366+
}
1367+
1368+
function confirmApiKeySubmission() {
1369+
closeSecurityModal();
1370+
saveApiKey();
1371+
}
1372+
12591373
async function saveApiKey() {
12601374
const key = document.getElementById('api-key-input').value.trim();
12611375
const btn = document.getElementById('btn-save-key');
@@ -1273,7 +1387,7 @@ <h1>StellarMind</h1>
12731387
});
12741388
const d = await res.json();
12751389
if (d.success) {
1276-
alert('API Key updated successfully! Agents will now use your key.');
1390+
alert('API Key updated successfully! Agents will now use your key.\n\n⚠️ Remember: Your key is stored in server memory only and will be cleared when the server restarts.');
12771391
document.getElementById('api-key-input').value = '';
12781392
loadStatusPage();
12791393
} else {

src/agents/services.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ export const MODEL_LABELS = {
2424
/**
2525
* Update the Anthropic API key at runtime.
2626
* Allows users to plug in their own key without restarting the server.
27+
* Security: Key is stored in memory only, never logged or persisted.
2728
*/
2829
export function setApiKey(newKey) {
2930
anthropic = new Anthropic({ apiKey: newKey });
3031
config.anthropicApiKey = newKey;
3132
claudeAvailable = true;
32-
console.log(' 🔑 Anthropic API key updated at runtime');
33+
// Security: Never log the actual key, only confirm it was set
34+
console.log(' 🔑 Anthropic API key updated at runtime (ephemeral, session-only)');
3335
}
3436

3537
/**

src/server.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ app.post('/api/config/apikey', (req, res, next) => {
323323
err.code = 'INVALID_API_KEY';
324324
return next(err);
325325
}
326+
// Security: Log only that a key was updated, never log the key itself
327+
console.log(' 🔑 API key updated (ephemeral, session-only)');
326328
setApiKey(apiKey);
327329
res.json({ success: true, masked: `sk-ant-...${apiKey.slice(-6)}` });
328330
});

0 commit comments

Comments
 (0)