Hashing Passwords
Why Hash Passwords? 🛡️
Plaintext storage is dangerous
graph LR
subgraph storingPasswords["fa:fa-database Storing Passwords"]
plainText["fa:fa-file-alt Password: 'password123'"] -->|"Insecure: Easily readable if database is breached"| dataThief["fa:fa-user-secret Data Thief"]
end
style plainText stroke:#800
style dataThief fill:#ffcccc,stroke:#800
Hashing protects against breaches
graph LR
subgraph passwordProtection["fa:fa-shield-alt Password Protection"]
user["fa:fa-user User"] -->|enter|plainText["fa:fa-file-alt Password: 'password123'"]
plainText --> hashFunction["fa:fa-key Hash Function (e.g., SHA-256)"]
hashFunction -->|"Irreversible process"| storedHash["fa:fa-lock Stored Hash (e.g., '9f86d0...1343c')"]
databaseBreach["fa:fa-exclamation-triangle Database Breach"] -->|"Cannot reverse hash to get password"| storedHash
end
style passwordProtection fill:#e0ffe0,stroke:#080
style user fill:#ccffcc,stroke:#080
style hashFunction fill:#ccffcc,stroke:#080
style storedHash fill:#cce5ff,stroke:#080
style databaseBreach fill:#ffe0e0,stroke:#800
- Insecure Plaintext: Storing passwords in plain text is like leaving your front door unlocked. If a database is breached, attackers can easily steal and use the passwords.
- Hashing as Protection: Hashing transforms passwords into unique, scrambled codes (hashes). This process is irreversible, meaning even if hackers get the hashes, they can’t directly obtain the original passwords.
Hashing is a one-way street
graph LR
subgraph hashingConcepts["fa:fa-road Hashing Concepts"]
input["fa:fa-unlock-alt Password: 'password123'"] -->|Hashing Algorithm| hashOutput["fa:fa-lock Hash: 9f86d0...1343c"]
hashOutput -.->|"fa:fa-ban Cannot be reversed"| input
end
style input fill:#cce5ee,stroke:#008
style hashOutput fill:#cce5ff,stroke:#008
- Hash Functions: A hash function takes an input (password) and produces a fixed-size string of characters (hash). Good hash functions are designed so different inputs result in very different outputs.
- One-Way Street: Hash functions are one-way, meaning you can’t reverse the process to get the original password from the hash. This is essential for security.
Salt and Pepper: Adding Flavor to Security 🧂🌶️
Salt
Salting makes each hash unique, even for identical passwords
- Salt: A unique, random value added to each password before hashing. This ensures that even if two users have the same password, their hashes will be different.
graph LR
subgraph salting["fa:fa-snowflake Salting"]
password["password123"]
salt1["fa:fa-snowflake Unique Salt 1"]
salt2["fa:fa-snowflake Unique Salt 2"]
password --> hashFunction1["fa:fa-key Hash Function"]
salt1 --> hashFunction1
password1["password123"] & salt2 --> hashFunction2["fa:fa-key Hash Function"]
hashFunction1 -->|"Different Hashes"| hashOutput1["Hash 1 + embed Salt 1"]
hashFunction2 -->|"Different Hashes"| hashOutput2["Hash 2 + embed Salt 2"]
end
style salt1 fill:#ffebcc,stroke:#880
style salt2 fill:#ffebcc,stroke:#880
style hashFunction1 fill:#cce5ff,stroke:#008
style hashFunction2 fill:#cce5ff,stroke:#008
style hashOutput1 fill:#fff0cc,stroke:#880
style hashOutput2 fill:#fff0cc,stroke:#880
Sample code for salting and verifying passwords
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const bcrypt = require('bcrypt');
const saltRounds = 10;
// Hashing a password
function hashPassword(password) {
const hash = bcrypt.hashSync(password, saltRounds);
return hash;
}
// Verifying a password
function verifyPassword(storedHash, inputPassword) {
return bcrypt.compareSync(inputPassword, storedHash);
}
// Example usage
const password = '123445';
const hash = hashPassword(password);
console.log(`Hash: ${hash}`);
const isValid = verifyPassword(hash, '123445');
console.log(`Password is valid: ${isValid}`);
Table: users
Sample table structure for storing salted passwords:
id | username | password_hash | |
---|---|---|---|
1 | joe | joe@gmail.com | hashed_password_with_salt_for_joe |
2 | doe | doe@gmail.com | hashed_password_with_salt_for_doe |
Pepper
Pepper adds a secret layer
graph LR
subgraph peppering["fa:fa-pepper-hot Peppering"]
plaintextPassword["fa:fa-keyboard Plaintext Password"] --> |fa:fa-plus|pepper["fa:fa-pepper-hot Secret Pepper"]
saltedHash["🧂 Salted Hash"]
pepper --> hashFunction
saltedHash --> hashFunction
hashFunction --> pepperedHash["fa:fa-lock Peppered Hash"]
end
style plaintextPassword fill:#ffebcc,stroke:#800
style pepper fill:#ffcccc,stroke:#800
style saltedHash fill:#ccffcc,stroke:#080
style hashFunction fill:#ccffcc,stroke:#080
style pepperedHash fill:#fff0cc,stroke:#880
- Pepper: A secret key added to the hashing process. Unlike salt, pepper is not stored with the hash, adding an extra layer of security. Typically kept in a separate, secure location such as in the application code or a secure environment variable. Even if the database is compromised, attackers won’t have access to the pepper.
Sample code for peppering passwords
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const bcrypt = require('bcrypt');
class PasswordService {
constructor(config) {
this.saltRounds = config.saltRounds || 10;
this.pepper = config.pepper;
if (!this.pepper) {
throw new Error('Missing required pepper configuration');
}
}
async hashPassword(password) {
try {
const pepperedPassword = password + this.pepper; // Simple concatenation
return await bcrypt.hash(pepperedPassword, this.saltRounds);
} catch (error) {
throw new Error('Password hashing failed');
}
}
async verifyPassword(storedHash, inputPassword) {
try {
const pepperedPassword = inputPassword + this.pepper; // Simple concatenation
return await bcrypt.compare(pepperedPassword, storedHash);
} catch (error) {
throw new Error('Password verification failed');
}
}
}
// sample usage
( async () => {
const passwordService = new PasswordService({
saltRounds: 10,
pepper: 'salty'
});
const password = 'password123';
const hashedPassword = await passwordService.hashPassword(password);
console.log(hashedPassword);
const isPasswordCorrect = await passwordService.verifyPassword(hashedPassword, password);
console.log(isPasswordCorrect);
} )()
Popular Hashing Algorithms 🧮
Different algorithms offer varying strengths
graph LR
subgraph hashingAlgorithms["fa:fa-calculator Hashing Algorithms"]
pbkdf2["fa:fa-calculator PBKDF2"]
bcrypt["fa:fa-calculator BCrypt"]
argon2["fa:fa-calculator Argon2"]
end
pbkdf2 -->|"Many iterations, slower"| strength["fa:fa-trophy Strength"]
bcrypt -->|"Memory-hard, even slower"| strength
argon2 -->|"Memory-hard, customizable, current standard"| strength
style hashingAlgorithms fill:#e0f0e0,stroke:#080
style pbkdf2 fill:#ffebcc,stroke:#880
style bcrypt fill:#cce5ff,stroke:#008
style argon2 fill:#ccffcc,stroke:#080
style strength fill:#ffe0e0,stroke:#800
- PBKDF2: Uses multiple iterations of a hash function to make it slower and more resistant to brute-force attacks.
- BCrypt: Specifically designed for password hashing. It’s slower than PBKDF2 and uses more memory, making it harder to crack.
- Argon2: The current standard for password hashing. It’s memory-hard, highly customizable, and resistant to various attacks.
Sample code for hashing and verifying passwords using Argon2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const argon2 = require('argon2');
async function hashPassword(password) {
// Hashing the password
const hash = await argon2.hash(password, {
type: argon2.argon2id, // Recommended variant
memoryCost: 2 ** 16, // Adjust memory usage (higher is slower)
timeCost: 3, // Adjust processing time (higher is slower)
parallelism: 1 // Number of parallel threads (usually 1)
});
return hash;
}
async function verifyPassword(password, hash) {
// Verifying the password against the stored hash
try {
if (await argon2.verify(hash, password)) {
return true; // Password matches
} else {
return false; // Password does not match
}
} catch (err) {
// Handle potential errors (e.g., invalid hash)
console.error('Error verifying password:', err);
return false;
}
}
// Example usage
async function main() {
const password = 'your_super_secret_password';
// Hash the password
const hashedPassword = await hashPassword(password);
console.log('Hashed password:', hashedPassword);
// Verify the password (replace with the actual stored hash)
const isMatch = await verifyPassword(password, hashedPassword);
console.log('Password match:', isMatch);
}
main();
Avoiding Weak Algorithms
Outdated algorithms like MD5
and SHA-1
should be avoided due to their vulnerability to collision attacks. DES
is insecure because of its short 56-bit key, making it susceptible to brute-force attacks. LANMAN
is weak as it splits passwords, converts them to uppercase, and uses DES, leading to easy brute-forcing. Using SHA-2
without salting is also insecure, as it leaves hashes open to rainbow table attacks
graph LR
subgraph outdatedAlgorithms["fa:fa-exclamation-triangle Outdated Algorithms to Avoid"]
md5["fa:fa-hashtag MD5"]
sha1["fa:fa-hashtag SHA-1"]
des["fa:fa-lock DES"]
lanman["fa:fa-key LANMAN"]
sha2NoSalt["fa:fa-hashtag SHA-2 (without salt)"]
end
md5 --> |"Vulnerable to collision attacks and fast brute-force attacks"| insecure["fa:fa-user-secret Insecure"]
sha1 --> |"Vulnerable to collision attacks"| insecure
des --> |"56-bit key length is too short, easily brute-forced"| insecure
lanman --> |"Splits password, converts to uppercase, uses DES, easily brute-forced"| insecure
sha2NoSalt --> |"Without salt, vulnerable to rainbow table attacks"| insecure
style md5 fill:#ffcccc,stroke:#800
style sha1 fill:#ffcccc,stroke:#800
style des fill:#ffcccc,stroke:#800
style lanman fill:#ffcccc,stroke:#800
style sha2NoSalt fill:#ffcccc,stroke:#800
style insecure fill:#eee,stroke:#802
Considerations for Secure Password Hashing 🔒
graph LR
subgraph bestPractices["fa:fa-thumbs-up Best Practices"]
useArgon2["fa:fa-check-circle Use Argon2 (if possible)"]
tuneParameters["fa:fa-sliders-h Tune parameters (memory, time)"]
updateRegularly["fa:fa-sync Update algorithm if needed"]
end
style bestPractices fill:#e0ffe0,stroke:#080
style useArgon2 fill:#ccffcc,stroke:#080
style tuneParameters fill:#fff0cc,stroke:#880
style updateRegularly fill:#cce5ff,stroke:#008
graph LR
subgraph advanced["fa:fa-star Advanced Techniques"]
keyStretching["fa:fa-arrows-alt-h Key Stretching (e.g., bcrypt)"]
hardwareModules["fa:fa-microchip Hardware Security Modules (HSMs)"]
end
style advanced fill:#e0f0e0,stroke:#080
style keyStretching fill:#cce5ff,stroke:#008
style hardwareModules fill:#ccffcc,stroke:#080
- Prioritize Argon2 as the recommended algorithm.
- Tune parameters (memory, time) to balance security and performance based on your system’s resources.
- Stay updated with the latest security guidelines and consider upgrading algorithms as needed.
- Key stretching: Applies the hash function multiple times to further slow down attackers.
- Hardware Security Modules (HSMs): Dedicated hardware for secure cryptographic operations, offering an extra layer of protection.
Use Hash to ensure data integrity
Hash functions are not only used for password hashing but also for ensuring data integrity. Hash-based message authentication codes (HMACs) combine a secret key with a hash function to create a secure way to verify data integrity.
graph
subgraph message
style message fill:#ccffcc,stroke:#,stroke-width:2px
data["💽 data"]
subgraph hash["HMAC"]
subgraph sha["✂️ SHA-256"]
style sha fill:#ccffff,stroke:#,stroke-width:2px
data1["💽 data"]
secret["㊙️ secret"]
end
end
end
Create a hash-based message authentication
1
2
3
4
5
6
7
8
9
const crypto = require('crypto');
const secretKey = 'your-secret-key';
const data = 'your-message-data';
const hmac = crypto.createHmac('sha256', secretKey);
hmac.update(data);
const hash = hmac.digest('hex');
console.log('HMAC:', hash);
Verify the integrity of the data using the HMAC
1
2
3
4
5
6
7
8
9
10
11
12
const receivedData = 'your-message-data';
const receivedHMAC = 'received-hmac-value'; // This should be the HMAC value received along with the data
const verifyHmac = crypto.createHmac('sha256', secretKey);
verifyHmac.update(receivedData);
const newHash = verifyHmac.digest('hex');
if (newHash === receivedHMAC) {
console.log('Data integrity verified');
} else {
console.log('Data has been tampered with');
}
Keywords To Remember
graph
subgraph
data["💽"]
salt["🧂"]
end
subgraph
hmac["#️"]
secret["㊙️"]
end
pepper["🌶️"]
hash["✂️"]