feat(security): implement AES-256-GCM encryption for API keys and HMAC-signed session tokens

This commit introduces:
- AES-256-GCM encryption for LLM provider API keys in the database.
- HMAC-SHA256 signed session tokens with activity-based refresh logic.
- Standardized frontend XSS protection using a global escapeHtml utility.
- Hardened security headers and request body size limits.
- Improved database integrity with foreign key enforcement and atomic transactions.
- Integration tests for the full encrypted key storage and proxy usage lifecycle.
This commit is contained in:
2026-03-06 14:17:56 -05:00
parent 149a7c3a29
commit 9b8483e797
28 changed files with 1260 additions and 227 deletions

View File

@@ -42,12 +42,15 @@ class ClientsPage {
const statusIcon = client.status === 'active' ? 'check-circle' : 'clock';
const created = luxon.DateTime.fromISO(client.created_at).toFormat('MMM dd, yyyy');
const escapedId = window.api.escapeHtml(client.id);
const escapedName = window.api.escapeHtml(client.name);
return `
<tr>
<td><span class="badge-client">${client.id}</span></td>
<td><strong>${client.name}</strong></td>
<td><span class="badge-client">${escapedId}</span></td>
<td><strong>${escapedName}</strong></td>
<td>
<code class="token-display">sk-••••${client.id.substring(client.id.length - 4)}</code>
<code class="token-display">sk-••••${escapedId.substring(escapedId.length - 4)}</code>
</td>
<td>${created}</td>
<td>${client.last_used ? window.api.formatTimeAgo(client.last_used) : 'Never'}</td>
@@ -55,16 +58,16 @@ class ClientsPage {
<td>
<span class="status-badge ${statusClass}">
<i class="fas fa-${statusIcon}"></i>
${client.status}
${window.api.escapeHtml(client.status)}
</span>
</td>
<td>
${window._userRole === 'admin' ? `
<div class="action-buttons">
<button class="btn-action" title="Edit" onclick="window.clientsPage.editClient('${client.id}')">
<button class="btn-action" title="Edit" onclick="window.clientsPage.editClient('${escapedId}')">
<i class="fas fa-edit"></i>
</button>
<button class="btn-action danger" title="Delete" onclick="window.clientsPage.deleteClient('${client.id}')">
<button class="btn-action danger" title="Delete" onclick="window.clientsPage.deleteClient('${escapedId}')">
<i class="fas fa-trash"></i>
</button>
</div>
@@ -188,10 +191,13 @@ class ClientsPage {
showTokenRevealModal(clientName, token) {
const modal = document.createElement('div');
modal.className = 'modal active';
const escapedName = window.api.escapeHtml(clientName);
const escapedToken = window.api.escapeHtml(token);
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Client Created: ${clientName}</h3>
<h3 class="modal-title">Client Created: ${escapedName}</h3>
</div>
<div class="modal-body">
<p style="margin-bottom: 0.75rem; color: var(--yellow);">
@@ -201,7 +207,7 @@ class ClientsPage {
<div class="form-control">
<label>API Token</label>
<div style="display: flex; gap: 0.5rem;">
<input type="text" id="revealed-token" value="${token}" readonly
<input type="text" id="revealed-token" value="${escapedToken}" readonly
style="font-family: monospace; font-size: 0.85rem;">
<button class="btn btn-secondary" id="copy-token-btn" title="Copy">
<i class="fas fa-copy"></i>
@@ -248,10 +254,16 @@ class ClientsPage {
showEditClientModal(client) {
const modal = document.createElement('div');
modal.className = 'modal active';
const escapedId = window.api.escapeHtml(client.id);
const escapedName = window.api.escapeHtml(client.name);
const escapedDescription = window.api.escapeHtml(client.description);
const escapedRateLimit = window.api.escapeHtml(client.rate_limit_per_minute);
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Edit Client: ${client.id}</h3>
<h3 class="modal-title">Edit Client: ${escapedId}</h3>
<button class="modal-close" onclick="this.closest('.modal').remove()">
<i class="fas fa-times"></i>
</button>
@@ -259,15 +271,15 @@ class ClientsPage {
<div class="modal-body">
<div class="form-control">
<label for="edit-client-name">Display Name</label>
<input type="text" id="edit-client-name" value="${client.name || ''}" placeholder="e.g. My Coding Assistant">
<input type="text" id="edit-client-name" value="${escapedName}" placeholder="e.g. My Coding Assistant">
</div>
<div class="form-control">
<label for="edit-client-description">Description</label>
<textarea id="edit-client-description" rows="3" placeholder="Optional description">${client.description || ''}</textarea>
<textarea id="edit-client-description" rows="3" placeholder="Optional description">${escapedDescription}</textarea>
</div>
<div class="form-control">
<label for="edit-client-rate-limit">Rate Limit (requests/minute)</label>
<input type="number" id="edit-client-rate-limit" min="0" value="${client.rate_limit_per_minute || ''}" placeholder="Leave empty for unlimited">
<input type="number" id="edit-client-rate-limit" min="0" value="${escapedRateLimit}" placeholder="Leave empty for unlimited">
</div>
<div class="form-control">
<label class="toggle-label">
@@ -357,12 +369,16 @@ class ClientsPage {
const lastUsed = t.last_used_at
? luxon.DateTime.fromISO(t.last_used_at).toRelative()
: 'Never';
const escapedMaskedToken = window.api.escapeHtml(t.token_masked);
const escapedClientId = window.api.escapeHtml(clientId);
const tokenId = parseInt(t.id); // Assuming ID is numeric
return `
<div style="display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0; border-bottom: 1px solid var(--bg3);">
<code style="flex: 1; font-size: 0.8rem; color: var(--fg2);">${t.token_masked}</code>
<code style="flex: 1; font-size: 0.8rem; color: var(--fg2);">${escapedMaskedToken}</code>
<span style="font-size: 0.75rem; color: var(--fg4);" title="Last used">${lastUsed}</span>
<button class="btn-action danger" title="Revoke" style="padding: 0.2rem 0.4rem;"
onclick="window.clientsPage.revokeToken('${clientId}', ${t.id}, this)">
onclick="window.clientsPage.revokeToken('${escapedClientId}', ${tokenId}, this)">
<i class="fas fa-trash" style="font-size: 0.75rem;"></i>
</button>
</div>