argon2
Version:
An Argon2 library for Node
197 lines (172 loc) • 4.96 kB
JavaScript
const { randomBytes, timingSafeEqual } = require("node:crypto");
const { promisify } = require("node:util");
const { deserialize, serialize } = require("@phc/format");
const gypBuild = require("node-gyp-build");
const { hash: bindingsHash } = gypBuild(__dirname);
/** @type {(size: number) => Promise<Buffer>} */
const generateSalt = promisify(randomBytes);
const argon2d = 0;
const argon2i = 1;
const argon2id = 2;
module.exports.argon2d = argon2d;
module.exports.argon2i = argon2i;
module.exports.argon2id = argon2id;
/** @enum {argon2i | argon2d | argon2id} */
const types = Object.freeze({ argon2d, argon2i, argon2id });
/** @enum {'argon2d' | 'argon2i' | 'argon2id'} */
const names = Object.freeze({
[types.argon2d]: "argon2d",
[types.argon2i]: "argon2i",
[types.argon2id]: "argon2id",
});
const defaults = {
hashLength: 32,
timeCost: 3,
memoryCost: 1 << 16,
parallelism: 4,
type: argon2id,
version: 0x13,
};
/**
* @typedef {Object} Options
* @property {number} [hashLength=32]
* @property {number} [timeCost=3]
* @property {number} [memoryCost=65536]
* @property {number} [parallelism=4]
* @property {keyof typeof names} [type=argon2id]
* @property {number} [version=19]
* @property {Buffer} [salt]
* @property {Buffer} [associatedData]
* @property {Buffer} [secret]
*/
/**
* Hashes a password with Argon2, producing a raw hash
*
* @overload
* @param {Buffer | string} password The plaintext password to be hashed
* @param {Options & { raw: true }} options The parameters for Argon2
* @returns {Promise<Buffer>} The raw hash generated from `password`
*/
/**
* Hashes a password with Argon2, producing an encoded hash
*
* @overload
* @param {Buffer | string} password The plaintext password to be hashed
* @param {Options & { raw?: boolean }} [options] The parameters for Argon2
* @returns {Promise<string>} The encoded hash generated from `password`
*/
/**
* @param {Buffer | string} password The plaintext password to be hashed
* @param {Options & { raw?: boolean }} [options] The parameters for Argon2
*/
async function hash(password, options) {
let { raw, salt, ...rest } = { ...defaults, ...options };
if (rest.hashLength > 2 ** 32 - 1) {
throw new RangeError("Hash length is too large");
}
if (rest.memoryCost > 2 ** 32 - 1) {
throw new RangeError("Memory cost is too large");
}
if (rest.timeCost > 2 ** 32 - 1) {
throw new RangeError("Time cost is too large");
}
if (rest.parallelism > 2 ** 24 - 1) {
throw new RangeError("Parallelism is too large");
}
salt = salt ?? (await generateSalt(16));
const {
hashLength,
secret = Buffer.alloc(0),
type,
version,
memoryCost: m,
timeCost: t,
parallelism: p,
associatedData: data = Buffer.alloc(0),
} = rest;
const hash = await bindingsHash({
password: Buffer.from(password),
salt,
secret,
data,
hashLength,
m,
t,
p,
version,
type,
});
if (raw) {
return hash;
}
return serialize({
id: names[type],
version,
params: { m, t, p, ...(data.byteLength > 0 ? { data } : {}) },
salt,
hash,
});
}
module.exports.hash = hash;
/**
* @param {string} digest The digest to be checked
* @param {Object} [options] The current parameters for Argon2
* @param {number} [options.timeCost=3]
* @param {number} [options.memoryCost=65536]
* @param {number} [options.parallelism=4]
* @param {number} [options.version=0x13]
* @returns {boolean} `true` if the digest parameters do not match the parameters in `options`, otherwise `false`
*/
function needsRehash(digest, options = {}) {
const { memoryCost, timeCost, parallelism, version } = {
...defaults,
...options,
};
const {
version: v,
params: { m, t, p },
} = deserialize(digest);
return (
+v !== +version ||
+m !== +memoryCost ||
+t !== +timeCost ||
+p !== +parallelism
);
}
module.exports.needsRehash = needsRehash;
/**
* @param {string} digest The digest to be checked
* @param {Buffer | string} password The plaintext password to be verified
* @param {Object} [options] The current parameters for Argon2
* @param {Buffer} [options.secret]
* @returns {Promise<boolean>} `true` if the digest parameters matches the hash generated from `password`, otherwise `false`
*/
async function verify(digest, password, options = {}) {
const { id, ...rest } = deserialize(digest);
if (!(id in types)) {
return false;
}
const {
version = 0x10,
params: { m, t, p, data = "" },
salt,
hash,
} = rest;
const { secret = Buffer.alloc(0) } = options;
return timingSafeEqual(
await bindingsHash({
password: Buffer.from(password),
salt,
secret,
data: Buffer.from(data, "base64"),
hashLength: hash.byteLength,
m: +m,
t: +t,
p: +p,
version: +version,
type: types[id],
}),
hash,
);
}
module.exports.verify = verify;