mirror of
https://github.com/arthur-pbty/portfolio2023.git
synced 2026-06-03 23:36:21 +02:00
345 lines
9.1 KiB
JavaScript
345 lines
9.1 KiB
JavaScript
/* eslint-disable capitalized-comments,complexity,prefer-destructuring,promise/prefer-await-to-then,unicorn/prevent-abbreviations */
|
|
'use strict';
|
|
|
|
const argon2 = require('argon2');
|
|
const tsse = require('tsse');
|
|
const phc = require('@phc/format');
|
|
const gensalt = require('@kdf/salt');
|
|
|
|
const MAX_UINT32 = 4294967295; // 2**32 - 1
|
|
const MAX_UINT24 = 16777215; // 2**24 - 1
|
|
|
|
/**
|
|
* Default configurations used to generate a new hash.
|
|
* @private
|
|
* @type {Object}
|
|
*/
|
|
const defaults = Object.freeze({
|
|
// Argon2 variant to use. Can be one of argon2(d), argon2(i) or argon2(id).
|
|
variant: 'id',
|
|
|
|
// time cost, in linear iterations.
|
|
iterations: 3,
|
|
// memory cost, in kibibytes.
|
|
memory: 4096,
|
|
// parallelism, in number of threads and lanes.
|
|
parallelism: 1,
|
|
// The minimum recommended size for the salt is 128 bits.
|
|
saltSize: 16
|
|
});
|
|
|
|
/**
|
|
* Supported Argon2 variants.
|
|
* Argon2 currently has three modes:
|
|
* - d: Argon2d data-dependent.
|
|
* - i: Argon2i data-independent.
|
|
* - id: Argon2id a mix of the two.
|
|
* See https://crypto.stackexchange.com/a/49969
|
|
* @private
|
|
* @type {Object}
|
|
*/
|
|
const variants = Object.freeze({
|
|
i: argon2.argon2i,
|
|
d: argon2.argon2d,
|
|
id: argon2.argon2id
|
|
});
|
|
|
|
/**
|
|
* Supported Argon2 versions.
|
|
* @private
|
|
* @type {number[]}
|
|
*/
|
|
const versions = [
|
|
0x10, // 1.0 (16)
|
|
0x13 // 1.3 (19)
|
|
];
|
|
|
|
/**
|
|
* Computes the hash string of the given password in the PHC format using argon2
|
|
* package.
|
|
* @public
|
|
* @param {string} password The password to hash.
|
|
* @param {Object} [options] Optional configurations related to the hashing
|
|
* function.
|
|
* @param {number} [options.variant=id] Optinal variant of argon2 to use.
|
|
* Can be one of [`'d'`, `'i'`, `'id'`] for argon2d, argon2i and argon2id
|
|
* respectively.
|
|
* @param {number} [options.iterations=3] Optional number of iterations to use.
|
|
* Must be an integer within the range (`1` <= `iterations` <= `2^32-1`).
|
|
* @param {number} [options.memory=4096] Optional amount of memory to use in
|
|
* kibibytes.
|
|
* Must be an integer within the range (`8` <= `memory` <= `2^32-1`).
|
|
* @param {number} [options.parallelism=1] Optional degree of parallelism to
|
|
* use.
|
|
* Must be an integer within the range (`1` <= `parallelism` <= `2^24-1`).
|
|
* @param {number} [options.saltSize=16] Optional number of bytes to use when
|
|
* autogenerating new salts.
|
|
* Must be an integer within the range (`1` <= `saltSize` <= `2^10-1`).
|
|
* @return {Promise.<string>} The generated secure hash string in the PHC
|
|
* format.
|
|
*/
|
|
function hash(password, options) {
|
|
options = options || {};
|
|
let variant = options.variant || defaults.variant;
|
|
const iterations = options.iterations || defaults.iterations;
|
|
const memory = options.memory || defaults.memory;
|
|
const parallelism = options.parallelism || defaults.parallelism;
|
|
const saltSize = options.saltSize || defaults.saltSize;
|
|
const version = versions[versions.length - 1];
|
|
|
|
// Iterations Validation
|
|
if (typeof iterations !== 'number' || !Number.isInteger(iterations)) {
|
|
return Promise.reject(
|
|
new TypeError("The 'iterations' option must be an integer")
|
|
);
|
|
}
|
|
|
|
if (iterations < 1 || iterations > MAX_UINT32) {
|
|
return Promise.reject(
|
|
new TypeError(
|
|
`The 'iterations' option must be in the range (1 <= iterations <= ${MAX_UINT32})`
|
|
)
|
|
);
|
|
}
|
|
|
|
// Parallelism Validation
|
|
if (typeof parallelism !== 'number' || !Number.isInteger(parallelism)) {
|
|
return Promise.reject(
|
|
new TypeError("The 'parallelism' option must be an integer")
|
|
);
|
|
}
|
|
|
|
if (parallelism < 1 || parallelism > MAX_UINT24) {
|
|
return Promise.reject(
|
|
new TypeError(
|
|
`The 'parallelism' option must be in the range (1 <= parallelism <= ${MAX_UINT24})`
|
|
)
|
|
);
|
|
}
|
|
|
|
// Memory Validation
|
|
if (typeof memory !== 'number' || !Number.isInteger(memory)) {
|
|
return Promise.reject(
|
|
new TypeError("The 'memory' option must be an integer")
|
|
);
|
|
}
|
|
|
|
const minmem = 8 * parallelism;
|
|
if (memory < minmem || memory > MAX_UINT32) {
|
|
return Promise.reject(
|
|
new TypeError(
|
|
`The 'memory' option must be in the range (${minmem} <= memory <= ${MAX_UINT32})`
|
|
)
|
|
);
|
|
}
|
|
|
|
// Variant Validation
|
|
if (typeof variant !== 'string') {
|
|
return Promise.reject(
|
|
new TypeError("The 'variant' option must be a string")
|
|
);
|
|
}
|
|
|
|
variant = variant.toLowerCase();
|
|
if (!Object.prototype.hasOwnProperty.call(variants, variant)) {
|
|
return Promise.reject(
|
|
new TypeError(
|
|
`The 'variant' option must be one of: ${Object.keys(variants)}`
|
|
)
|
|
);
|
|
}
|
|
|
|
// Salt Size Validation
|
|
if (saltSize < 8 || saltSize > 1024) {
|
|
return Promise.reject(
|
|
new TypeError(
|
|
"The 'saltSize' option must be in the range (8 <= parallelism <= 1023)"
|
|
)
|
|
);
|
|
}
|
|
|
|
return gensalt(saltSize).then((salt) => {
|
|
const params = {
|
|
version,
|
|
type: variants[variant],
|
|
timeCost: iterations,
|
|
memoryCost: memory,
|
|
parallelism,
|
|
salt,
|
|
raw: true
|
|
};
|
|
return argon2.hash(password, params).then((hash) => {
|
|
const phcstr = phc.serialize({
|
|
id: `argon2${variant}`,
|
|
version,
|
|
params: {
|
|
t: iterations,
|
|
m: memory,
|
|
p: parallelism
|
|
},
|
|
salt,
|
|
hash
|
|
});
|
|
return phcstr;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Determines whether or not the hash stored inside the PHC formatted string
|
|
* matches the hash generated for the password provided.
|
|
* @public
|
|
* @param {string} phcstr Secure hash string generated from this package.
|
|
* @param {string} password User's password input.
|
|
* @returns {Promise.<boolean>} A boolean that is true if the hash computed
|
|
* for the password matches.
|
|
*/
|
|
function verify(phcstr, password) {
|
|
let phcobj;
|
|
try {
|
|
phcobj = phc.deserialize(phcstr);
|
|
} catch (error) {
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
// Identifier Validation
|
|
const idparts = phcobj.id.split('2');
|
|
if (
|
|
idparts.length !== 2 ||
|
|
idparts[0] === '' ||
|
|
idparts[1] === '' ||
|
|
idparts[0] !== 'argon'
|
|
) {
|
|
return Promise.reject(
|
|
new TypeError(`Incompatible ${phcobj.id} identifier found in the hash`)
|
|
);
|
|
}
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(variants, idparts[1])) {
|
|
return Promise.reject(
|
|
new TypeError(`Unsupported ${idparts[1]} variant function`)
|
|
);
|
|
}
|
|
|
|
const variant = idparts[1];
|
|
|
|
// Version Validation
|
|
if (typeof phcobj.version === 'undefined') {
|
|
phcobj.version = versions[0]; // Old Argon2 strings without the version.
|
|
}
|
|
|
|
if (!versions.includes(phcobj.version)) {
|
|
return Promise.reject(
|
|
new TypeError(`Unsupported ${phcobj.version} version`)
|
|
);
|
|
}
|
|
|
|
const version = phcobj.version;
|
|
|
|
// Parameters Existence Validation
|
|
if (typeof phcobj.params !== 'object') {
|
|
return Promise.reject(new TypeError('The param section cannot be empty'));
|
|
}
|
|
|
|
// Iterations Validation
|
|
if (
|
|
typeof phcobj.params.t !== 'number' ||
|
|
!Number.isInteger(phcobj.params.t)
|
|
) {
|
|
return Promise.reject(new TypeError("The 't' param must be an integer"));
|
|
}
|
|
|
|
if (phcobj.params.t < 1 || phcobj.params.t > MAX_UINT32) {
|
|
return Promise.reject(
|
|
new TypeError(
|
|
`The 't' param must be in the range (1 <= t <= ${MAX_UINT32})`
|
|
)
|
|
);
|
|
}
|
|
|
|
const iterations = phcobj.params.t;
|
|
|
|
// Parallelism Validation
|
|
if (
|
|
typeof phcobj.params.p !== 'number' ||
|
|
!Number.isInteger(phcobj.params.p)
|
|
) {
|
|
return Promise.reject(new TypeError("The 'p' param must be an integer"));
|
|
}
|
|
|
|
if (phcobj.params.p < 1 || phcobj.params.p > MAX_UINT24) {
|
|
return Promise.reject(
|
|
new TypeError(
|
|
`The 'p' param must be in the range (1 <= p <= ${MAX_UINT24})`
|
|
)
|
|
);
|
|
}
|
|
|
|
const parallelism = phcobj.params.p;
|
|
|
|
// Memory Validation
|
|
if (
|
|
typeof phcobj.params.m !== 'number' ||
|
|
!Number.isInteger(phcobj.params.m)
|
|
) {
|
|
return Promise.reject(new TypeError("The 'm' param must be an integer"));
|
|
}
|
|
|
|
const minmem = 8 * phcobj.params.p;
|
|
if (phcobj.params.m < minmem || phcobj.params.m > MAX_UINT32) {
|
|
return Promise.reject(
|
|
new TypeError(
|
|
`The 'm' param must be in the range (${minmem} <= m <= ${MAX_UINT32})`
|
|
)
|
|
);
|
|
}
|
|
|
|
const memory = phcobj.params.m;
|
|
|
|
// Salt Validation
|
|
if (typeof phcobj.salt === 'undefined') {
|
|
return Promise.reject(new TypeError('No salt found in the given string'));
|
|
}
|
|
|
|
const salt = phcobj.salt;
|
|
|
|
// Hash Validation
|
|
if (typeof phcobj.hash === 'undefined') {
|
|
return Promise.reject(new TypeError('No hash found in the given string'));
|
|
}
|
|
|
|
const hash = phcobj.hash;
|
|
const keylen = phcobj.hash.byteLength;
|
|
|
|
const params = {
|
|
version,
|
|
type: variants[variant],
|
|
timeCost: iterations,
|
|
memoryCost: memory,
|
|
parallelism,
|
|
salt,
|
|
hashLength: keylen,
|
|
raw: true
|
|
};
|
|
|
|
return argon2.hash(password, params).then((newhash) => {
|
|
const match = tsse(hash, newhash);
|
|
return match;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the list of all identifiers supported by this hashing function.
|
|
* @public
|
|
* @returns {string[]} A list of identifiers supported by this hashing function.
|
|
*/
|
|
function identifiers() {
|
|
return Object.keys(variants).map((variant) => `argon2${variant}`);
|
|
}
|
|
|
|
module.exports = {
|
|
hash,
|
|
verify,
|
|
identifiers
|
|
};
|