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");