I often wondered that if I have entered a password and someone else has coincidentally entered the same password, then after hashing the passwords would have been same. But I was wrong. There is a technique known as "salting" which, as its name suggests, adds some salt to the content (the password).
Salting in security is the practice of adding unique, random data (a "salt") to each user's password before it is hashed and stored. For robust implementation, best practices require using unique per-user salts of at least 16 bytes paired with modern algorithms and defense-in-depth techniques like peppering.
This is how salting works in steps:
- Generating the salt - it uses CSPRNG which is Cryptographically Secure Random Number Generator, creates a unique, random string for each user account.
- Combine password and salt - note, that this step is done before hashing which makes the hashed passwords different.
- Hashing the combined strings - this is where the passwords are hashed, run the result through Argon2id, bcrypt, or scrypt.
- Storing the salt and the hash - both of these go into the user's records for verification.
- Verifying while logging - the entered password is rehashed and compared with the stored password for a match.
Seeing it in code (Node.js)
Below is a simple Node.js example using the built-in crypto module (no external dependencies needed). It walks through the exact same five steps described above.
Step 1: Generate the salt
We use crypto.randomBytes, which is a CSPRNG, to create a unique, random salt for each user.
const crypto = require('crypto');
function generateSalt(length = 16) {
return crypto.randomBytes(length).toString('hex');
}
Step 2 & 3: Combine the password with the salt, then hash
The salt is combined with the password before hashing. Here we use scrypt, a memory-hard hashing algorithm similar in spirit to bcrypt/Argon2id.
function hashPassword(password, salt) {
const hash = crypto.scryptSync(password, salt, 64).toString('hex');
return hash;
}
Step 4: Store the salt and hash
Both the salt and the resulting hash are saved to the user's record (in a real app, this would go into a database).
function createUserRecord(password) {
const salt = generateSalt();
const hash = hashPassword(password, salt);
return { salt, hash }; // store both in the user's record
}
Step 5: Verify the password on login
When a user logs in, we rehash their entered password using the stored salt, and compare it against the stored hash.
function verifyPassword(inputPassword, storedSalt, storedHash) {
const inputHash = hashPassword(inputPassword, storedSalt);
return crypto.timingSafeEqual(
Buffer.from(inputHash, 'hex'),
Buffer.from(storedHash, 'hex')
);
}
We use crypto.timingSafeEqual instead of a plain === comparison to avoid timing attacks - this is a small example of the "defense-in-depth" approach mentioned earlier.
Putting it together: proving the point
This is the part that answers the question I started with - do two identical passwords really produce different hashes?
const password1 = "mySecret123";
const password2 = "mySecret123"; // same password as user 1
const user1 = createUserRecord(password1);
const user2 = createUserRecord(password2);
console.log("User 1 salt:", user1.salt);
console.log("User 1 hash:", user1.hash);
console.log("User 2 salt:", user2.salt);
console.log("User 2 hash:", user2.hash);
console.log("Hashes are different even though passwords match:",
user1.hash !== user2.hash);
// Login verification
const isValid = verifyPassword("mySecret123", user1.salt, user1.hash);
console.log("Password verification result:", isValid);
Even though password1 and password2 are identical strings, the random salt generated for each user makes the stored hashes completely different - which is exactly the mechanism that answers the doubt I had at the start.
Note: For production applications, dedicated libraries like
bcryptorargon2(available as npm packages) wrap this logic with sensible, battle-tested defaults and are generally preferred over hand-rollingscryptcalls. Since this post covers all three algorithms conceptually, the nativecryptomodule was used here to keep the example dependency-free and easy to follow.
Top comments (0)