JavaScript (Node.js) Implementation

JavaScript (Node.js) Implementation

Node.js applications require careful library selection due to the ecosystem's rapid evolution. The argon2 package provides excellent performance with native bindings, while bcryptjs offers a pure JavaScript alternative for environments where native modules are problematic. The built-in crypto module handles random generation and HMAC operations for pepper implementation.

const crypto = require('crypto');
const argon2 = require('argon2');
const bcrypt = require('bcryptjs');

class SecurePasswordManager {
    constructor(options = {}) {
        this.algorithm = options.algorithm || 'argon2';
        this.pepper = options.pepper || null;
        this.version = 'v3';
        
        // Argon2 configuration
        this.argon2Options = {
            type: argon2.argon2id,
            memoryCost: 65536,  // 64MB
            timeCost: 3,        // iterations
            parallelism: 4,     // threads
            hashLength: 32,     // output length
            saltLength: 16      // salt length
        };
        
        // Bcrypt configuration
        this.bcryptRounds = 13;
    }
    
    async hashPassword(password) {
        if (!password || typeof password !== 'string') {
            throw new Error('Invalid password');
        }
        
        if (password.length > 1024) {
            throw new Error('Password too long');
        }
        
        // Apply pepper if configured
        let processedPassword = password;
        if (this.pepper) {
            processedPassword = this._applyPepper(password);
        }
        
        try {
            let hash;
            
            if (this.algorithm === 'argon2') {
                hash = await argon2.hash(processedPassword, this.argon2Options);
                return `${this.version}$argon2$${hash}`;
                
            } else if (this.algorithm === 'bcrypt') {
                // Handle bcrypt's 72-byte limit
                if (Buffer.byteLength(processedPassword, 'utf8') > 72) {
                    const hash = crypto.createHash('sha256')
                        .update(processedPassword)
                        .digest('base64');
                    processedPassword = hash;
                }
                
                hash = await bcrypt.hash(processedPassword, this.bcryptRounds);
                return `${this.version}$bcrypt$${hash}`;
                
            } else {
                throw new Error(`Unknown algorithm: ${this.algorithm}`);
            }
            
        } catch (error) {
            this._logError(`Hashing failed: ${error.message}`);
            throw new Error('Password hashing failed');
        }
    }
    
    async verifyPassword(password, storedHash) {
        if (!password || !storedHash) {
            return { valid: false, needsRehash: false };
        }
        
        try {
            const parts = storedHash.split('$');
            
            if (parts.length !== 3) {
                // Legacy format
                return await this._verifyLegacy(password, storedHash);
            }
            
            const [version, algorithm, hash] = parts;
            
            // Apply pepper if configured
            let processedPassword = password;
            if (this.pepper) {
                processedPassword = this._applyPepper(password);
            }
            
            if (algorithm === 'argon2') {
                const valid = await argon2.verify(hash, processedPassword);
                
                if (valid) {
                    // Check if rehashing needed
                    const needsRehash = await argon2.needsRehash(
                        hash, 
                        this.argon2Options
                    );
                    
                    return { valid: true, needsRehash };
                }
                
                return { valid: false, needsRehash: false };
                
            } else if (algorithm === 'bcrypt') {
                // Handle bcrypt's 72-byte limit
                if (Buffer.byteLength(processedPassword, 'utf8') > 72) {
                    const hash = crypto.createHash('sha256')
                        .update(processedPassword)
                        .digest('base64');
                    processedPassword = hash;
                }
                
                const valid = await bcrypt.compare(processedPassword, hash);
                
                if (valid && this.algorithm === 'argon2') {
                    // Migrate from bcrypt to argon2
                    return { valid: true, needsRehash: true };
                }
                
                return { valid, needsRehash: false };
                
            } else {
                this._logError(`Unknown algorithm: ${algorithm}`);
                return { valid: false, needsRehash: false };
            }
            
        } catch (error) {
            this._logError(`Verification failed: ${error.message}`);
            return { valid: false, needsRehash: false };
        }
    }
    
    _applyPepper(password) {
        const hmac = crypto.createHmac('sha256', this.pepper);
        hmac.update(password, 'utf8');
        return hmac.digest('base64');
    }
    
    async _verifyLegacy(password, legacyHash) {
        try {
            // Example: Legacy bcrypt without version prefix
            const valid = await bcrypt.compare(password, legacyHash);
            
            if (valid) {
                return { valid: true, needsRehash: true };
            }
        } catch (error) {
            // Legacy verification failed
        }
        
        return { valid: false, needsRehash: false };
    }
    
    _logError(message) {
        console.error(`[SECURITY] ${new Date().toISOString()} - ${message}`);
    }
    
    generateSecurePassword(length = 16) {
        const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
        const randomBytes = crypto.randomBytes(length);
        let password = '';
        
        for (let i = 0; i < length; i++) {
            password += charset[randomBytes[i] % charset.length];
        }
        
        return password;
    }
}

// Example usage and demonstration
async function demonstrateNodeImplementation() {
    console.log('Node.js Password Hashing Demo\n');
    
    // Initialize password manager
    const pm = new SecurePasswordManager({ algorithm: 'argon2' });
    
    // Test passwords
    const passwords = [
        'SimplePassword123',
        'VeryLongPassword'.repeat(10),
        'Unicode: café été 你好',
        'Special chars: !@#$%^&*()'
    ];
    
    const hashedPasswords = new Map();
    
    // Hash passwords
    for (const password of passwords) {
        try {
            const hash = await pm.hashPassword(password);
            hashedPasswords.set(password, hash);
            
            console.log(`Password: ${password.substring(0, 20)}...`);
            console.log(`Hash: ${hash.substring(0, 60)}...\n`);
        } catch (error) {
            console.error(`Failed to hash password: ${error.message}\n`);
        }
    }
    
    // Verify passwords
    console.log('Verification Tests:');
    
    for (const [password, hash] of hashedPasswords) {
        const result = await pm.verifyPassword(password, hash);
        console.log(`Password: ${password.substring(0, 20)}...`);
        console.log(`Valid: ${result.valid}, Needs rehash: ${result.needsRehash}`);
    }
    
    // Test wrong password
    const firstHash = hashedPasswords.values().next().value;
    const wrongResult = await pm.verifyPassword('wrong password', firstHash);
    console.log(`\nWrong password - Valid: ${wrongResult.valid}`);
    
    // Generate secure password
    const securePassword = pm.generateSecurePassword(20);
    console.log(`\nGenerated secure password: ${securePassword}`);
}

// Run demonstration
demonstrateNodeImplementation().catch(console.error);