Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions lib/__tests__/passwordUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { PasswordUtils } from '../passwordUtils';

describe('PasswordUtils', () => {
const testPassword = 'StrongPass123!';

describe('hashPassword', () => {
it('should hash a valid password', () => {
const { salt, hashedPassword } = PasswordUtils.hashPassword(testPassword);

expect(salt).toBeTruthy();
expect(hashedPassword).toBeTruthy();
expect(salt).not.toEqual(hashedPassword);
});

it('should throw error for short password', () => {
expect(() => PasswordUtils.hashPassword('short')).toThrow('Password must be at least 8 characters long');
});
});

describe('verifyPassword', () => {
it('should verify correct password', () => {
const { salt, hashedPassword } = PasswordUtils.hashPassword(testPassword);

const isValid = PasswordUtils.verifyPassword(salt, hashedPassword, testPassword);
expect(isValid).toBe(true);
});

it('should reject incorrect password', () => {
const { salt, hashedPassword } = PasswordUtils.hashPassword(testPassword);

const isValid = PasswordUtils.verifyPassword(salt, hashedPassword, 'WrongPassword123!');
expect(isValid).toBe(false);
});

it('should handle empty inputs', () => {
const isValid = PasswordUtils.verifyPassword('', '', '');
expect(isValid).toBe(false);
});
});

describe('validatePasswordStrength', () => {
it('should validate strong passwords', () => {
expect(PasswordUtils.validatePasswordStrength('StrongPass123')).toBe(true);
expect(PasswordUtils.validatePasswordStrength('AnotherSecure456')).toBe(true);
});

it('should reject weak passwords', () => {
expect(PasswordUtils.validatePasswordStrength('weak')).toBe(false);
expect(PasswordUtils.validatePasswordStrength('onlylowercase')).toBe(false);
expect(PasswordUtils.validatePasswordStrength('ONLYUPPERCASE')).toBe(false);
expect(PasswordUtils.validatePasswordStrength('12345678')).toBe(false);
});
});
});
83 changes: 83 additions & 0 deletions lib/passwordUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import crypto from 'crypto';

/**
* Password utility functions for secure password hashing and verification
*/
export class PasswordUtils {
// Number of iterations for key derivation (adjustable for security)
private static readonly ITERATIONS = 10000;
// Length of the derived key
private static readonly KEY_LENGTH = 64;
// Hashing algorithm
private static readonly HASH_ALGORITHM = 'sha512';

/**
* Hash a password using PBKDF2 (Password-Based Key Derivation Function 2)
* @param password Plain text password
* @returns Object containing salt and hashed password
*/
static hashPassword(password: string): { salt: string; hashedPassword: string } {
// Validate input
if (!password || password.length < 8) {
throw new Error('Password must be at least 8 characters long');
}

// Generate a cryptographically secure random salt
const salt = crypto.randomBytes(16).toString('hex');

// Derive key using PBKDF2
const hashedPassword = crypto.pbkdf2Sync(
password,
salt,
this.ITERATIONS,
this.KEY_LENGTH,
this.HASH_ALGORITHM
).toString('hex');

return { salt, hashedPassword };
}

/**
* Verify a password against a stored hash
* @param storedSalt Salt used in original hashing
* @param storedHash Stored hashed password
* @param providedPassword Password to verify
* @returns Boolean indicating if password is correct
*/
static verifyPassword(
storedSalt: string,
storedHash: string,
providedPassword: string
): boolean {
// Validate inputs
if (!storedSalt || !storedHash || !providedPassword) {
return false;
}

// Hash the provided password with the stored salt
const hashedAttempt = crypto.pbkdf2Sync(
providedPassword,
storedSalt,
this.ITERATIONS,
this.KEY_LENGTH,
this.HASH_ALGORITHM
).toString('hex');

// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(storedHash),
Buffer.from(hashedAttempt)
);
}

/**
* Validate password strength
* @param password Password to validate
* @returns Boolean indicating if password meets requirements
*/
static validatePasswordStrength(password: string): boolean {
// At least 8 characters, one uppercase, one lowercase, one number
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
return passwordRegex.test(password);
}
}