Password hashing in nodejs using built-in `crypto`
Asked Answered
M

3

9

What's the best way to implement password hashing and verification in node.js using only the built-in crypto module. Basically what is needed:

function passwordHash(password) {} // => passwordHash
function passwordVerify(password, passwordHash) {} // => boolean

People usually are using bcrypt or other third-party libs for this purpose. I wonder isn't built-in crypto module huge enough already to cover at least all basic needs?

There's scrypt(), which appears to be the right guy for this purpose, but there's no verified counterpart and nobody seems to care.

Milfordmilhaud answered 15/7, 2020 at 6:40 Comment(0)
S
21
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
import { promisify } from "util";

// scrypt is callback based so with promisify we can await it
const scryptAsync = promisify(scrypt);

Hashing process has two methods. First method, you hash the password, second method, you need to compare the new sign-in password with the stored password. I use typescript to write everything in detail

export class Password {

  static async hashPassword(password: string) {
    const salt = randomBytes(16).toString("hex");
    const buf = (await scryptAsync(password, salt, 64)) as Buffer;
    return `${buf.toString("hex")}.${salt}`;
  }

  static async comparePassword(
    storedPassword: string,
    suppliedPassword: string
  ): Promise<boolean> {
    // split() returns array
    const [hashedPassword, salt] = storedPassword.split(".");
    // we need to pass buffer values to timingSafeEqual
    const hashedPasswordBuf = Buffer.from(hashedPassword, "hex");
    // we hash the new sign-in password
    const suppliedPasswordBuf = (await scryptAsync(suppliedPassword, salt, 64)) as Buffer;
    // compare the new supplied password with the stored hashed password
    return timingSafeEqual(hashedPasswordBuf, suppliedPasswordBuf);
  }
}

Test it:

Password.hashPassword("123dafdas")
  .then((res) => Password.comparePassword(res, "123edafdas"))
  .then((res) => console.log(res));
Shaughnessy answered 10/4, 2021 at 18:46 Comment(4)
You may want to use crypto.timingSafeEqual to compareMilfordmilhaud
Shouldn't that be randomBytes(16), not 8? Even the official Node docs themselves recommend a 16 byte (minimum) salt.Pentapody
Here's the source for @machineghost's comment: "The salt should be as unique as possible. It is recommended that a salt is random and at least 16 bytes long. See NIST SP 800-132 for details."Valente
You can now use scryptSync() instead of awaiting the scryptAsync()Sessoms
L
5

It is quite an interesting thread, and different solutions are provided. After my findings, I propose a solution based on the Easy profiling for Node.js Applications

See the sample code below.

// add new user
app.get('/newUser', (req, res) => {
  let username = req.query.username || '';
  const password = req.query.password || '';

  username = username.replace(/[!@#$%^&*]/g, '');

  if (!username || !password || users[username]) {
    return res.sendStatus(400);
  }

  const salt = crypto.randomBytes(128).toString('base64');
  const hash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512');

  users[username] = { salt, hash };

  res.sendStatus(200);
});

validating user authentication attempts

// validating user authentication attempts
app.get('/auth', (req, res) => {
  let username = req.query.username || '';
  const password = req.query.password || '';

  username = username.replace(/[!@#$%^&*]/g, '');

  if (!username || !password || !users[username]) {
    return res.sendStatus(400);
  }

  const { salt, hash } = users[username];
  const encryptHash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512');

  if (crypto.timingSafeEqual(hash, encryptHash)) {
    res.sendStatus(200);
  } else {
    res.sendStatus(401);
  }
});

Please note that these are NOT recommended handlers for authenticating users in your Node.js applications and are used purely for illustration purposes. You should not be trying to design your own cryptographic authentication mechanisms in general. It is much better to use existing, proven authentication solutions.

Linn answered 25/12, 2022 at 13:29 Comment(0)
R
-1
const password = "my_password"; 

// Creating a unique salt for a particular user
const salt = crypto.randomBytes(16).toString('hex'); 
  
// Hash the salt and password with 1000 iterations, 64 length and sha512 digest 
const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');

Store both salt and hash for the user in DB.

const re_entered_password = "my_password";

// To verify the same - salt (stored in DB) with same other parameters used while creating hash (1000 iterations, 64 length and sha512 digest)
const newHash = crypto.pbkdf2Sync(re_entered_password, salt, 1000, 64, 'sha512').toString('hex');

// check if hash (stored in DB) and newly generated hash (newHash) are the same
hash === newHash;
Rios answered 15/7, 2020 at 7:16 Comment(4)
Thanks for the contribution. I see two drawbacks here (not an expert, though). 1) Requirement to store salt while I believe it's already stored somehow in hash 2) Verify function shouldn't have the same computational complexity as the hashing function.Milfordmilhaud
1) IMHO we can't extract salt directly from hash. We can store both salt and hash in one field (salt+hash) like other crypt libraries do. 2) I am not exactly sure on the shortcut methods to verify the same. To the best of my knowledge, we should recreate hash with same computational complexity to verify if it's same as the hash present in DB. Hope this answer clear the air for you.Rios
Hey @disfated, did you manage to get any simple solutions? I am little curious to know. Could you please help posting the answer if any. Thanks.Rios
PBKDF is really quite old, and the recommendation is to use 600,000 iterations if you still use it (which you may need to do because of FIPS) You might as well install argon2 these days. (and, I'm sure, in a few years, something even newer.)Ginni

© 2022 - 2024 — McMap. All rights reserved.