From 3e2be2c33a199b69f57cbb5dcf2a616be397e5e3 Mon Sep 17 00:00:00 2001 From: momstrosity Date: Tue, 13 May 2025 14:56:29 +0000 Subject: [PATCH 1/5] Start draft PR From b67d8e2ab4bb722c82e5e6ac7a94f691867ec93d Mon Sep 17 00:00:00 2001 From: momstrosity Date: Tue, 13 May 2025 14:57:13 +0000 Subject: [PATCH 2/5] Create secure password hashing utility --- lib/auth/password.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 lib/auth/password.ts diff --git a/lib/auth/password.ts b/lib/auth/password.ts new file mode 100644 index 0000000..936bc40 --- /dev/null +++ b/lib/auth/password.ts @@ -0,0 +1,67 @@ +import bcrypt from 'bcrypt'; + +const SALT_ROUNDS = 12; // Recommended secure number of salt rounds + +/** + * Hash a plain text password using bcrypt + * @param password - Plain text password to hash + * @returns Hashed password + */ +export const hashPassword = async (password: string): Promise => { + // Input validation + if (!password || password.length < 8) { + throw new Error('Password must be at least 8 characters long'); + } + + try { + const salt = await bcrypt.genSalt(SALT_ROUNDS); + return await bcrypt.hash(password, salt); + } catch (error) { + console.error('Password hashing failed:', error); + throw new Error('Password hashing failed'); + } +}; + +/** + * Verify a password against its hash + * @param password - Plain text password to verify + * @param hashedPassword - Stored hashed password to compare against + * @returns Boolean indicating if password is correct + */ +export const verifyPassword = async ( + password: string, + hashedPassword: string +): Promise => { + // Input validation + if (!password || !hashedPassword) { + return false; + } + + try { + return await bcrypt.compare(password, hashedPassword); + } catch (error) { + console.error('Password verification failed:', error); + return false; + } +}; + +/** + * Check password complexity + * @param password - Password to validate + * @returns Boolean indicating password meets complexity requirements + */ +export const isPasswordComplex = (password: string): boolean => { + const minLength = 8; + const hasUppercase = /[A-Z]/.test(password); + const hasLowercase = /[a-z]/.test(password); + const hasNumber = /[0-9]/.test(password); + const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); + + return ( + password.length >= minLength && + hasUppercase && + hasLowercase && + hasNumber && + hasSpecialChar + ); +}; \ No newline at end of file From 8a918baa422e99973d688d1b41bcd93a8f5a930f Mon Sep 17 00:00:00 2001 From: momstrosity Date: Tue, 13 May 2025 14:57:22 +0000 Subject: [PATCH 3/5] Add tests for password hashing utility --- lib/auth/password.test.ts | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 lib/auth/password.test.ts diff --git a/lib/auth/password.test.ts b/lib/auth/password.test.ts new file mode 100644 index 0000000..74ab8fa --- /dev/null +++ b/lib/auth/password.test.ts @@ -0,0 +1,49 @@ +import { + hashPassword, + verifyPassword, + isPasswordComplex +} from './password'; + +describe('Password Utilities', () => { + const validPassword = 'StrongP@ssw0rd123!'; + const invalidPassword = 'weak'; + + describe('hashPassword', () => { + it('should hash a valid password', async () => { + const hashedPassword = await hashPassword(validPassword); + expect(hashedPassword).toBeTruthy(); + expect(hashedPassword).not.toBe(validPassword); + }); + + it('should throw error for short password', async () => { + await expect(hashPassword('short')).rejects.toThrow(); + }); + }); + + describe('verifyPassword', () => { + it('should verify correct password', async () => { + const hashedPassword = await hashPassword(validPassword); + const result = await verifyPassword(validPassword, hashedPassword); + expect(result).toBe(true); + }); + + it('should reject incorrect password', async () => { + const hashedPassword = await hashPassword(validPassword); + const result = await verifyPassword('WrongPassword123!', hashedPassword); + expect(result).toBe(false); + }); + }); + + describe('isPasswordComplex', () => { + it('should validate complex password', () => { + expect(isPasswordComplex(validPassword)).toBe(true); + }); + + it('should reject simple password', () => { + expect(isPasswordComplex('simple')).toBe(false); + expect(isPasswordComplex('onlylowercase123')).toBe(false); + expect(isPasswordComplex('ONLYUPPERCASE123')).toBe(false); + expect(isPasswordComplex('NoSpecialChars123')).toBe(false); + }); + }); +}); \ No newline at end of file From 16feda34b53f62fdc7f21c84049a156e89a01feb Mon Sep 17 00:00:00 2001 From: momstrosity Date: Tue, 13 May 2025 14:58:24 +0000 Subject: [PATCH 4/5] Implement secure password hashing with cryptographic best practices --- lib/auth/password.ts | 116 +++++++++++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 33 deletions(-) diff --git a/lib/auth/password.ts b/lib/auth/password.ts index 936bc40..4294027 100644 --- a/lib/auth/password.ts +++ b/lib/auth/password.ts @@ -1,24 +1,94 @@ -import bcrypt from 'bcrypt'; +import * as crypto from 'crypto'; +import * as bcrypt from 'bcrypt'; -const SALT_ROUNDS = 12; // Recommended secure number of salt rounds +// Constants for password security +const SALT_ROUNDS = 12; +const MIN_PASSWORD_LENGTH = 8; + +// Password complexity requirements +interface PasswordComplexityOptions { + minLength?: number; + requireUppercase?: boolean; + requireLowercase?: boolean; + requireNumbers?: boolean; + requireSpecialChars?: boolean; +} + +const DEFAULT_COMPLEXITY_OPTIONS: PasswordComplexityOptions = { + minLength: 8, + requireUppercase: true, + requireLowercase: true, + requireNumbers: true, + requireSpecialChars: true, +}; + +/** + * Generate a cryptographically secure random salt + * @returns Secure random salt + */ +export const generateSecureSalt = (): string => { + return crypto.randomBytes(16).toString('hex'); +}; /** - * Hash a plain text password using bcrypt - * @param password - Plain text password to hash + * Validate password complexity + * @param password - Password to validate + * @param options - Complexity requirements + * @returns Boolean indicating password meets complexity requirements + */ +export const validatePasswordComplexity = ( + password: string, + options: PasswordComplexityOptions = DEFAULT_COMPLEXITY_OPTIONS +): boolean => { + // Ensure password is not empty or undefined + if (!password) return false; + + // Check minimum length + if (password.length < (options.minLength || MIN_PASSWORD_LENGTH)) { + return false; + } + + // Uppercase check + if (options.requireUppercase && !/[A-Z]/.test(password)) { + return false; + } + + // Lowercase check + if (options.requireLowercase && !/[a-z]/.test(password)) { + return false; + } + + // Number check + if (options.requireNumbers && !/[0-9]/.test(password)) { + return false; + } + + // Special character check + if (options.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + return false; + } + + return true; +}; + +/** + * Securely hash a password + * @param password - Plain text password * @returns Hashed password + * @throws Error if password is invalid */ export const hashPassword = async (password: string): Promise => { - // Input validation - if (!password || password.length < 8) { - throw new Error('Password must be at least 8 characters long'); + // Validate password complexity before hashing + if (!validatePasswordComplexity(password)) { + throw new Error('Password does not meet complexity requirements'); } try { - const salt = await bcrypt.genSalt(SALT_ROUNDS); - return await bcrypt.hash(password, salt); + // Use bcrypt with 12 rounds and generate a secure salt + return await bcrypt.hash(password, SALT_ROUNDS); } catch (error) { - console.error('Password hashing failed:', error); - throw new Error('Password hashing failed'); + console.error('Password hashing failed', error); + throw new Error('Failed to hash password'); } }; @@ -38,30 +108,10 @@ export const verifyPassword = async ( } try { + // Use constant-time comparison to prevent timing attacks return await bcrypt.compare(password, hashedPassword); } catch (error) { - console.error('Password verification failed:', error); + console.error('Password verification failed', error); return false; } }; - -/** - * Check password complexity - * @param password - Password to validate - * @returns Boolean indicating password meets complexity requirements - */ -export const isPasswordComplex = (password: string): boolean => { - const minLength = 8; - const hasUppercase = /[A-Z]/.test(password); - const hasLowercase = /[a-z]/.test(password); - const hasNumber = /[0-9]/.test(password); - const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); - - return ( - password.length >= minLength && - hasUppercase && - hasLowercase && - hasNumber && - hasSpecialChar - ); -}; \ No newline at end of file From 32309714148ae31b5731c2fc9cd3b4d2a76a5e67 Mon Sep 17 00:00:00 2001 From: momstrosity Date: Tue, 13 May 2025 14:58:40 +0000 Subject: [PATCH 5/5] Add comprehensive tests for password security utilities --- lib/auth/password.test.ts | 93 +++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/lib/auth/password.test.ts b/lib/auth/password.test.ts index 74ab8fa..530644e 100644 --- a/lib/auth/password.test.ts +++ b/lib/auth/password.test.ts @@ -1,22 +1,80 @@ +import * as bcrypt from 'bcrypt'; import { + generateSecureSalt, + validatePasswordComplexity, hashPassword, - verifyPassword, - isPasswordComplex + verifyPassword } from './password'; -describe('Password Utilities', () => { +describe('Password Security Utilities', () => { + // Valid complex password for testing const validPassword = 'StrongP@ssw0rd123!'; - const invalidPassword = 'weak'; + const invalidPasswords = [ + 'short', // too short + 'onlylowercase', // missing uppercase, numbers, special chars + 'ONLYUPPERCASE', // missing lowercase, numbers, special chars + 'NoSpecialChars123', // missing special chars + ]; + + describe('generateSecureSalt', () => { + it('should generate a unique salt each time', () => { + const salt1 = generateSecureSalt(); + const salt2 = generateSecureSalt(); + + expect(salt1).toBeDefined(); + expect(salt2).toBeDefined(); + expect(salt1).not.toEqual(salt2); + }); + }); + + describe('validatePasswordComplexity', () => { + it('should validate complex password', () => { + expect(validatePasswordComplexity(validPassword)).toBe(true); + }); + + it.each(invalidPasswords)('should reject invalid password: %s', (password) => { + expect(validatePasswordComplexity(password)).toBe(false); + }); + + it('should handle empty password', () => { + expect(validatePasswordComplexity('')).toBe(false); + expect(validatePasswordComplexity(undefined as any)).toBe(false); + }); + + it('should allow custom complexity options', () => { + // Less strict options + expect(validatePasswordComplexity('simplePwd', { + minLength: 5, + requireUppercase: false, + requireLowercase: false, + requireNumbers: false, + requireSpecialChars: false + })).toBe(true); + }); + }); describe('hashPassword', () => { it('should hash a valid password', async () => { const hashedPassword = await hashPassword(validPassword); + expect(hashedPassword).toBeTruthy(); - expect(hashedPassword).not.toBe(validPassword); + expect(hashedPassword).not.toEqual(validPassword); + expect(hashedPassword.length).toBeGreaterThan(0); + }); + + it('should throw error for invalid password', async () => { + await expect(hashPassword('weak')).rejects.toThrow('Password does not meet complexity requirements'); }); - it('should throw error for short password', async () => { - await expect(hashPassword('short')).rejects.toThrow(); + it('should use 12 rounds of bcrypt hashing', async () => { + const bcryptHashSpy = jest.spyOn(bcrypt, 'hash'); + + await hashPassword(validPassword); + + expect(bcryptHashSpy).toHaveBeenCalledWith( + validPassword, + 12 // Verify 12 rounds are used + ); }); }); @@ -24,26 +82,23 @@ describe('Password Utilities', () => { it('should verify correct password', async () => { const hashedPassword = await hashPassword(validPassword); const result = await verifyPassword(validPassword, hashedPassword); + expect(result).toBe(true); }); it('should reject incorrect password', async () => { const hashedPassword = await hashPassword(validPassword); const result = await verifyPassword('WrongPassword123!', hashedPassword); + expect(result).toBe(false); }); - }); - - describe('isPasswordComplex', () => { - it('should validate complex password', () => { - expect(isPasswordComplex(validPassword)).toBe(true); - }); - it('should reject simple password', () => { - expect(isPasswordComplex('simple')).toBe(false); - expect(isPasswordComplex('onlylowercase123')).toBe(false); - expect(isPasswordComplex('ONLYUPPERCASE123')).toBe(false); - expect(isPasswordComplex('NoSpecialChars123')).toBe(false); + it('should handle empty inputs', async () => { + const result1 = await verifyPassword('', ''); + const result2 = await verifyPassword(undefined as any, undefined as any); + + expect(result1).toBe(false); + expect(result2).toBe(false); }); }); -}); \ No newline at end of file +});