From 5ccb52071d7d2503fea69ea0db3530030910cbce Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Sun, 21 Sep 2025 03:00:59 +0800 Subject: [PATCH] Eliminate JWT security vulnerability with unified encryption architecture SECURITY FIX: Replace dangerous JWT_SECRET environment variable with encrypted database storage using hardware-bound KEK protection. Changes: - EncryptionKeyManager: Add JWT secret management with AES-256-GCM encryption - All route files: Eliminate process.env.JWT_SECRET dependencies - Database server: Initialize JWT secret during startup with proper error handling - Testing: Add comprehensive JWT secret management test coverage - API: Add /encryption/regenerate-jwt endpoint for key rotation Technical implementation: - JWT secrets now use same protection as SSH keys (hardware fingerprint binding) - 512-bit JWT secrets generated via crypto.randomBytes(64) - KEK-protected storage prevents cross-device secret migration - No backward compatibility for insecure environment variable approach This eliminates the critical security flaw where JWT tokens could be forged using the default "secret" value, achieving uniform security architecture with no special cases. Co-Authored-By: Claude --- src/backend/database/database.ts | 33 ++++ src/backend/database/routes/credentials.ts | 8 +- src/backend/database/routes/ssh.ts | 8 +- src/backend/database/routes/users.ts | 22 ++- src/backend/utils/encryption-key-manager.ts | 166 ++++++++++++++++++++ src/backend/utils/encryption-test.ts | 36 +++++ 6 files changed, 263 insertions(+), 10 deletions(-) diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 5c61009d..4eb93817 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -398,6 +398,29 @@ app.post("/encryption/regenerate", async (req, res) => { } }); +app.post("/encryption/regenerate-jwt", async (req, res) => { + try { + const { EncryptionKeyManager } = await import("../utils/encryption-key-manager.js"); + const keyManager = EncryptionKeyManager.getInstance(); + await keyManager.regenerateJWTSecret(); + + apiLogger.warn("JWT secret regenerated via API", { + operation: "jwt_secret_regenerate_api", + }); + + res.json({ + success: true, + message: "New JWT secret generated", + warning: "All existing JWT tokens are now invalid - users must re-authenticate", + }); + } catch (error) { + apiLogger.error("Failed to regenerate JWT secret", error, { + operation: "jwt_secret_regenerate_failed", + }); + res.status(500).json({ error: "Failed to regenerate JWT secret" }); + } +}); + // Database migration and backup endpoints app.post("/database/export", async (req, res) => { try { @@ -689,10 +712,20 @@ async function initializeEncryption() { }, ); } + + // Initialize JWT secret using the same encryption infrastructure + const { EncryptionKeyManager } = await import("../utils/encryption-key-manager.js"); + const keyManager = EncryptionKeyManager.getInstance(); + await keyManager.getJWTSecret(); + + databaseLogger.success("JWT secret initialized successfully", { + operation: "jwt_secret_init_complete", + }); } catch (error) { databaseLogger.error("Failed to initialize database encryption", error, { operation: "encryption_init_error", }); + throw error; // JWT secret is critical for API functionality } } diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts index a5cb14f4..493daa62 100644 --- a/src/backend/database/routes/credentials.ts +++ b/src/backend/database/routes/credentials.ts @@ -84,7 +84,7 @@ function isNonEmptyString(val: any): val is string { return typeof val === "string" && val.trim().length > 0; } -function authenticateJWT(req: Request, res: Response, next: NextFunction) { +async function authenticateJWT(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers["authorization"]; if (!authHeader || !authHeader.startsWith("Bearer ")) { authLogger.warn("Missing or invalid Authorization header"); @@ -93,8 +93,12 @@ function authenticateJWT(req: Request, res: Response, next: NextFunction) { .json({ error: "Missing or invalid Authorization header" }); } const token = authHeader.split(" ")[1]; - const jwtSecret = process.env.JWT_SECRET || "secret"; + try { + const { EncryptionKeyManager } = await import("../../utils/encryption-key-manager.js"); + const keyManager = EncryptionKeyManager.getInstance(); + const jwtSecret = await keyManager.getJWTSecret(); + const payload = jwt.verify(token, jwtSecret) as JWTPayload; (req as any).userId = payload.userId; next(); diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index dfe9643b..11e68421 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -31,7 +31,7 @@ function isValidPort(port: any): port is number { return typeof port === "number" && port > 0 && port <= 65535; } -function authenticateJWT(req: Request, res: Response, next: NextFunction) { +async function authenticateJWT(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { sshLogger.warn("Missing or invalid Authorization header"); @@ -40,8 +40,12 @@ function authenticateJWT(req: Request, res: Response, next: NextFunction) { .json({ error: "Missing or invalid Authorization header" }); } const token = authHeader.split(" ")[1]; - const jwtSecret = process.env.JWT_SECRET || "secret"; + try { + const { EncryptionKeyManager } = await import("../../utils/encryption-key-manager.js"); + const keyManager = EncryptionKeyManager.getInstance(); + const jwtSecret = await keyManager.getJWTSecret(); + const payload = jwt.verify(token, jwtSecret) as JWTPayload; (req as any).userId = payload.userId; next(); diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index fe4a7a10..60964a8c 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -130,7 +130,7 @@ interface JWTPayload { } // JWT authentication middleware -function authenticateJWT(req: Request, res: Response, next: NextFunction) { +async function authenticateJWT(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers["authorization"]; if (!authHeader || !authHeader.startsWith("Bearer ")) { authLogger.warn("Missing or invalid Authorization header", { @@ -143,8 +143,12 @@ function authenticateJWT(req: Request, res: Response, next: NextFunction) { .json({ error: "Missing or invalid Authorization header" }); } const token = authHeader.split(" ")[1]; - const jwtSecret = process.env.JWT_SECRET || "secret"; + try { + const { EncryptionKeyManager } = await import("../../utils/encryption-key-manager.js"); + const keyManager = EncryptionKeyManager.getInstance(); + const jwtSecret = await keyManager.getJWTSecret(); + const payload = jwt.verify(token, jwtSecret) as JWTPayload; (req as any).userId = payload.userId; next(); @@ -693,7 +697,9 @@ router.get("/oidc/callback", async (req, res) => { const userRecord = user[0]; - const jwtSecret = process.env.JWT_SECRET || "secret"; + const { EncryptionKeyManager } = await import("../../utils/encryption-key-manager.js"); + const keyManager = EncryptionKeyManager.getInstance(); + const jwtSecret = await keyManager.getJWTSecret(); const token = jwt.sign({ userId: userRecord.id }, jwtSecret, { expiresIn: "50d", }); @@ -775,7 +781,9 @@ router.post("/login", async (req, res) => { }); return res.status(401).json({ error: "Incorrect password" }); } - const jwtSecret = process.env.JWT_SECRET || "secret"; + const { EncryptionKeyManager } = await import("../../utils/encryption-key-manager.js"); + const keyManager = EncryptionKeyManager.getInstance(); + const jwtSecret = await keyManager.getJWTSecret(); const token = jwt.sign({ userId: userRecord.id }, jwtSecret, { expiresIn: "50d", }); @@ -1245,9 +1253,11 @@ router.post("/totp/verify-login", async (req, res) => { return res.status(400).json({ error: "Token and TOTP code are required" }); } - const jwtSecret = process.env.JWT_SECRET || "secret"; - try { + const { EncryptionKeyManager } = await import("../../utils/encryption-key-manager.js"); + const keyManager = EncryptionKeyManager.getInstance(); + const jwtSecret = await keyManager.getJWTSecret(); + const decoded = jwt.verify(temp_token, jwtSecret) as any; if (!decoded.pending_totp) { return res.status(401).json({ error: "Invalid temporary token" }); diff --git a/src/backend/utils/encryption-key-manager.ts b/src/backend/utils/encryption-key-manager.ts index be678af5..c3d802f7 100644 --- a/src/backend/utils/encryption-key-manager.ts +++ b/src/backend/utils/encryption-key-manager.ts @@ -16,6 +16,7 @@ class EncryptionKeyManager { private static instance: EncryptionKeyManager; private currentKey: string | null = null; private keyInfo: EncryptionKeyInfo | null = null; + private jwtSecret: string | null = null; private constructor() {} @@ -347,6 +348,171 @@ class EncryptionKeyManager { return false; } } + + async getJWTSecret(): Promise { + if (this.jwtSecret) { + return this.jwtSecret; + } + + try { + let existingSecret = await this.getStoredJWTSecret(); + + if (existingSecret) { + databaseLogger.success("Found existing JWT secret", { + operation: "jwt_secret_init", + hasSecret: true, + }); + this.jwtSecret = existingSecret; + return existingSecret; + } + + const newSecret = await this.generateJWTSecret(); + databaseLogger.success("Generated new JWT secret", { + operation: "jwt_secret_generated", + secretLength: newSecret.length, + }); + + return newSecret; + } catch (error) { + databaseLogger.error("Failed to initialize JWT secret", error, { + operation: "jwt_secret_init_failed", + }); + throw new Error("JWT secret initialization failed - cannot start server"); + } + } + + private async generateJWTSecret(): Promise { + const newSecret = crypto.randomBytes(64).toString("hex"); + const secretId = crypto.randomBytes(8).toString("hex"); + + await this.storeJWTSecret(newSecret, secretId); + this.jwtSecret = newSecret; + + databaseLogger.success("Generated secure JWT secret", { + operation: "jwt_secret_generated", + secretId, + secretLength: newSecret.length, + }); + + return newSecret; + } + + private async storeJWTSecret(secret: string, secretId?: string): Promise { + const now = new Date().toISOString(); + const id = secretId || crypto.randomBytes(8).toString("hex"); + + const secretData = { + secret: this.encodeKey(secret), + secretId: id, + createdAt: now, + algorithm: "aes-256-gcm", + }; + + const encodedData = JSON.stringify(secretData); + + try { + const existing = await db + .select() + .from(settings) + .where(eq(settings.key, "jwt_secret")); + + if (existing.length > 0) { + await db + .update(settings) + .set({ value: encodedData }) + .where(eq(settings.key, "jwt_secret")); + } else { + await db.insert(settings).values({ + key: "jwt_secret", + value: encodedData, + }); + } + + const existingCreated = await db + .select() + .from(settings) + .where(eq(settings.key, "jwt_secret_created")); + + if (existingCreated.length > 0) { + await db + .update(settings) + .set({ value: now }) + .where(eq(settings.key, "jwt_secret_created")); + } else { + await db.insert(settings).values({ + key: "jwt_secret_created", + value: now, + }); + } + + databaseLogger.success("JWT secret stored securely", { + operation: "jwt_secret_stored", + secretId: id, + }); + } catch (error) { + databaseLogger.error("Failed to store JWT secret", error, { + operation: "jwt_secret_store_failed", + }); + throw error; + } + } + + private async getStoredJWTSecret(): Promise { + try { + const result = await db + .select() + .from(settings) + .where(eq(settings.key, "jwt_secret")); + + if (result.length === 0) { + return null; + } + + const encodedData = result[0].value; + let secretData; + + try { + secretData = JSON.parse(encodedData); + } catch { + databaseLogger.warn("Found legacy JWT secret data, migrating", { + operation: "jwt_secret_migration_legacy", + }); + return null; + } + + const decodedSecret = this.decodeKey(secretData.secret); + + if (!MasterKeyProtection.isProtectedKey(secretData.secret)) { + databaseLogger.info("Auto-migrating legacy JWT secret to KEK protection", { + operation: "jwt_secret_auto_migration", + secretId: secretData.secretId, + }); + await this.storeJWTSecret(decodedSecret, secretData.secretId); + } + + return decodedSecret; + } catch (error) { + databaseLogger.error("Failed to retrieve stored JWT secret", error, { + operation: "jwt_secret_retrieve_failed", + }); + return null; + } + } + + async regenerateJWTSecret(): Promise { + databaseLogger.warn("Regenerating JWT secret - ALL ACTIVE TOKENS WILL BE INVALIDATED", { + operation: "jwt_secret_regenerate", + }); + + const newSecret = await this.generateJWTSecret(); + + databaseLogger.success("JWT secret regenerated successfully", { + operation: "jwt_secret_regenerated", + warning: "All existing JWT tokens are now invalid", + }); + + return newSecret; + } } export { EncryptionKeyManager }; diff --git a/src/backend/utils/encryption-test.ts b/src/backend/utils/encryption-test.ts index e4368b0e..bdfbec94 100644 --- a/src/backend/utils/encryption-test.ts +++ b/src/backend/utils/encryption-test.ts @@ -34,6 +34,7 @@ class EncryptionTest { }, { name: "Error Handling", test: () => this.testErrorHandling() }, { name: "Performance Test", test: () => this.testPerformance() }, + { name: "JWT Secret Management", test: () => this.testJWTSecretManagement() }, ]; let passedTests = 0; @@ -267,6 +268,41 @@ class EncryptionTest { } } + private async testJWTSecretManagement(): Promise { + const { EncryptionKeyManager } = await import("./encryption-key-manager.js"); + const keyManager = EncryptionKeyManager.getInstance(); + + // Test JWT secret generation and retrieval + const jwtSecret1 = await keyManager.getJWTSecret(); + if (!jwtSecret1 || jwtSecret1.length < 32) { + throw new Error("JWT secret should be at least 32 characters long"); + } + + // Test that subsequent calls return the same secret (caching) + const jwtSecret2 = await keyManager.getJWTSecret(); + if (jwtSecret1 !== jwtSecret2) { + throw new Error("JWT secret should be cached and consistent"); + } + + // Test JWT secret regeneration + const newJwtSecret = await keyManager.regenerateJWTSecret(); + if (newJwtSecret === jwtSecret1) { + throw new Error("Regenerated JWT secret should be different from original"); + } + + if (newJwtSecret.length !== 128) { // 64 bytes * 2 (hex encoding) + throw new Error(`JWT secret should be 128 hex characters (64 bytes), got ${newJwtSecret.length}`); + } + + // Test that after regeneration, getJWTSecret returns the new secret + const currentSecret = await keyManager.getJWTSecret(); + if (currentSecret !== newJwtSecret) { + throw new Error("getJWTSecret should return the new secret after regeneration"); + } + + console.log(" ✅ JWT secret generation, caching, and regeneration working correctly"); + } + static async validateProduction(): Promise { console.log("🔒 Validating production encryption setup...\n");