Enhance security: Rotate VAPID keys, encrypt LLM API keys, and use env vars

This commit is contained in:
Gemini Agent
2026-01-25 05:13:18 +00:00
parent 3ca83f304f
commit ca0569ab0d
5 changed files with 101 additions and 7 deletions

View File

@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { getSession } from "@/lib/auth";
import { eq, and, gte, lte, desc } from "drizzle-orm";
import { decrypt } from "@/lib/crypto";
// POST /api/reflections - Generate an LLM reflection for a time period
export async function POST(request: NextRequest) {
@@ -31,7 +32,8 @@ export async function POST(request: NextRequest) {
);
}
const { llmProvider, llmApiKey, llmModel } = users[0];
const { llmProvider, llmModel } = users[0];
const llmApiKey = decrypt(users[0].llmApiKey);
// Calculate date range
const now = new Date();

View File

@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db";
import { getSession } from "@/lib/auth";
import { eq } from "drizzle-orm";
import { encrypt } from "@/lib/crypto";
// GET /api/settings - Get current user settings
export async function GET() {
@@ -70,7 +71,8 @@ export async function PATCH(request: NextRequest) {
}
if (typeof body.llmApiKey === "string") {
updates.llmApiKey = body.llmApiKey || null;
// Encrypt the API key before storing
updates.llmApiKey = body.llmApiKey ? encrypt(body.llmApiKey) : null;
}
if (typeof body.llmModel === "string") {

55
src/lib/crypto.ts Normal file
View File

@@ -0,0 +1,55 @@
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '';
const ALGORITHM = 'aes-256-gcm';
export function encrypt(text: string): string {
if (!ENCRYPTION_KEY) {
console.warn('ENCRYPTION_KEY not set, storing in plain text');
return text;
}
try {
const iv = randomBytes(16);
const key = Buffer.from(ENCRYPTION_KEY, 'hex');
const cipher = createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
// Format: iv:authTag:encrypted
return `${iv.toString('hex')}:${authTag}:${encrypted}`;
} catch (error) {
console.error('Encryption failed:', error);
return text;
}
}
export function decrypt(text: string): string {
if (!ENCRYPTION_KEY || !text) return text;
const parts = text.split(':');
// If not in encrypted format (legacy plain text), return as is
if (parts.length !== 3) return text;
try {
const [ivHex, authTagHex, encryptedHex] = parts;
const key = Buffer.from(ENCRYPTION_KEY, 'hex');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
// If decryption fails, assume it might be plain text or corrupted
console.warn('Decryption failed, returning original text');
return text;
}
}