dev-1.7.0 #294
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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<string> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<string> {
|
||||
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 };
|
||||
|
||||
@@ -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<void> {
|
||||
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<boolean> {
|
||||
console.log("🔒 Validating production encryption setup...\n");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user