DEV Community

geekordian
geekordian

Posted on

How are hashed passwords not the same even if the original passwords are.

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:

  1. Generating the salt - it uses CSPRNG which is Cryptographically Secure Random Number Generator, creates a unique, random string for each user account.
  2. Combine password and salt - note, that this step is done before hashing which makes the hashed passwords different.
  3. Hashing the combined strings - this is where the passwords are hashed, run the result through Argon2id, bcrypt, or scrypt.
  4. Storing the salt and the hash - both of these go into the user's records for verification.
  5. 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');
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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')
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 bcrypt or argon2 (available as npm packages) wrap this logic with sensible, battle-tested defaults and are generally preferred over hand-rolling scrypt calls. Since this post covers all three algorithms conceptually, the native crypto module was used here to keep the example dependency-free and easy to follow.

Top comments (0)