All files / src/auth password.ts

100% Statements 29/29
100% Branches 7/7
100% Functions 3/3
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 762x 2x   2x     2x 2x 2x                           2x 8x   8x 2x     8x 2x     8x 1x     8x 2x     8x                   2x 12x 12x 12x             2x 28x 28x 2x     26x 26x   26x     26x    
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
import { promisify } from "util";
 
const scryptAsync = promisify(scrypt);
 
// Scrypt parameters (recommended values for 2024+)
const SALT_LENGTH = 32;
const KEY_LENGTH = 64;
const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };
 
export interface PasswordValidationResult {
    valid: boolean;
    errors: string[];
}
 
/**
 * Password requirements:
 * - Minimum 8 characters
 * - At least 1 uppercase letter
 * - At least 1 lowercase letter
 * - At least 1 number
 */
export function validatePasswordStrength(password: string): PasswordValidationResult {
    const errors: string[] = [];
 
    if (password.length < 8) {
        errors.push("Password must be at least 8 characters long");
    }
 
    if (!/[A-Z]/.test(password)) {
        errors.push("Password must contain at least one uppercase letter");
    }
 
    if (!/[a-z]/.test(password)) {
        errors.push("Password must contain at least one lowercase letter");
    }
 
    if (!/[0-9]/.test(password)) {
        errors.push("Password must contain at least one number");
    }
 
    return {
        valid: errors.length === 0,
        errors
    };
}
 
/**
 * Hash a password using Node's built-in scrypt
 * Returns format: salt:hash (both hex encoded)
 */
export async function hashPassword(password: string): Promise<string> {
    const salt = randomBytes(SALT_LENGTH);
    const derivedKey = await scryptAsync(password, salt, KEY_LENGTH) as Buffer;
    return `${salt.toString("hex")}:${derivedKey.toString("hex")}`;
}
 
/**
 * Verify a password against a scrypt hash
 * Expects format: salt:hash (both hex encoded)
 */
export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
    const [saltHex, hashHex] = storedHash.split(":");
    if (!saltHex || !hashHex) {
        return false;
    }
 
    const salt = Buffer.from(saltHex, "hex");
    const storedKey = Buffer.from(hashHex, "hex");
 
    const derivedKey = await scryptAsync(password, salt, KEY_LENGTH) as Buffer;
 
    // Use timing-safe comparison to prevent timing attacks
    return timingSafeEqual(derivedKey, storedKey);
}