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

@@ -0,0 +1,34 @@
services:
quietthanks:
build: .
container_name: quietthanks
restart: unless-stopped
ports:
- "6124:3000"
volumes:
- ./data:/app/data
environment:
- DATABASE_PATH=/app/data/quietthanks.db
- NEXT_PUBLIC_VAPID_PUBLIC_KEY=${NEXT_PUBLIC_VAPID_PUBLIC_KEY}
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
- VAPID_EMAIL=${VAPID_EMAIL}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- TZ=${TZ}
scheduler:
image: alpine
restart: unless-stopped
depends_on:
quietthanks:
condition: service_started
environment:
- TZ=${TZ}
entrypoint: /bin/sh
command: >
-c "apk add --no-cache curl &&
while true; do
echo 'Checking for notifications...' &&
curl -s -X POST http://quietthanks:3000/api/notifications/send &&
echo '' &&
sleep 60;
done"

View File

@@ -9,10 +9,11 @@ services:
- ./data:/app/data - ./data:/app/data
environment: environment:
- DATABASE_PATH=/app/data/quietthanks.db - DATABASE_PATH=/app/data/quietthanks.db
- NEXT_PUBLIC_VAPID_PUBLIC_KEY=BIKukAq5-KPwJAMpksxD7UNL8XfF-oJOI0CLGGZQAY93igZgf1PYa9MVvS8GaBv-vv9ckcXPCEKdzWDCtOyQpKg - NEXT_PUBLIC_VAPID_PUBLIC_KEY=${NEXT_PUBLIC_VAPID_PUBLIC_KEY}
- VAPID_PRIVATE_KEY=IBkQ14BLKFCg2PmGOWheC7xfYHS5J49vXS8duHCeDBw - VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
- VAPID_EMAIL=mailto:admin@example.com - VAPID_EMAIL=${VAPID_EMAIL}
- TZ=Australia/Perth - ENCRYPTION_KEY=${ENCRYPTION_KEY}
- TZ=${TZ}
scheduler: scheduler:
image: alpine image: alpine
@@ -21,7 +22,7 @@ services:
quietthanks: quietthanks:
condition: service_started condition: service_started
environment: environment:
- TZ=Australia/Perth - TZ=${TZ}
entrypoint: /bin/sh entrypoint: /bin/sh
command: > command: >
-c "apk add --no-cache curl && -c "apk add --no-cache curl &&

View File

@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db"; import { db, schema } from "@/lib/db";
import { getSession } from "@/lib/auth"; import { getSession } from "@/lib/auth";
import { eq, and, gte, lte, desc } from "drizzle-orm"; 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 // POST /api/reflections - Generate an LLM reflection for a time period
export async function POST(request: NextRequest) { 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 // Calculate date range
const now = new Date(); const now = new Date();

View File

@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { db, schema } from "@/lib/db"; import { db, schema } from "@/lib/db";
import { getSession } from "@/lib/auth"; import { getSession } from "@/lib/auth";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { encrypt } from "@/lib/crypto";
// GET /api/settings - Get current user settings // GET /api/settings - Get current user settings
export async function GET() { export async function GET() {
@@ -70,7 +71,8 @@ export async function PATCH(request: NextRequest) {
} }
if (typeof body.llmApiKey === "string") { 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") { 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;
}
}