@softvisio/core
Version:
Softisio core
677 lines (596 loc) • 20.1 kB
JavaScript
import "#lib/result";
import crypto from "node:crypto";
import { hmac, randomBytes } from "#lib/crypto";
import { fromPhc, toPhc } from "#lib/phc";
const DEFAULT_SALT_LENGTH = 16,
DEFAULT_HASH_LENGTH = 16,
ARGON2_VERSIONS = new Set( [ 16, 19 ] ),
ALGORITHMS = {
"argon2i": {
"algorithm": "argon2",
"version": 19,
"memoryCost": 1024 * 19, // 19 MiB
"timeCost": 2,
"parallelism": 1,
},
"argon2d": {
"algorithm": "argon2",
"version": 19,
"memoryCost": 1024 * 19, // 19 MiB
"timeCost": 2,
"parallelism": 1,
},
// DOCS: OWASP minimal settings in 2025
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
"argon2id": {
"algorithm": "argon2",
"version": 19,
"memoryCost": 1024 * 19, // 19 MiB
"timeCost": 2,
"parallelism": 1,
},
"scrypt": {
"algorithm": "scrypt",
"cost": 2 ** 17, // 128 MiB
"blockSize": 8, // 1024 bytes
"parallelism": 1,
},
"pbkdf2-sha1": {
"algorithm": "pbkdf2",
"digest": "SHA1",
"iterations": 1_300_000,
},
"pbkdf2-sha256": {
"algorithm": "pbkdf2",
"digest": "SHA256",
"iterations": 600_000,
},
"pbkdf2-sha512": {
"algorithm": "pbkdf2",
"digest": "SHA3-512",
"iterations": 210_000,
},
"hmac-sha256": {
"algorithm": "hmac",
"digest": "SHA256",
"saltLength": 32,
"keyLength": 32,
},
"hmac-sha512": {
"algorithm": "hmac",
"digest": "SHA512",
"saltLength": 64,
"keyLength": 64,
},
"hmac-sha3-256": {
"algorithm": "hmac",
"digest": "SHA3-256",
"saltLength": 32,
"keyLength": 32,
},
"hmac-sha3-512": {
"algorithm": "hmac",
"digest": "SHA3-512",
"saltLength": 64,
"keyLength": 64,
},
},
PRESETS = Object.freeze( {
"owasp": {
"id": "argon2id",
},
"argon2": {
"id": "argon2id",
},
"argon2i": {
"id": "argon2i",
},
"argon2d": {
"id": "argon2d",
},
"argon2id": {
"id": "argon2id",
},
// DOCS: rfc-9106 recommended settings in 2025
// https://datatracker.ietf.org/doc/html/rfc9106#name-recommendations
"rfc-9106-high": {
"id": "argon2id",
"memoryCost": 1024 * 1024 * 2, // 2 GiB
"timeCost": 1,
"parallelism": 1,
},
"rfc-9106-low": {
"id": "argon2id",
"memoryCost": 1024 * 64, // 64 MiB
"timeCost": 3,
"parallelism": 1,
},
"scrypt": {
"id": "scrypt",
},
"pbkdf2": {
"id": "pbkdf2-sha256",
},
"pbkdf2-sha1": {
"id": "pbkdf2-sha1",
},
"pbkdf2-sha256": {
"id": "pbkdf2-sha256",
},
"pbkdf2-sha512": {
"id": "pbkdf2-sha512",
},
"openssl": {
"id": "pbkdf2-sha256",
"iterations": 10_000,
"saltLength": 8,
},
"hmac-sha256": {
"id": "hmac-sha256",
},
"hmac-sha512": {
"id": "hmac-sha512",
},
"hmac-sha3-256": {
"id": "hmac-sha3-256",
},
"hmac-sha3-512": {
"id": "hmac-sha3-512",
},
} ),
DEFAULT_PRESET = "owasp";
export default class PasswordHash {
#preset;
#options;
constructor ( { preset, version, memoryCost, timeCost, parallelism, cost, blockSize, maxMemory, iterations, saltLength, hashLength } = {} ) {
preset ||= DEFAULT_PRESET;
this.#preset = preset;
const defaults = PRESETS[ this.#preset ];
if ( !defaults ) throw new Error( "Preset value is not valid" );
this.#options = {
"id": defaults.id,
};
const algorithm = ALGORITHMS[ this.#options.id ];
if ( !algorithm ) throw new Error( "Algorithm is not supported" );
this.#options.algorithm = algorithm.algorithm;
this.#options.saltLength = saltLength || defaults.saltLength || algorithm.saltLength || DEFAULT_SALT_LENGTH;
if ( this.#options.saltLength < 8 || this.#options.saltLength > 64 ) throw "Salt length value is not valid";
this.#options.hashLength = hashLength || defaults.hashLength || algorithm.hashLength || DEFAULT_HASH_LENGTH;
if ( this.#options.hashLength < 12 || this.#options.hashLength > 64 ) throw "Hash length value is not valid";
// argon2
if ( algorithm.algorithm === "argon2" ) {
this.#options.version = version || defaults.version || algorithm.version;
if ( !ARGON2_VERSIONS.has( this.#options.version ) ) throw "Argon2 version is not valid";
this.#options.memoryCost = memoryCost || defaults.memoryCost || algorithm.memoryCost;
if ( this.#options.memoryCost < 1 || this.#options.memoryCost > 2 ** 32 - 1 ) throw "Argon2 memory cost value is not valid";
this.#options.timeCost = timeCost || defaults.timeCost || algorithm.timeCost;
if ( this.#options.timeCost < 1 || this.#options.timeCost > 2 ** 32 - 1 ) throw "Argon2 time cost value is not valid";
this.#options.parallelism = parallelism || defaults.parallelism || algorithm.parallelism;
if ( this.#options.parallelism < 1 || this.#options.parallelism > 255 ) throw "Argon2 parallelism value is not valid";
}
// scrypt
else if ( algorithm.algorithm === "scrypt" ) {
this.#options.cost = cost || defaults.cost || algorithm.cost;
this.#options.blockSize = blockSize || defaults.blockSize || algorithm.blockSize;
this.#options.parallelism = parallelism || defaults.parallelism || algorithm.parallelism;
this.#options.maxMemory = maxMemory || this.constructor.calculateMaxMemory( this.#options );
}
// pbkdf2
else if ( algorithm.algorithm === "pbkdf2" ) {
this.#options.digest = algorithm.digest;
this.#options.iterations = iterations || defaults.iterations || algorithm.iterations;
}
// hmac
else if ( algorithm.algorithm === "hmac" ) {
this.#options.digest = algorithm.digest;
}
}
// static
static get presets () {
return PRESETS;
}
static get defaultPreset () {
return DEFAULT_PRESET;
}
static calculateMaxMemory ( { cost, blockSize, parallelism } ) {
// 128 * p * r + 128 * ( 2 + N ) * r
return 128 * parallelism * blockSize + 128 * ( 2 + cost ) * blockSize;
}
// properties
get preset () {
return this.#preset;
}
get algorithm () {
return this.#options.algorithm;
}
get id () {
return this.#options.id;
}
get version () {
return this.#options.version;
}
get memoryCost () {
return this.#options.memoryCost;
}
get timeCost () {
return this.#options.timeCost;
}
get parallelism () {
return this.#options.parallelism;
}
get cost () {
return this.#options.cost;
}
get blockSize () {
return this.#options.blockSize;
}
get maxMemory () {
return this.#options.maxMemory;
}
get digest () {
return this.#options.digest;
}
get iterations () {
return this.#options.iterations;
}
get saltLength () {
return this.#options.saltLength;
}
get hashLength () {
return this.#options.hashLength;
}
// public
async createPasswordHash ( password, { phc = true, salt, hashLength, secret, data } = {} ) {
return this.#createHash( password, {
...this.#options,
phc,
"salt": salt || ( await randomBytes( this.saltLength ) ),
"hashLength": hashLength || this.#options.hashLength,
secret,
data,
} );
}
async verifyPasswordHash ( digest, password, { update, phc = true, secret } = {} ) {
try {
if ( !( password instanceof Buffer ) ) password = Buffer.from( password );
const parsed = fromPhc( digest );
const algorithm = ALGORITHMS[ parsed.id ];
if ( !algorithm ) return result( [ 500, "Algorithm is not supported" ] );
const defaults = parsed.id === this.id
? this.#options
: algorithm;
var match = false,
requireUpdate = parsed.id !== this.id,
compareHash = true;
const options = {
"id": parsed.id,
"phc": false,
"salt": parsed.salt,
"hashLength": parsed.hash.length,
};
// argon2
if ( algorithm.algorithm === "argon2" ) {
// version
if ( !parsed.version ) {
options.version = defaults.version;
requireUpdate = true;
}
else if ( parsed.version === defaults.version ) {
options.version = parsed.version;
}
else {
requireUpdate = true;
compareHash = false;
}
// memoryCost
if ( !parsed.params.m ) {
options.memoryCost = defaults.memoryCost;
requireUpdate = true;
}
else if ( parsed.params.m === defaults.memoryCost ) {
options.memoryCost = parsed.params.m;
}
else {
requireUpdate = true;
compareHash = false;
}
// timeCost
if ( !parsed.params.t ) {
options.timeCost = defaults.timeCost;
requireUpdate = true;
}
else if ( parsed.params.t === defaults.timeCost ) {
options.timeCost = parsed.params.t;
}
else {
requireUpdate = true;
compareHash = false;
}
// parallelism
if ( !parsed.params.p ) {
options.parallelism = defaults.parallelism;
requireUpdate = true;
}
else if ( parsed.params.p === defaults.parallelism ) {
options.parallelism = parsed.params.p;
}
else {
requireUpdate = true;
compareHash = false;
}
options.secret = secret;
options.data = Buffer.from( parsed.params.data || "", "base64" );
}
// scrypt
else if ( algorithm.algorithm === "scrypt" ) {
// cost
if ( !parsed.params.ln ) {
options.cost = defaults.cost;
requireUpdate = true;
}
else if ( parsed.params.ln === defaults.cost ) {
options.cost = parsed.params.ln;
}
else {
requireUpdate = true;
compareHash = false;
}
// blockSize
if ( !parsed.params.r ) {
options.blockSize = defaults.blockSize;
requireUpdate = true;
}
else if ( parsed.params.r === defaults.blockSize ) {
options.blockSize = parsed.params.r;
}
else {
requireUpdate = true;
compareHash = false;
}
// parallelism
if ( !parsed.params.p ) {
options.parallelism = defaults.parallelism;
requireUpdate = true;
}
else if ( parsed.params.p === defaults.parallelism ) {
options.parallelism = parsed.params.p;
}
else {
requireUpdate = true;
compareHash = false;
}
options.maxMemory = this.maxMemory || this.constructor.calculateMaxMemory( options );
}
// pbkdf2
else if ( algorithm.algorithm === "pbkdf2" ) {
options.digest = algorithm.digest;
// iterations
if ( !parsed.params.i ) {
options.iterations = defaults.iterations;
requireUpdate = true;
}
else if ( parsed.params.i === defaults.iterations ) {
options.iterations = parsed.params.i;
}
else {
requireUpdate = true;
compareHash = false;
}
}
// hmac
else if ( algorithm.algorithm === "hmac" ) {
options.digest = algorithm.digest;
}
// compare hash
if ( compareHash ) {
const res = await this.#createHash( password, options );
if ( !res.ok ) return res;
match = crypto.timingSafeEqual( res.data.hash, parsed.hash );
}
var updatedHash;
if ( update && requireUpdate ) {
const res = await this.createPasswordHash( password, { phc } );
if ( !res.ok ) return res;
updatedHash = res.data;
}
else {
updatedHash = {};
}
if ( match ) {
return result( 200, {
requireUpdate,
...updatedHash,
} );
}
else {
return result( [ 400, "Password is not valid" ], {
requireUpdate,
...updatedHash,
} );
}
}
catch ( e ) {
return result.catch( e );
}
}
// private
async #createHash ( password, options ) {
try {
if ( !( password instanceof Buffer ) ) password = Buffer.from( password );
const algorithm = ALGORITHMS[ options.id ].algorithm;
let hash;
// argon2
if ( algorithm === "argon2" ) {
hash = await this.#createArgon2Hash( password, options );
}
// scrypt
else if ( algorithm === "scrypt" ) {
hash = await this.#createScryptHash( password, options );
}
// pbkdf2
else if ( algorithm === "pbkdf2" ) {
hash = await this.#createPbkdf2Hash( password, options );
}
// hmac
else if ( algorithm === "hmac" ) {
hash = await this.#createHmacHash( password, options );
}
return result( 200, hash );
}
catch ( e ) {
return result.catch( e );
}
}
async #createArgon2Hash ( password, { id, phc, salt, hashLength, version, memoryCost, timeCost, parallelism, secret, data } ) {
const hash = await new Promise( ( resolve, reject ) => {
crypto.argon2(
id,
{
"message": password,
"nonce": salt,
parallelism,
"tagLength": hashLength,
"memory": memoryCost,
"passes": timeCost,
secret,
"associatedData": data,
},
( e, hash ) => {
if ( e ) {
reject( e );
}
else {
resolve( hash );
}
}
);
} );
if ( phc ) {
return {
salt,
hash,
"phc": toPhc( {
id,
version,
"params": {
"m": memoryCost,
"t": timeCost,
"p": parallelism,
"data": data?.length
? data
: undefined,
},
salt,
hash,
} ),
};
}
else {
return {
salt,
hash,
};
}
}
async #createScryptHash ( password, { id, phc, salt, hashLength, cost, blockSize, parallelism, maxMemory } ) {
const hash = await new Promise( ( resolve, reject ) => {
crypto.scrypt(
password,
salt,
hashLength,
{
cost,
blockSize,
"parallelization": parallelism,
"maxmem": maxMemory,
},
( e, hash ) => {
if ( e ) {
reject( e );
}
else {
resolve( hash );
}
}
);
} );
if ( phc ) {
return {
salt,
hash,
"phc": toPhc( {
id,
"version": undefined,
"params": {
"ln": cost,
"r": blockSize,
"p": parallelism,
},
salt,
hash,
} ),
};
}
else {
return {
salt,
hash,
};
}
}
async #createPbkdf2Hash ( password, { id, phc, salt, hashLength, digest, iterations } ) {
const hash = await new Promise( ( resolve, reject ) => {
crypto.pbkdf2( password, salt, iterations, hashLength, digest, ( e, hash ) => {
if ( e ) {
reject( e );
}
else {
resolve( hash );
}
} );
} );
if ( phc ) {
return {
salt,
hash,
"phc": toPhc( {
id,
"version": undefined,
"params": {
"i": iterations,
},
salt,
hash,
} ),
};
}
else {
return {
salt,
hash,
};
}
}
async #createHmacHash ( password, { id, phc, salt, digest } ) {
const hash = await hmac( digest, salt, password );
if ( phc ) {
return {
salt,
hash,
"phc": toPhc( {
id,
"version": undefined,
salt,
hash,
} ),
};
}
else {
return {
salt,
hash,
};
}
}
}