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 formatsalt: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>:trueif the data matches the hash,falseotherwise
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
-
"Data to hash cannot be empty" error
- Ensure you're not passing empty strings or null values
- Add input validation before hashing
-
Hash comparison always returns false
- Check that you're using the correct hash format
- Ensure the hash wasn't corrupted during storage
-
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.