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

@@ -47,16 +47,21 @@ class ProvidersPage {
const isLowBalance = provider.credit_balance <= provider.low_credit_threshold && provider.id !== 'ollama';
const balanceColor = isLowBalance ? 'var(--red-light)' : 'var(--green-light)';
const escapedId = window.api.escapeHtml(provider.id);
const escapedName = window.api.escapeHtml(provider.name);
const escapedStatus = window.api.escapeHtml(provider.status);
const billingMode = provider.billing_mode ? provider.billing_mode.toUpperCase() : 'PREPAID';
return `
<div class="provider-card ${provider.status}">
<div class="provider-card ${escapedStatus}">
<div class="provider-card-header">
<div class="provider-info">
<h4 class="provider-name">${provider.name}</h4>
<span class="provider-id">${provider.id}</span>
<h4 class="provider-name">${escapedName}</h4>
<span class="provider-id">${escapedId}</span>
</div>
<span class="status-badge ${statusClass}">
<i class="fas fa-circle"></i>
${provider.status}
${escapedStatus}
</span>
</div>
<div class="provider-card-body">
@@ -67,12 +72,12 @@ class ProvidersPage {
</div>
<div class="meta-item" style="color: ${balanceColor}; font-weight: 700;">
<i class="fas fa-wallet"></i>
<span>Balance: ${provider.id === 'ollama' ? 'FREE' : window.api.formatCurrency(provider.credit_balance)}</span>
<span>Balance: ${escapedId === 'ollama' ? 'FREE' : window.api.formatCurrency(provider.credit_balance)}</span>
${isLowBalance ? '<i class="fas fa-exclamation-triangle" title="Low Balance"></i>' : ''}
</div>
<div class="meta-item">
<i class="fas fa-exchange-alt"></i>
<span>Billing: ${provider.billing_mode ? provider.billing_mode.toUpperCase() : 'PREPAID'}</span>
<span>Billing: ${window.api.escapeHtml(billingMode)}</span>
</div>
<div class="meta-item">
<i class="fas fa-clock"></i>
@@ -80,16 +85,16 @@ class ProvidersPage {
</div>
</div>
<div class="model-tags">
${(provider.models || []).slice(0, 5).map(m => `<span class="model-tag">${m}</span>`).join('')}
${(provider.models || []).slice(0, 5).map(m => `<span class="model-tag">${window.api.escapeHtml(m)}</span>`).join('')}
${modelCount > 5 ? `<span class="model-tag more">+${modelCount - 5} more</span>` : ''}
</div>
</div>
<div class="provider-card-footer">
<button class="btn btn-secondary btn-sm" onclick="window.providersPage.testProvider('${provider.id}')">
<button class="btn btn-secondary btn-sm" onclick="window.providersPage.testProvider('${escapedId}')">
<i class="fas fa-vial"></i> Test
</button>
${window._userRole === 'admin' ? `
<button class="btn btn-primary btn-sm" onclick="window.providersPage.configureProvider('${provider.id}')">
<button class="btn btn-primary btn-sm" onclick="window.providersPage.configureProvider('${escapedId}')">
<i class="fas fa-cog"></i> Config
</button>
` : ''}
@@ -144,10 +149,17 @@ class ProvidersPage {
const modal = document.createElement('div');
modal.className = 'modal active';
const escapedId = window.api.escapeHtml(provider.id);
const escapedName = window.api.escapeHtml(provider.name);
const escapedBaseUrl = window.api.escapeHtml(provider.base_url);
const escapedBalance = window.api.escapeHtml(provider.credit_balance);
const escapedThreshold = window.api.escapeHtml(provider.low_credit_threshold);
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Configure ${provider.name}</h3>
<h3 class="modal-title">Configure ${escapedName}</h3>
<button class="modal-close" onclick="this.closest('.modal').remove()">
<i class="fas fa-times"></i>
</button>
@@ -161,7 +173,7 @@ class ProvidersPage {
</div>
<div class="form-control">
<label for="provider-base-url">Base URL</label>
<input type="text" id="provider-base-url" value="${provider.base_url || ''}" placeholder="Default API URL">
<input type="text" id="provider-base-url" value="${escapedBaseUrl}" placeholder="Default API URL">
</div>
<div class="form-control">
<label for="provider-api-key">API Key (Optional / Overwrite)</label>
@@ -170,11 +182,11 @@ class ProvidersPage {
<div class="grid-2">
<div class="form-control">
<label for="provider-balance">Current Credit Balance ($)</label>
<input type="number" id="provider-balance" value="${provider.credit_balance}" step="0.01">
<input type="number" id="provider-balance" value="${escapedBalance}" step="0.01">
</div>
<div class="form-control">
<label for="provider-threshold">Low Balance Alert ($)</label>
<input type="number" id="provider-threshold" value="${provider.low_credit_threshold}" step="0.50">
<input type="number" id="provider-threshold" value="${escapedThreshold}" step="0.50">
</div>
</div>
<div class="form-control">