Post

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
    salt2 --> hashFunction2["fa:fa-key Hash Function"]

    hashFunction1 -->|"Different Hashes"| hashOutput1["Hash 1"]
    hashFunction2 -->|"Different Hashes"| hashOutput2["Hash 2"]
end

style password fill:#ffebcc,stroke:#880
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
23
24
const bcrypt = require('bcrypt');
const saltRounds = 10;

// Hashing a password
function hashPassword(password) {
    const salt = bcrypt.genSaltSync(saltRounds);
    const hash = bcrypt.hashSync(password, salt);
    return { salt, hash };
}

// Verifying a password
function verifyPassword(storedHash, storedSalt, inputPassword) {
    const hash = bcrypt.hashSync(inputPassword, storedSalt);
    return hash === storedHash;
}

// Example usage
const password = '123445';
const { salt, hash } = hashPassword(password);
console.log(`Salt: ${salt}`);
console.log(`Hash: ${hash}`);

const isValid = verifyPassword(hash, salt, '123445');
console.log(`Password is valid: ${isValid}`);

Table: users

Sample table structure for storing salted passwords:

idusernameemailsaltpassword_hash
1joejoe@gmail.comrandom_salt_for_joehashed_password_with_salt_for_joe
2doedoe@gmail.comrandom_salt_for_doehashed_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
49
50
51
52
53
54
55
56
57
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const saltRounds = 10;
const PEPPER = process.env.PASSWORD_PEPPER || 'default_pepper'; // For testing, set default value
const HMAC_KEY = process.env.HMAC_KEY || 'default_hmac_key'; // For testing, set default value

// Generate HMAC for a given input
function generateHmac(input) {
    return crypto.createHmac('sha256', HMAC_KEY).update(input).digest('hex');
}

// User registration
function registerUser(password) {
    const salt = bcrypt.genSaltSync(saltRounds);
    const hmacPassword = generateHmac(password + PEPPER);
    const hashedPassword = bcrypt.hashSync(hmacPassword, salt);
    // Store salt and hashedPassword in the database
    // Example: return { username: 'joe', salt: salt, password_hash: hashedPassword }
    return { salt, hashedPassword };
}

// User login
function loginUser(storedSalt, storedHash, inputPassword) {
    const hmacInputPassword = generateHmac(inputPassword + PEPPER);
    const hashedInputPassword = bcrypt.hashSync(hmacInputPassword, storedSalt);
    if (hashedInputPassword === storedHash) {
        console.log('Login successful!');
        return true;
    } else {
        console.log('Invalid password.');
        return false;
    }
}

// Test code
function runTests() {
    console.log('Running tests...');

    // Test 1: Register and login with correct password
    console.log(`Test 1: Register and login with correct password`)
    const password1 = 'mypassword';
    const { salt, hashedPassword } = registerUser(password1);
    console.log('Registered user with salt:', salt);
    const loginSuccess1 = loginUser(salt, hashedPassword, password1);
    console.assert(loginSuccess1, 'Test 1 failed: login should succeed with correct password');

    // Test 2: Attempt to login with incorrect password
     console.log(`\n Test 2: Attempt to login with incorrect password`)
    const incorrectPassword = 'wrongpassword';
    const loginSuccess2 = loginUser(salt, hashedPassword, incorrectPassword);
    console.assert(!loginSuccess2, 'Test 2 failed: login should fail with incorrect password');

}

// Run tests
runTests();

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["✂️"]
  
  
This post is licensed under CC BY 4.0 by the author.