/** * Secret Validation Framework * * Implements FIPS 140-2 Level 2+ secret validation per NIST SP 800-53 SC-12 * and NIST SP 800-171 3.5.10 (Cryptographic Key Management) * * This module ensures that: * - No default or insecure secrets are used in production * - Secrets meet minimum complexity requirements * - Secrets are properly validated before use */ import { logger } from './logger' /** * Default/insecure secrets that must never be used in production */ const INSECURE_SECRETS = [ 'your-secret-key-change-in-production', 'change-me', 'secret', 'password', 'admin', 'root', 'postgres', 'default', 'test', 'dev', 'development', '123456', 'password123', '', ] /** * Minimum secret requirements per DoD/MilSpec standards */ interface SecretRequirements { minLength: number requireUppercase: boolean requireLowercase: boolean requireNumbers: boolean requireSpecialChars: boolean maxAge?: number // in days } const DEFAULT_REQUIREMENTS: SecretRequirements = { minLength: 32, // NIST SP 800-63B recommends minimum 32 characters for secrets requireUppercase: true, requireLowercase: true, requireNumbers: true, requireSpecialChars: true, } /** * Validates that a secret meets DoD/MilSpec requirements */ export class SecretValidationError extends Error { constructor( message: string, public code: string, public requirements?: SecretRequirements ) { super(message) this.name = 'SecretValidationError' } } /** * Validates a secret against security requirements */ export function validateSecret( secret: string | undefined, name: string, requirements: Partial = {} ): void { const req = { ...DEFAULT_REQUIREMENTS, ...requirements } // Check if secret is provided if (!secret) { throw new SecretValidationError( `Secret '${name}' is required but not provided`, 'MISSING_SECRET', req ) } // Check for insecure defaults if (INSECURE_SECRETS.includes(secret.toLowerCase().trim())) { throw new SecretValidationError( `Secret '${name}' uses an insecure default value. This is not allowed in production.`, 'INSECURE_DEFAULT', req ) } // Check minimum length if (secret.length < req.minLength) { throw new SecretValidationError( `Secret '${name}' must be at least ${req.minLength} characters long (current: ${secret.length})`, 'INSUFFICIENT_LENGTH', req ) } // Check complexity requirements if (req.requireUppercase && !/[A-Z]/.test(secret)) { throw new SecretValidationError( `Secret '${name}' must contain at least one uppercase letter`, 'MISSING_UPPERCASE', req ) } if (req.requireLowercase && !/[a-z]/.test(secret)) { throw new SecretValidationError( `Secret '${name}' must contain at least one lowercase letter`, 'MISSING_LOWERCASE', req ) } if (req.requireNumbers && !/[0-9]/.test(secret)) { throw new SecretValidationError( `Secret '${name}' must contain at least one number`, 'MISSING_NUMBER', req ) } if (req.requireSpecialChars && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(secret)) { throw new SecretValidationError( `Secret '${name}' must contain at least one special character`, 'MISSING_SPECIAL_CHAR', req ) } // Check for common patterns (optional but recommended) if (isCommonPattern(secret)) { logger.warn(`Secret '${name}' matches a common pattern and may be insecure`) } } /** * Checks if a secret matches common insecure patterns */ function isCommonPattern(secret: string): boolean { const patterns = [ /^[a-z]+$/i, // All same case /^[0-9]+$/, // All numbers /^(.)\1+$/, // All same character /^12345/, // Sequential numbers /^abcde/i, // Sequential letters ] return patterns.some(pattern => pattern.test(secret)) } /** * Validates a secret and returns it, or throws if invalid * This is the main function to use for secret validation */ export function requireSecret( secret: string | undefined, name: string, requirements?: Partial ): string { validateSecret(secret, name, requirements) return secret! } /** * Validates a secret in production environment * Fails fast if secret is insecure in production */ export function requireProductionSecret( secret: string | undefined, name: string, requirements?: Partial ): string { const isProduction = process.env.NODE_ENV === 'production' || process.env.ENVIRONMENT === 'production' || process.env.PRODUCTION === 'true' if (isProduction) { // Stricter requirements for production const prodRequirements: SecretRequirements = { ...DEFAULT_REQUIREMENTS, minLength: 64, // Longer secrets for production ...requirements, } validateSecret(secret, name, prodRequirements) } else { validateSecret(secret, name, requirements) } return secret! } /** * Validates JWT secret specifically */ export function requireJWTSecret(): string { return requireProductionSecret( process.env.JWT_SECRET, 'JWT_SECRET', { minLength: 64, // JWT secrets should be longer } ) } /** * Validates database password specifically * Relaxed requirements for development mode */ export function requireDatabasePassword(): string { const isProduction = process.env.NODE_ENV === 'production' || process.env.ENVIRONMENT === 'production' || process.env.PRODUCTION === 'true' if (isProduction) { return requireProductionSecret( process.env.DB_PASSWORD, 'DB_PASSWORD', { minLength: 32, } ) } else { // Development mode: relaxed requirements // Still validate but allow shorter passwords for local development const password = process.env.DB_PASSWORD if (!password) { throw new SecretValidationError( 'DB_PASSWORD is required but not provided. Please set it in your .env file.', 'MISSING_SECRET', { minLength: 8, requireUppercase: false, requireLowercase: false, requireNumbers: false, requireSpecialChars: false } ) } // Basic validation for dev (just check it's not empty and not insecure) if (password.length < 8) { throw new SecretValidationError( 'DB_PASSWORD must be at least 8 characters long for development', 'INSUFFICIENT_LENGTH', { minLength: 8 } ) } if (INSECURE_SECRETS.includes(password.toLowerCase().trim())) { throw new SecretValidationError( 'DB_PASSWORD uses an insecure default value', 'INSECURE_DEFAULT' ) } return password } } /** * Validates all required secrets at application startup * Call this during application initialization */ export function validateAllSecrets(): void { const isProduction = process.env.NODE_ENV === 'production' || process.env.ENVIRONMENT === 'production' || process.env.PRODUCTION === 'true' if (!isProduction) { logger.warn('Not in production environment - secret validation may be relaxed') return } logger.info('Validating all required secrets for production...') const requiredSecrets = [ { env: 'JWT_SECRET', name: 'JWT_SECRET', minLength: 64 }, { env: 'DB_PASSWORD', name: 'DB_PASSWORD', minLength: 32 }, { env: 'KEYCLOAK_CLIENT_SECRET', name: 'KEYCLOAK_CLIENT_SECRET', minLength: 32 }, ] const missing: string[] = [] const invalid: Array<{ name: string; error: string }> = [] for (const secret of requiredSecrets) { const value = process.env[secret.env] if (!value) { missing.push(secret.name) continue } try { requireProductionSecret(value, secret.name, { minLength: secret.minLength }) } catch (error) { if (error instanceof SecretValidationError) { invalid.push({ name: secret.name, error: error.message }) } else { invalid.push({ name: secret.name, error: String(error) }) } } } if (missing.length > 0) { throw new Error( `Missing required secrets in production: ${missing.join(', ')}\n` + 'Please set all required environment variables before starting the application.' ) } if (invalid.length > 0) { const errors = invalid.map(i => ` - ${i.name}: ${i.error}`).join('\n') throw new Error( `Invalid secrets in production:\n${errors}\n` + 'Please ensure all secrets meet security requirements.' ) } logger.info('All required secrets validated successfully') }