Encryption & Hashing
Keeping secrets secret - passwords, tokens, and sensitive data protection.
Two Different Jobs#
People often confuse encryption and hashing. They're different tools for different jobs:
Hashing = One-way transformation. Can't be reversed.
- Use for: Passwords, data integrity checks
- Input → Hash (no way to get input back)
Encryption = Two-way transformation. Can be reversed with a key.
- Use for: Data you need to read later
- Input + Key → Encrypted → Input (with key)
Password Hashing#
Never store plain text passwords. Ever.
// NEVER DO THIS
const user = {
email: 'john@example.com',
password: 'secret123' // Visible to anyone with database access
};
Instead, hash passwords with bcrypt:
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 10; // Higher = more secure but slower
// Hash a password
async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
}
// Verify a password
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
// Usage
const hash = await hashPassword('secret123');
// '$2b$10$N9qo8uLOickgx2ZMRZoMye...'
const isValid = await verifyPassword('secret123', hash); // true
const isInvalid = await verifyPassword('wrong', hash); // false
Why bcrypt?#
bcrypt is designed for passwords:
- Slow on purpose - Makes brute force attacks impractical
- Built-in salt - Prevents rainbow table attacks
- Adjustable work factor - Can increase security as hardware improves
// Salt rounds affect hashing time
// 10 = ~100ms
// 12 = ~300ms
// 14 = ~1s
// Start with 10, increase if your server can handle it
In Your Model#
// src/models/User.js
import mongoose from 'mongoose';
import bcrypt from 'bcrypt';
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true, select: false },
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
// Compare password method
userSchema.methods.comparePassword = async function(candidate) {
return bcrypt.compare(candidate, this.password);
};
export const User = mongoose.model('User', userSchema);
Token Generation#
For password resets, email verification, API keys:
import crypto from 'crypto';
// Generate random token
function generateToken(length = 32) {
return crypto.randomBytes(length).toString('hex');
}
// Generate URL-safe token (no special characters)
function generateUrlSafeToken(length = 32) {
return crypto.randomBytes(length)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Usage
const resetToken = generateToken();
// 'a1b2c3d4e5f6...' (64 hex characters)
const urlToken = generateUrlSafeToken();
// 'a1B2c3D4e5F6...' (URL-safe)
Storing Tokens Securely#
Don't store tokens in plain text either:
import crypto from 'crypto';
// Hash token before storing in database
function hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}
// Password reset flow
async function createPasswordReset(email) {
const user = await User.findOne({ email });
if (!user) return; // Don't reveal if email exists
// Generate token
const resetToken = generateToken();
// Store HASHED token in database
user.passwordResetToken = hashToken(resetToken);
user.passwordResetExpires = Date.now() + 60 * 60 * 1000; // 1 hour
await user.save();
// Send PLAIN token to user
await sendResetEmail(email, resetToken);
}
// Verify reset token
async function resetPassword(token, newPassword) {
// Hash the provided token
const hashedToken = hashToken(token);
// Find user with matching hash and non-expired token
const user = await User.findOne({
passwordResetToken: hashedToken,
passwordResetExpires: { $gt: Date.now() },
});
if (!user) {
throw new UnauthorizedError('Invalid or expired token');
}
// Update password and clear reset fields
user.password = newPassword;
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save();
}
Encryption for Sensitive Data#
When you need to read data back (API keys, personal info):
import crypto from 'crypto';
import { config } from '../config/index.js';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;
function encrypt(text) {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(
ALGORITHM,
Buffer.from(config.encryptionKey, 'hex'), // 32 bytes for aes-256
iv
);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Return IV + authTag + encrypted (all needed for decryption)
return iv.toString('hex') + authTag.toString('hex') + encrypted;
}
function decrypt(encryptedData) {
const iv = Buffer.from(encryptedData.slice(0, IV_LENGTH * 2), 'hex');
const authTag = Buffer.from(
encryptedData.slice(IV_LENGTH * 2, IV_LENGTH * 2 + AUTH_TAG_LENGTH * 2),
'hex'
);
const encrypted = encryptedData.slice(IV_LENGTH * 2 + AUTH_TAG_LENGTH * 2);
const decipher = crypto.createDecipheriv(
ALGORITHM,
Buffer.from(config.encryptionKey, 'hex'),
iv
);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
When to Encrypt#
Encrypt data that:
- You need to read later (unlike passwords)
- Is sensitive (SSN, credit cards, medical info)
- Must be protected at rest
// Encrypting user's personal data
const user = {
email: 'john@example.com',
ssn: encrypt('123-45-6789'), // Encrypted in database
};
// Reading it back
const ssn = decrypt(user.ssn); // '123-45-6789'
Generating Encryption Keys#
# Generate a 32-byte key for AES-256
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Store in environment variables:
# .env
ENCRYPTION_KEY=a1b2c3d4e5f6... # 64 hex characters
OTP Generation#
For two-factor authentication:
function generateOTP(length = 6) {
const digits = '0123456789';
let otp = '';
const randomBytes = crypto.randomBytes(length);
for (let i = 0; i < length; i++) {
otp += digits[randomBytes[i] % 10];
}
return otp;
}
// Usage
const otp = generateOTP(); // '847291'
API Key Generation#
function generateApiKey() {
const prefix = 'sk_live_'; // Identifies key type
const key = crypto.randomBytes(24).toString('base64url');
return prefix + key;
}
// 'sk_live_a1B2c3D4e5F6g7H8i9J0k1L2...'
Storing API Keys#
// Don't store the full key - store prefix + hash
async function createApiKey(userId) {
const apiKey = generateApiKey();
const prefix = apiKey.slice(0, 12); // 'sk_live_a1B2'
const hash = hashToken(apiKey);
await ApiKey.create({
userId,
prefix, // For display: "sk_live_a1B2..."
hash, // For verification
createdAt: new Date(),
});
// Return full key ONCE - user must save it
return apiKey;
}
// Verify API key
async function verifyApiKey(apiKey) {
const hash = hashToken(apiKey);
const key = await ApiKey.findOne({ hash });
if (!key) {
throw new UnauthorizedError('Invalid API key');
}
return key;
}
Security Checklist#
Passwords#
- Hash with bcrypt (not SHA, not MD5)
- Use sufficient salt rounds (10+)
- Never log passwords
- Never return passwords in API responses
Tokens#
- Use crypto.randomBytes (not Math.random)
- Store hashed tokens in database
- Set expiration times
- Invalidate after use (for reset tokens)
Encryption#
- Use AES-256-GCM (authenticated encryption)
- Generate random IV for each encryption
- Store encryption key securely (env var, secret manager)
- Rotate keys periodically
General#
- Never commit secrets to git
- Use HTTPS in production
- Implement rate limiting
- Log failed attempts (without logging passwords)
Key Takeaways#
- Hash passwords, never store plain text - bcrypt is the standard
- Hash tokens before storing - Protects against database leaks
- Encrypt data you need to read back - API keys, personal info
- Use crypto.randomBytes - Not Math.random for security
- Keys go in environment variables - Never in code
If Your Database Leaks
With proper hashing/encryption:
- Passwords: Attacker gets useless hashes
- Reset tokens: Attacker gets useless hashes
- Encrypted data: Attacker gets encrypted blobs (useless without key)
Without proper protection: Attacker gets everything.
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.