Skip to main content

Hashing

Balda.js provides built-in secure password hashing functionality using industry-standard PBKDF2 with SHA-256. This feature is essential for securely storing user passwords and sensitive data.

Overview

The hashing system in Balda.js uses:

  • PBKDF2 (Password-Based Key Derivation Function 2) algorithm
  • SHA-256 as the underlying hash function
  • 600,000 iterations for enhanced security
  • 16-byte random salt for each hash
  • 256-bit (32-byte) hash output

Basic Usage

Hashing Passwords

import { Server } from 'balda-js';

const server = new Server();

// Hash a password
const password = 'user-password-123';
const hashedPassword = await server.hash(password);

console.log(hashedPassword);
// Output: "salt:hash" (base64 encoded)

Verifying Passwords

// Verify a password against its hash
const isValid = await server.compareHash(hashedPassword, password);

if (isValid) {
console.log('Password is correct');
} else {
console.log('Password is incorrect');
}

Complete Authentication Example

import { Server } from 'balda-js';
import { controller, post, get } from 'balda-js';

@controller('/auth')
export class AuthController {
private users = new Map<string, string>(); // In production, use a database

@post('/register')
async register(req: Request, res: Response) {
const { email, password } = req.body;

if (!email || !password) {
return res.badRequest({ error: 'Email and password are required' });
}

if (this.users.has(email)) {
return res.conflict({ error: 'User already exists' });
}

// Hash the password
const hashedPassword = await server.hash(password);
this.users.set(email, hashedPassword);

res.created({ message: 'User registered successfully' });
}

@post('/login')
async login(req: Request, res: Response) {
const { email, password } = req.body;

if (!email || !password) {
return res.badRequest({ error: 'Email and password are required' });
}

const hashedPassword = this.users.get(email);
if (!hashedPassword) {
return res.unauthorized({ error: 'Invalid credentials' });
}

// Verify the password
const isValid = await server.compareHash(hashedPassword, password);

if (!isValid) {
return res.unauthorized({ error: 'Invalid credentials' });
}

res.json({ message: 'Login successful', token: 'jwt-token-here' });
}

@get('/profile')
async getProfile(req: Request, res: Response) {
// This would be protected by authentication middleware
res.json({ user: 'authenticated-user' });
}
}

Security Features

Salt Generation

Each password hash includes a unique, cryptographically secure random salt:

const password = 'my-password';
const hash1 = await server.hash(password);
const hash2 = await server.hash(password);

// Different hashes due to different salts
console.log(hash1 !== hash2); // true

// But both verify correctly
const isValid1 = await server.compareHash(hash1, password);
const isValid2 = await server.compareHash(hash2, password);

console.log(isValid1 && isValid2); // true

Hash Format

The hash format is salt:hash where both parts are base64-encoded:

const hash = await server.hash('password');
const [salt, hashPart] = hash.split(':');

console.log('Salt (base64):', salt);
console.log('Hash (base64):', hashPart);

Advanced Usage

Password Strength Validation

function validatePasswordStrength(password: string): { valid: boolean; errors: string[] } {
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');
}

if (!/[^A-Za-z0-9]/.test(password)) {
errors.push('Password must contain at least one special character');
}

return {
valid: errors.length === 0,
errors
};
}

@post('/register')
async register(req: Request, res: Response) {
const { email, password } = req.body;

const validation = validatePasswordStrength(password);
if (!validation.valid) {
return res.badRequest({
error: 'Password does not meet requirements',
details: validation.errors
});
}

const hashedPassword = await server.hash(password);
// Store user...
}

Password Reset Flow

@post('/forgot-password')
async forgotPassword(req: Request, res: Response) {
const { email } = req.body;

// Generate reset token
const resetToken = crypto.randomBytes(32).toString('hex');
const resetTokenHash = await server.hash(resetToken);

// Store reset token with expiration
await storeResetToken(email, resetTokenHash, Date.now() + 3600000); // 1 hour

// Send email with reset link
await sendResetEmail(email, resetToken);

res.json({ message: 'Password reset email sent' });
}

@post('/reset-password')
async resetPassword(req: Request, res: Response) {
const { token, newPassword } = req.body;

// Find user by reset token
const user = await findUserByResetToken(token);
if (!user || user.resetTokenExpires < Date.now()) {
return res.badRequest({ error: 'Invalid or expired reset token' });
}

// Verify the token
const isValidToken = await server.compareHash(user.resetTokenHash, token);
if (!isValidToken) {
return res.badRequest({ error: 'Invalid reset token' });
}

// Hash new password
const hashedPassword = await server.hash(newPassword);

// Update user password and clear reset token
await updateUserPassword(user.email, hashedPassword);
await clearResetToken(user.email);

res.json({ message: 'Password reset successfully' });
}

Error Handling

Input Validation

@post('/change-password')
async changePassword(req: Request, res: Response) {
const { currentPassword, newPassword } = req.body;

if (!currentPassword || !newPassword) {
return res.badRequest({ error: 'Current and new passwords are required' });
}

if (newPassword.length < 8) {
return res.badRequest({ error: 'New password must be at least 8 characters' });
}

// Get user's current password hash
const user = await getUserByEmail(req.user.email);
const isCurrentPasswordValid = await server.compareHash(user.passwordHash, currentPassword);

if (!isCurrentPasswordValid) {
return res.unauthorized({ error: 'Current password is incorrect' });
}

// Hash new password
const newPasswordHash = await server.hash(newPassword);
await updateUserPassword(user.email, newPasswordHash);

res.json({ message: 'Password changed successfully' });
}

Empty String Handling

// This will throw an error
try {
await server.hash('');
} catch (error) {
console.log(error.message); // "Data to hash cannot be empty"
}

// Handle empty inputs gracefully
async function safeHash(password: string): Promise<string | null> {
if (!password || password.trim() === '') {
return null;
}
return await server.hash(password);
}

Performance Considerations

Hashing Performance

The PBKDF2 algorithm with 600,000 iterations is intentionally slow to prevent brute-force attacks:

// Benchmark hashing performance
async function benchmarkHashing() {
const password = 'test-password';
const iterations = 10;

console.time('Hashing Performance');

for (let i = 0; i < iterations; i++) {
await server.hash(password);
}

console.timeEnd('Hashing Performance');
// Typical result: ~2-5 seconds for 10 hashes
}

Async Operations

Always use await when hashing or comparing:

// Correct - async/await
const hash = await server.hash(password);
const isValid = await server.compareHash(hash, password);

// Incorrect - missing await
const hash = server.hash(password); // Returns Promise<string>
const isValid = server.compareHash(hash, password); // Returns Promise<boolean>

Security Best Practices

1. Never Store Plain Text Passwords

// ❌ Never do this
const user = {
email: 'user@example.com',
password: 'plain-text-password' // NEVER!
};

// ✅ Always hash passwords
const user = {
email: 'user@example.com',
passwordHash: await server.hash('user-password')
};

2. Use Strong Password Requirements

const passwordRequirements = {
minLength: 12,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
maxLength: 128
};

3. Implement Rate Limiting

import { rateLimiter } from 'balda-js';

// Rate limit login attempts
server.use('/auth/login', rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many login attempts'
}));

4. Use HTTPS in Production

// Ensure HTTPS is used for password transmission
if (process.env.NODE_ENV === 'production' && !req.secure) {
return res.redirect(`https://${req.get('host')}${req.url}`);
}

5. Log Security Events

@post('/login')
async login(req: Request, res: Response) {
const { email, password } = req.body;

const user = await getUserByEmail(email);
if (!user) {
// Log failed login attempt
logger.warn(`Failed login attempt for non-existent user: ${email}`, {
ip: req.ip,
userAgent: req.get('User-Agent')
});
return res.unauthorized({ error: 'Invalid credentials' });
}

const isValid = await server.compareHash(user.passwordHash, password);
if (!isValid) {
// Log failed login attempt
logger.warn(`Failed login attempt for user: ${email}`, {
ip: req.ip,
userAgent: req.get('User-Agent')
});
return res.unauthorized({ error: 'Invalid credentials' });
}

// Log successful login
logger.info(`Successful login for user: ${email}`, {
ip: req.ip,
userAgent: req.get('User-Agent')
});

res.json({ token: generateJWT(user) });
}

API Reference

server.hash(data: string): Promise<string>

Hashes a string using PBKDF2 with SHA-256.

Parameters:

  • data (string): The string to hash

Returns:

  • Promise<string>: A base64-encoded string in format salt:hash

Throws:

  • Error: If data is empty or null

Example:

const hash = await server.hash('my-password');
console.log(hash); // "dGVzdC1zYWx0:dGVzdC1oYXNo"

server.compareHash(hash: string, data: string): Promise<boolean>

Compares a string against a hash.

Parameters:

  • hash (string): The hash to compare against (format: salt:hash)
  • data (string): The string to verify

Returns:

  • Promise<boolean>: true if the data matches the hash, false otherwise

Example:

const hash = await server.hash('my-password');
const isValid = await server.compareHash(hash, 'my-password');
console.log(isValid); // true

const isInvalid = await server.compareHash(hash, 'wrong-password');
console.log(isInvalid); // false

Troubleshooting

Common Issues

  1. "Data to hash cannot be empty" error

    • Ensure you're not passing empty strings or null values
    • Add input validation before hashing
  2. Hash comparison always returns false

    • Check that you're using the correct hash format
    • Ensure the hash wasn't corrupted during storage
  3. Performance issues with hashing

    • The 600,000 iterations are intentional for security
    • Consider using background jobs for bulk operations

Debugging

// Enable debug logging
const server = new Server({
debug: true
});

// Log hash operations (remove in production)
const originalHash = server.hash;
server.hash = async function(data: string) {
console.log('Hashing data:', data.substring(0, 3) + '...');
const result = await originalHash.call(this, data);
console.log('Hash result:', result.substring(0, 10) + '...');
return result;
};

The hashing functionality in Balda.js provides enterprise-grade security for password management while maintaining ease of use and excellent performance characteristics.