Created
April 26, 2023 10:28
-
-
Save GauBen/8891c4a92f81a447b179f4c14deac90e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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