UNPKG

7.45 kBJavaScriptView Raw
1/**
2 * This file is part of Hashy which is released under the MIT license.
3 *
4 * @author Julien Fontanet <julien.fontanet@isonoe.net>
5 */
6
7"use strict";
8
9// ===================================================================
10
11const promiseToolbox = require("promise-toolbox");
12
13const asCallback = promiseToolbox.asCallback;
14const promisifyAll = promiseToolbox.promisifyAll;
15
16// ===================================================================
17
18// Similar to Bluebird.method(fn) but handle Node callbacks.
19const makeAsyncWrapper = (function(push) {
20 return function makeAsyncWrapper(fn) {
21 return function asyncWrapper() {
22 const args = [];
23 push.apply(args, arguments);
24 let callback;
25
26 const n = args.length;
27 if (n && typeof args[n - 1] === "function") {
28 callback = args.pop();
29 }
30
31 return asCallback.call(
32 new Promise(function(resolve) {
33 resolve(fn.apply(this, args));
34 }),
35 callback
36 );
37 };
38 };
39})(Array.prototype.push);
40
41// ===================================================================
42
43const algorithmsById = Object.create(null);
44const algorithmsByName = Object.create(null);
45
46const globalOptions = Object.create(null);
47exports.options = globalOptions;
48
49let DEFAULT_ALGO;
50Object.defineProperty(exports, "DEFAULT_ALGO", {
51 enumerable: true,
52 get: function() {
53 return DEFAULT_ALGO;
54 },
55});
56
57function registerAlgorithm(algo) {
58 const name = algo.name;
59
60 if (algorithmsByName[name]) {
61 throw new Error("name " + name + " already taken");
62 }
63 algorithmsByName[name] = algo;
64
65 algo.ids.forEach(function(id) {
66 if (algorithmsById[id]) {
67 throw new Error("id " + id + " already taken");
68 }
69 algorithmsById[id] = algo;
70 });
71
72 globalOptions[name] = Object.assign(Object.create(null), algo.defaults);
73
74 if (!DEFAULT_ALGO) {
75 DEFAULT_ALGO = name;
76 }
77}
78
79// -------------------------------------------------------------------
80
81(function(argon2) {
82 registerAlgorithm({
83 name: "argon2",
84 ids: ["argon2d", "argon2i"],
85 defaults: require("argon2").defaults,
86
87 getOptions: function(hash, info) {
88 let rawOptions = info.options;
89 let options = {};
90
91 // Since Argon2 1.3, the version number is encoded in the hash.
92 let version;
93 if (rawOptions.slice(0, 2) === "v=") {
94 version = +rawOptions.slice(2);
95
96 const index = hash.indexOf(rawOptions) + rawOptions.length + 1;
97 rawOptions = hash.slice(index, hash.indexOf("$", index));
98 }
99
100 rawOptions.split(",").forEach(function(datum) {
101 const index = datum.indexOf("=");
102 if (index === -1) {
103 options[datum] = true;
104 } else {
105 options[datum.slice(0, index)] = datum.slice(index + 1);
106 }
107 });
108
109 options = {
110 memoryCost: +options.m,
111 parallelism: +options.p,
112 timeCost: +options.t,
113 };
114 if (version !== undefined) {
115 options.version = version;
116 }
117 return options;
118 },
119 hash: argon2.hash,
120 verify: function(password, hash) {
121 return argon2.verify(hash, password);
122 },
123 });
124})(require("argon2"));
125
126(function(bcrypt) {
127 registerAlgorithm({
128 name: "bcrypt",
129 ids: ["2", "2a", "2b", "2x", "2y"],
130 defaults: { cost: 10 },
131
132 getOptions: function(_, info) {
133 return {
134 cost: +info.options,
135 };
136 },
137 hash: function(password, options) {
138 return bcrypt.genSalt(options.cost).then(function(salt) {
139 return bcrypt.hash(password, salt);
140 });
141 },
142 needsRehash: function(_, info) {
143 const id = info.id;
144 if (id !== "2a" && id !== "2b" && id !== "2y") {
145 return true;
146 }
147
148 // Otherwise, let the default algorithm decides.
149 },
150 verify: function(password, hash) {
151 // See: https://github.com/ncb000gt/node.bcrypt.js/issues/175#issuecomment-26837823
152 if (hash.startsWith("$2y$")) {
153 hash = "$2a$" + hash.slice(4);
154 }
155
156 return bcrypt.compare(password, hash);
157 },
158 });
159})(promisifyAll(require("bcryptjs")));
160
161// -------------------------------------------------------------------
162
163const getHashInfo = (function(HASH_RE) {
164 return function getHashInfo(hash) {
165 const matches = hash.match(HASH_RE);
166 if (!matches) {
167 throw new Error("invalid hash " + hash);
168 }
169
170 return {
171 id: matches[1],
172 options: matches[2],
173 };
174 };
175})(/^\$([^$]+)\$([^$]*)\$/);
176
177function getAlgorithmByName(name) {
178 const algo = algorithmsByName[name];
179 if (!algo) {
180 throw new Error("no available algorithm with name " + name);
181 }
182
183 return algo;
184}
185
186function getAlgorithmFromId(id) {
187 const algo = algorithmsById[id];
188 if (!algo) {
189 throw new Error("no available algorithm with id " + id);
190 }
191
192 return algo;
193}
194
195function getAlgorithmFromHash(hash) {
196 return getAlgorithmFromId(getHashInfo(hash).id);
197}
198
199// ===================================================================
200
201/**
202 * Hashes a password.
203 *
204 * @param {string} password The password to hash.
205 * @param {integer} algo Identifier of the algorithm to use.
206 * @param {object} options Options for the algorithm.
207 * @param {function} callback Optional callback.
208 *
209 * @return {object} A promise which will receive the hashed password.
210 */
211function hash(password, algo, options) {
212 algo = getAlgorithmByName(algo || DEFAULT_ALGO);
213
214 return algo.hash(
215 password,
216 Object.assign(Object.create(null), globalOptions[algo.name], options)
217 );
218}
219exports.hash = makeAsyncWrapper(hash);
220
221/**
222 * Returns information about a hash.
223 *
224 * @param {string} hash The hash you want to get information from.
225 *
226 * @return {object} Object containing information about the given
227 * hash: “algorithm”: the algorithm used, “options” the options
228 * used.
229 */
230function getInfo(hash) {
231 const info = getHashInfo(hash);
232 const algo = getAlgorithmFromId(info.id);
233 info.algorithm = algo.name;
234 info.options = algo.getOptions(hash, info);
235
236 return info;
237}
238exports.getInfo = getInfo;
239
240/**
241 * Checks whether the hash needs to be recomputed.
242 *
243 * The hash should be recomputed if it does not use the given
244 * algorithm and options.
245 *
246 * @param {string} hash The hash to analyse.
247 * @param {integer} algo The algorithm to use.
248 * @param {options} options The options to use.
249 *
250 * @return {boolean} Whether the hash needs to be recomputed.
251 */
252function needsRehash(hash, algo, options) {
253 const info = getInfo(hash);
254
255 if (info.algorithm !== (algo || DEFAULT_ALGO)) {
256 return true;
257 }
258
259 const algoNeedsRehash = getAlgorithmFromId(info.id).needsRehash;
260 const result = algoNeedsRehash && algoNeedsRehash(hash, info);
261 if (typeof result === "boolean") {
262 return result;
263 }
264
265 const expected = Object.assign(
266 Object.create(null),
267 globalOptions[info.algorithm],
268 options
269 );
270 const actual = info.options;
271
272 for (const prop in actual) {
273 const value = actual[prop];
274 if (typeof value === "number" && value < expected[prop]) {
275 return true;
276 }
277 }
278
279 return false;
280}
281exports.needsRehash = needsRehash;
282
283/**
284 * Checks whether the password and the hash match.
285 *
286 * @param {string} password The password.
287 * @param {string} hash The hash.
288 * @param {function} callback Optional callback.
289 *
290 * @return {object} A promise which will receive a boolean.
291 */
292function verify(password, hash) {
293 return getAlgorithmFromHash(hash).verify(password, hash);
294}
295exports.verify = makeAsyncWrapper(verify);