Skip to content

Instantly share code, notes, and snippets.

@teohhanhui
Last active December 12, 2025 20:57
Show Gist options
  • Select an option

  • Save teohhanhui/d6d73b952d448b593264b8591dc32202 to your computer and use it in GitHub Desktop.

Select an option

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/
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