"use strict"; const assert = require("assert"); const { randomBytes, timingSafeEqual } = require("crypto"); const path = require("path"); const { promisify } = require("util"); const binary = require("@mapbox/node-pre-gyp"); const bindingPath = binary.find(path.resolve(__dirname, "./package.json")); const { hash: _hash } = require(bindingPath); const { deserialize, serialize } = require("@phc/format"); const types = Object.freeze({ argon2d: 0, argon2i: 1, argon2id: 2 }); const defaults = Object.freeze({ hashLength: 32, saltLength: 16, timeCost: 3, memoryCost: 1 << 16, parallelism: 4, type: types.argon2id, version: 0x13, }); const limits = Object.freeze({ hashLength: { min: 4, max: 2 ** 32 - 1 }, memoryCost: { min: 1 << 10, max: 2 ** 32 - 1 }, timeCost: { min: 2, max: 2 ** 32 - 1 }, parallelism: { min: 1, max: 2 ** 24 - 1 }, }); const names = Object.freeze({ [types.argon2d]: "argon2d", [types.argon2i]: "argon2i", [types.argon2id]: "argon2id", }); const bindingsHash = promisify(_hash); const generateSalt = promisify(randomBytes); const assertLimits = (options) => ([key, { max, min }]) => { const value = options[key]; assert( min <= value && value <= max, `Invalid ${key}, must be between ${min} and ${max}.`, ); }; const hash = async (plain, { raw, salt, ...options } = {}) => { options = { ...defaults, ...options }; Object.entries(limits).forEach(assertLimits(options)); salt = salt || (await generateSalt(options.saltLength)); const hash = await bindingsHash(Buffer.from(plain), salt, options); if (raw) { return hash; } const { type, version, memoryCost: m, timeCost: t, parallelism: p, associatedData: data, } = options; return serialize({ id: names[type], version, params: { m, t, p, ...(data ? { data } : {}) }, salt, hash, }); }; const needsRehash = (digest, options) => { const { memoryCost, timeCost, version } = { ...defaults, ...options }; const { version: v, params: { m, t }, } = deserialize(digest); return +v !== +version || +m !== +memoryCost || +t !== +timeCost; }; const verify = async (digest, plain, options) => { const obj = deserialize(digest); // Only these have the "params" key, so if the password was encoded // using any other method, the destructuring throws an error if (!(obj.id in types)) { return false; } const { id, version = 0x10, params: { m, t, p, data }, salt, hash, } = obj; return timingSafeEqual( await bindingsHash(Buffer.from(plain), salt, { ...options, type: types[id], version: +version, hashLength: hash.length, memoryCost: +m, timeCost: +t, parallelism: +p, ...(data ? { associatedData: Buffer.from(data, "base64") } : {}), }), hash, ); }; module.exports = { defaults, limits, hash, needsRehash, verify, ...types };