hashy
Version:
Hash passwords the right way (Argon2 & bcrypt support)
297 lines (248 loc) • 7.51 kB
JavaScript
/**
* This file is part of Hashy which is released under the MIT license.
*
* @author Julien Fontanet <julien.fontanet@isonoe.net>
*/
;
// ===================================================================
const promiseToolbox = require("promise-toolbox");
const asCallback = promiseToolbox.asCallback;
const promisifyAll = promiseToolbox.promisifyAll;
// ===================================================================
// Similar to Bluebird.method(fn) but handle Node callbacks.
const makeAsyncWrapper = (function (push) {
return function makeAsyncWrapper(fn) {
return function asyncWrapper() {
const args = [];
push.apply(args, arguments);
let callback;
const n = args.length;
if (n && typeof args[n - 1] === "function") {
callback = args.pop();
}
return asCallback.call(
new Promise(function (resolve) {
resolve(fn.apply(this, args));
}),
callback,
);
};
};
})(Array.prototype.push);
// ===================================================================
const algorithmsById = Object.create(null);
const algorithmsByName = Object.create(null);
const globalOptions = Object.create(null);
exports.options = globalOptions;
let DEFAULT_ALGO;
Object.defineProperty(exports, "DEFAULT_ALGO", {
enumerable: true,
get: function () {
return DEFAULT_ALGO;
},
});
function registerAlgorithm(algo) {
const name = algo.name;
if (algorithmsByName[name]) {
throw new Error("name " + name + " already taken");
}
algorithmsByName[name] = algo;
algo.ids.forEach(function (id) {
if (algorithmsById[id]) {
throw new Error("id " + id + " already taken");
}
algorithmsById[id] = algo;
});
globalOptions[name] = Object.assign(Object.create(null), algo.defaults);
if (!DEFAULT_ALGO) {
DEFAULT_ALGO = name;
}
}
// -------------------------------------------------------------------
(function (argon2) {
registerAlgorithm({
name: "argon2",
ids: ["argon2d", "argon2i", "argon2id"],
defaults: require("argon2").defaults,
getOptions: function (hash, info) {
let rawOptions = info.options;
let options = {};
// Since Argon2 1.3, the version number is encoded in the hash.
let version;
if (rawOptions.slice(0, 2) === "v=") {
version = +rawOptions.slice(2);
const index = hash.indexOf(rawOptions) + rawOptions.length + 1;
rawOptions = hash.slice(index, hash.indexOf("$", index));
}
rawOptions.split(",").forEach(function (datum) {
const index = datum.indexOf("=");
if (index === -1) {
options[datum] = true;
} else {
options[datum.slice(0, index)] = datum.slice(index + 1);
}
});
options = {
memoryCost: +options.m,
parallelism: +options.p,
timeCost: +options.t,
};
if (version !== undefined) {
options.version = version;
}
return options;
},
hash: argon2.hash,
needsRehash: argon2.needsRehash,
verify: function (password, hash) {
return argon2.verify(hash, password);
},
});
})(require("argon2"));
(function (bcrypt) {
registerAlgorithm({
name: "bcrypt",
ids: ["2", "2a", "2b", "2x", "2y"],
defaults: { cost: 10 },
getOptions: function (_, info) {
return {
cost: +info.options,
};
},
hash: function (password, options) {
return bcrypt.genSalt(options.cost).then(function (salt) {
return bcrypt.hash(password, salt);
});
},
needsRehash: function (_, info) {
const id = info.id;
if (id !== "2a" && id !== "2b" && id !== "2y") {
return true;
}
// Otherwise, let the default algorithm decides.
},
verify: function (password, hash) {
// See: https://github.com/ncb000gt/node.bcrypt.js/issues/175#issuecomment-26837823
if (hash.startsWith("$2y$")) {
hash = "$2a$" + hash.slice(4);
}
return bcrypt.compare(password, hash);
},
});
})(promisifyAll(require("bcryptjs")));
// -------------------------------------------------------------------
const getHashInfo = (function (HASH_RE) {
return function getHashInfo(hash) {
const matches = hash.match(HASH_RE);
if (!matches) {
throw new Error("invalid hash " + hash);
}
return {
id: matches[1],
options: matches[2],
};
};
})(/^\$([^$]+)\$([^$]*)\$/);
function getAlgorithmByName(name) {
const algo = algorithmsByName[name];
if (!algo) {
throw new Error("no available algorithm with name " + name);
}
return algo;
}
function getAlgorithmFromId(id) {
const algo = algorithmsById[id];
if (!algo) {
throw new Error("no available algorithm with id " + id);
}
return algo;
}
function getAlgorithmFromHash(hash) {
return getAlgorithmFromId(getHashInfo(hash).id);
}
// ===================================================================
/**
* Hashes a password.
*
* @param {string} password The password to hash.
* @param {integer} algo Identifier of the algorithm to use.
* @param {object} options Options for the algorithm.
* @param {function} callback Optional callback.
*
* @return {object} A promise which will receive the hashed password.
*/
function hash(password, algo, options) {
algo = getAlgorithmByName(algo || DEFAULT_ALGO);
return algo.hash(
password,
Object.assign(Object.create(null), globalOptions[algo.name], options),
);
}
exports.hash = makeAsyncWrapper(hash);
/**
* Returns information about a hash.
*
* @param {string} hash The hash you want to get information from.
*
* @return {object} Object containing information about the given
* hash: “algorithm”: the algorithm used, “options” the options
* used.
*/
function getInfo(hash) {
const info = getHashInfo(hash);
const algo = getAlgorithmFromId(info.id);
info.algorithm = algo.name;
info.options = algo.getOptions(hash, info);
return info;
}
exports.getInfo = getInfo;
/**
* Checks whether the hash needs to be recomputed.
*
* The hash should be recomputed if it does not use the given
* algorithm and options.
*
* @param {string} hash The hash to analyse.
* @param {integer} algo The algorithm to use.
* @param {options} options The options to use.
*
* @return {boolean} Whether the hash needs to be recomputed.
*/
function needsRehash(hash, algo, options) {
const info = getInfo(hash);
if (info.algorithm !== (algo || DEFAULT_ALGO)) {
return true;
}
const algoNeedsRehash = getAlgorithmFromId(info.id).needsRehash;
const result = algoNeedsRehash && algoNeedsRehash(hash, info);
if (typeof result === "boolean") {
return result;
}
const expected = Object.assign(
Object.create(null),
globalOptions[info.algorithm],
options,
);
const actual = info.options;
for (const prop in actual) {
const value = actual[prop];
if (typeof value === "number" && value < expected[prop]) {
return true;
}
}
return false;
}
exports.needsRehash = needsRehash;
/**
* Checks whether the password and the hash match.
*
* @param {string} password The password.
* @param {string} hash The hash.
* @param {function} callback Optional callback.
*
* @return {object} A promise which will receive a boolean.
*/
function verify(password, hash) {
return getAlgorithmFromHash(hash).verify(password, hash);
}
exports.verify = makeAsyncWrapper(verify);