UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

278 lines (242 loc) 7.56 kB
/** * Password Hasher - CVE-2 Remediation * * Fixes weak password hashing by replacing SHA-256 with hardcoded salt * with bcrypt using 12 rounds (configurable). * * Security Properties: * - bcrypt with adaptive cost factor (12 rounds) * - Automatic salt generation per password * - Timing-safe comparison * - Minimum password length enforcement * * @module v3/security/password-hasher */ // #1608 — switched from `bcrypt` to `bcryptjs` to drop the // `@mapbox/node-pre-gyp → tar <=7.5.10` transitive chain that pulled in // 6 HIGH-severity CVEs (GHSA-34x7-hfp2-rc4v, GHSA-8qq5-rm4j-mr97, etc.). // `bcryptjs` is a pure-JS implementation that produces the same `$2a$` / // `$2b$` hash format and exposes the same `hash()` / `compare()` API // surface this module uses, so the swap is transparent to callers and to // any persisted hashes. import * as bcrypt from 'bcryptjs'; export interface PasswordHasherConfig { /** * Number of bcrypt rounds (cost factor). * Default: 12 (recommended minimum for production) * Each increment doubles the computation time. */ rounds?: number; /** * Minimum password length. * Default: 8 characters */ minLength?: number; /** * Maximum password length. * Default: 128 characters (bcrypt limit is 72 bytes) */ maxLength?: number; /** * Require at least one uppercase letter. * Default: true */ requireUppercase?: boolean; /** * Require at least one lowercase letter. * Default: true */ requireLowercase?: boolean; /** * Require at least one digit. * Default: true */ requireDigit?: boolean; /** * Require at least one special character. * Default: false */ requireSpecial?: boolean; } export interface PasswordValidationResult { isValid: boolean; errors: string[]; } export class PasswordHashError extends Error { constructor( message: string, public readonly code: string, ) { super(message); this.name = 'PasswordHashError'; } } /** * Secure password hasher using bcrypt. * * This class replaces the vulnerable SHA-256 + hardcoded salt implementation * with industry-standard bcrypt hashing. * * @example * ```typescript * const hasher = new PasswordHasher({ rounds: 12 }); * const hash = await hasher.hash('securePassword123'); * const isValid = await hasher.verify('securePassword123', hash); * ``` */ export class PasswordHasher { private readonly config: Required<PasswordHasherConfig>; constructor(config: PasswordHasherConfig = {}) { this.config = { rounds: config.rounds ?? 12, minLength: config.minLength ?? 8, maxLength: config.maxLength ?? 128, requireUppercase: config.requireUppercase ?? true, requireLowercase: config.requireLowercase ?? true, requireDigit: config.requireDigit ?? true, requireSpecial: config.requireSpecial ?? false, }; // Validate configuration if (this.config.rounds < 10 || this.config.rounds > 20) { throw new PasswordHashError( 'Bcrypt rounds must be between 10 and 20 for security and performance balance', 'INVALID_ROUNDS' ); } if (this.config.minLength < 8) { throw new PasswordHashError( 'Minimum password length must be at least 8 characters', 'INVALID_MIN_LENGTH' ); } } /** * Validates password against configured requirements. * * @param password - The password to validate * @returns Validation result with errors if any */ validate(password: string): PasswordValidationResult { const errors: string[] = []; if (!password) { errors.push('Password is required'); return { isValid: false, errors }; } if (password.length < this.config.minLength) { errors.push(`Password must be at least ${this.config.minLength} characters`); } if (password.length > this.config.maxLength) { errors.push(`Password must not exceed ${this.config.maxLength} characters`); } if (this.config.requireUppercase && !/[A-Z]/.test(password)) { errors.push('Password must contain at least one uppercase letter'); } if (this.config.requireLowercase && !/[a-z]/.test(password)) { errors.push('Password must contain at least one lowercase letter'); } if (this.config.requireDigit && !/\d/.test(password)) { errors.push('Password must contain at least one digit'); } if (this.config.requireSpecial && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) { errors.push('Password must contain at least one special character'); } return { isValid: errors.length === 0, errors, }; } /** * Hashes a password using bcrypt. * * @param password - The plaintext password to hash * @returns The bcrypt hash * @throws PasswordHashError if password is invalid */ async hash(password: string): Promise<string> { const validation = this.validate(password); if (!validation.isValid) { throw new PasswordHashError( validation.errors.join('; '), 'VALIDATION_FAILED' ); } try { // bcrypt automatically generates a random salt per hash return await bcrypt.hash(password, this.config.rounds); } catch (error) { throw new PasswordHashError( 'Failed to hash password', 'HASH_FAILED' ); } } /** * Verifies a password against a bcrypt hash. * Uses timing-safe comparison internally. * * @param password - The plaintext password to verify * @param hash - The bcrypt hash to compare against * @returns True if password matches, false otherwise */ async verify(password: string, hash: string): Promise<boolean> { if (!password || !hash) { return false; } // Validate hash format (bcrypt hashes start with $2a$, $2b$, or $2y$) if (!this.isValidBcryptHash(hash)) { return false; } try { // bcrypt.compare uses timing-safe comparison return await bcrypt.compare(password, hash); } catch (error) { // Return false on any error to prevent timing attacks return false; } } /** * Checks if a hash needs to be rehashed with updated parameters. * Useful for upgrading hash strength over time. * * @param hash - The bcrypt hash to check * @returns True if hash should be updated */ needsRehash(hash: string): boolean { if (!this.isValidBcryptHash(hash)) { return true; } // Extract rounds from hash (format: $2b$XX$...) const match = hash.match(/^\$2[aby]\$(\d{2})\$/); if (!match) { return true; } const hashRounds = parseInt(match[1], 10); return hashRounds < this.config.rounds; } /** * Validates bcrypt hash format. * * @param hash - The hash to validate * @returns True if valid bcrypt hash format */ private isValidBcryptHash(hash: string): boolean { // bcrypt hash format: $2a$XX$22charsSalt31charsHash // Total length: 60 characters return /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/.test(hash); } /** * Returns current configuration (without sensitive defaults). */ getConfig(): Readonly<Omit<Required<PasswordHasherConfig>, never>> { return { ...this.config }; } } /** * Factory function to create a production-ready password hasher. * * @param rounds - Bcrypt rounds (default: 12) * @returns Configured PasswordHasher instance */ export function createPasswordHasher(rounds = 12): PasswordHasher { return new PasswordHasher({ rounds }); }