Skip to content

Instantly share code, notes, and snippets.

@GauBen
Created April 26, 2023 10:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GauBen/8891c4a92f81a447b179f4c14deac90e to your computer and use it in GitHub Desktop.
Save GauBen/8891c4a92f81a447b179f4c14deac90e to your computer and use it in GitHub Desktop.
/**
* This module contains cryptography functions. While we strive to comply to all
* the best practices, we are not cryptographers. Use at your own risk.
*
* We use the `pbkdf2` algorithm to hash passwords because it works in the
* browser. It is considered secure by the RFC8018 (published in 2017), but most
* sources recommend using `argon2id` instead.
*
* @module
* @see https://datatracker.ietf.org/doc/html/rfc8018
* @see https://www.password-hashing.net/
* @see https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
*/
import { webcrypto as crypto } from 'node:crypto';
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
const iterations = 600000;
const digest = 'SHA-256';
/**
* > The salt SHALL be at least 32 bits.
* > https://pages.nist.gov/800-63-3/sp800-63b.html#-5112-memorized-secret-verifiers
*
* We will use 96 bits.
*
* @remarks
* These values must be multiple of 3 to be properly encoded in base64.
*/
const saltLength = 12;
const hashLength = 48;
/** Resulting length after hashing. */
export const passwordLength = ((saltLength + hashLength) * 4) / 3;
/** Hashes a password. */
export const hash = async (password) => {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits']
);
const salt = Buffer.from(crypto.getRandomValues(new Uint8Array(saltLength)));
const hashed = Buffer.from(
await crypto.subtle.deriveBits(
{ name: 'PBKDF2', hash: digest, iterations, salt },
key,
8 * hashLength
)
);
return salt.toString('base64url') + hashed.toString('base64url');
};
/** Verifies that a password matches its hash. */
export const verify = async (password, hash) => {
if (hash.length !== passwordLength) return false;
const salt = Buffer.from(hash.slice(0, (saltLength * 4) / 3), 'base64url');
const refHashed = new Uint8Array(
Buffer.from(hash.slice((saltLength * 4) / 3), 'base64url')
);
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits']
);
const hashed = new Uint8Array(
await crypto.subtle.deriveBits(
{ name: 'PBKDF2', hash: digest, iterations, salt },
key,
8 * hashLength
)
);
// Constant time comparison
let bits = 0;
for (let i = 0; i < hashed.length; i++) bits |= hashed[i] ^ refHashed[i];
return bits === 0;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment