Last active
December 12, 2025 20:57
-
-
Save teohhanhui/d6d73b952d448b593264b8591dc32202 to your computer and use it in GitHub Desktop.
Port of https://docs.rs/password-auth/ using Web Crypto API + experimental Argon2 support: https://wicg.github.io/webcrypto-modern-algos/
This file contains hidden or 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
| import { Buffer } from 'node:buffer'; | |
| import { webcrypto as crypto } from 'node:crypto'; | |
| /** | |
| * @see {@link https://datatracker.ietf.org/doc/html/rfc9106#section-3.1-2.2|RFC 9106} | |
| */ | |
| const SALT_LEN_BYTES: number = 16; | |
| /** | |
| * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id|OWASP Password Storage Cheat Sheet} | |
| */ | |
| const PARALLELISM: number = 1; | |
| const OUTPUT_LEN_BYTES: number = 32; | |
| /** | |
| * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id|OWASP Password Storage Cheat Sheet} | |
| */ | |
| const MEMORY_KIB: number = 19 * 1024; | |
| /** | |
| * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id|OWASP Password Storage Cheat Sheet} | |
| */ | |
| const PASSES: number = 2; | |
| /** | |
| * Generates password hash in the PHC string format. | |
| * | |
| * @see {@link https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md|PHC string format specification} | |
| */ | |
| async function generatePhcHash( | |
| password_: string, | |
| salt: Buffer, | |
| { | |
| parallelism = PARALLELISM, | |
| memory = MEMORY_KIB, | |
| passes = PASSES, | |
| tagLength = OUTPUT_LEN_BYTES, | |
| } = {}, | |
| ): Promise<string> { | |
| /** | |
| * @see {@link https://datatracker.ietf.org/doc/html/rfc8265#section-4|RFC 8265} | |
| */ | |
| const password = new TextEncoder().encode(password_.normalize('NFC')); | |
| if (password.byteLength === 0) { | |
| throw new RangeError('Password must not be zero bytes in length'); | |
| } | |
| const passwordKey = await crypto.subtle.importKey( | |
| 'raw-secret', | |
| password, | |
| 'Argon2id', | |
| false, | |
| ['deriveBits'], | |
| ); | |
| const deriveParams: crypto.Argon2Params = { | |
| name: 'Argon2id', | |
| nonce: salt, | |
| parallelism, | |
| memory, | |
| passes, | |
| version: 19, | |
| }; | |
| const hash = await crypto.subtle.deriveBits( | |
| deriveParams, | |
| passwordKey, | |
| tagLength * 8, | |
| ); | |
| const encodedSalt = salt.toString('base64').replace(/=/g, ''); | |
| const encodedHash = Buffer.from(hash).toString('base64').replace(/=/g, ''); | |
| return `$argon2id$v=19$m=${memory},t=${passes},p=${parallelism}$${encodedSalt}$${encodedHash}`; | |
| } | |
| /** | |
| * Generates a password hash for the given password. | |
| */ | |
| export async function generateHash(password: string): Promise<string> { | |
| const salt = crypto.getRandomValues(new Uint8Array(SALT_LEN_BYTES)); | |
| return generatePhcHash(password, Buffer.from(salt.buffer)); | |
| } | |
| /** | |
| * Verifies the provided password against the provided password hash. | |
| */ | |
| export async function verifyPassword( | |
| password: string, | |
| hash: string, | |
| ): Promise<boolean> { | |
| const hashPrefix = '$argon2id$v=19$'; | |
| if (!hash.startsWith(hashPrefix)) { | |
| return false; | |
| } | |
| const [parameters_, encodedSalt, encodedHash] = hash | |
| .substring(hashPrefix.length) | |
| .split('$', 3); | |
| if (!parameters_ || !encodedSalt || !encodedHash) { | |
| return false; | |
| } | |
| const parameters: { p?: string; m?: string; t?: string } = Object.fromEntries( | |
| parameters_.split(',').map((p) => p.split('=', 2)), | |
| ); | |
| if (!parameters.p || !parameters.m || !parameters.t) { | |
| return false; | |
| } | |
| const parallelism = Number.parseInt(parameters.p, 10); | |
| const memory = Number.parseInt(parameters.m, 10); | |
| const passes = Number.parseInt(parameters.t, 10); | |
| const salt = Buffer.from(encodedSalt, 'base64'); | |
| const tagLength = Buffer.byteLength(encodedHash, 'base64'); | |
| const computedHash = await generatePhcHash(password, salt, { | |
| parallelism, | |
| memory, | |
| passes, | |
| tagLength, | |
| }); | |
| return computedHash === hash; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment