UNPKG

7.55 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
81try {
82 (function(argon2) {
83 registerAlgorithm({
84 name: "argon2",
85 ids: ["argon2d", "argon2i"],
86 defaults: require("argon2").defaults,
87
88 getOptions: function(hash, info) {
89 let rawOptions = info.options;
90 let options = {};
91
92 // Since Argon2 1.3, the version number is encoded in the hash.
93 let version;
94 if (rawOptions.slice(0, 2) === "v=") {
95 version = +rawOptions.slice(2);
96
97 const index = hash.indexOf(rawOptions) + rawOptions.length + 1;
98 rawOptions = hash.slice(index, hash.indexOf("$", index));
99 }
100
101 rawOptions.split(",").forEach(function(datum) {
102 const index = datum.indexOf("=");
103 if (index === -1) {
104 options[datum] = true;
105 } else {
106 options[datum.slice(0, index)] = datum.slice(index + 1);
107 }
108 });
109
110 options = {
111 memoryCost: +options.m,
112 parallelism: +options.p,
113 timeCost: +options.t,
114 };
115 if (version !== undefined) {
116 options.version = version;
117 }
118 return options;
119 },
120 hash: argon2.hash,
121 verify: function(password, hash) {
122 return argon2.verify(hash, password);
123 },
124 });
125 })(require("argon2"));
126} catch (_) {}
127
128(function(bcrypt) {
129 registerAlgorithm({
130 name: "bcrypt",
131 ids: ["2", "2a", "2b", "2x", "2y"],
132 defaults: { cost: 10 },
133
134 getOptions: function(_, info) {
135 return {
136 cost: +info.options,
137 };
138 },
139 hash: function(password, options) {
140 return bcrypt.genSalt(options.cost).then(function(salt) {
141 return bcrypt.hash(password, salt);
142 });
143 },
144 needsRehash: function(_, info) {
145 const id = info.id;
146 if (id !== "2a" && id !== "2b" && id !== "2y") {
147 return true;
148 }
149
150 // Otherwise, let the default algorithm decides.
151 },
152 verify: function(password, hash) {
153 // See: https://github.com/ncb000gt/node.bcrypt.js/issues/175#issuecomment-26837823
154 if (hash.startsWith("$2y$")) {
155 hash = "$2a$" + hash.slice(4);
156 }
157
158 return bcrypt.compare(password, hash);
159 },
160 });
161})(promisifyAll(require("bcryptjs")));
162
163// -------------------------------------------------------------------
164
165const getHashInfo = (function(HASH_RE) {
166 return function getHashInfo(hash) {
167 const matches = hash.match(HASH_RE);
168 if (!matches) {
169 throw new Error("invalid hash " + hash);
170 }
171
172 return {
173 id: matches[1],
174 options: matches[2],
175 };
176 };
177})(/^\$([^$]+)\$([^$]*)\$/);
178
179function getAlgorithmByName(name) {
180 const algo = algorithmsByName[name];
181 if (!algo) {
182 throw new Error("no available algorithm with name " + name);
183 }
184
185 return algo;
186}
187
188function getAlgorithmFromId(id) {
189 const algo = algorithmsById[id];
190 if (!algo) {
191 throw new Error("no available algorithm with id " + id);
192 }
193
194 return algo;
195}
196
197function getAlgorithmFromHash(hash) {
198 return getAlgorithmFromId(getHashInfo(hash).id);
199}
200
201// ===================================================================
202
203/**
204 * Hashes a password.
205 *
206 * @param {string} password The password to hash.
207 * @param {integer} algo Identifier of the algorithm to use.
208 * @param {object} options Options for the algorithm.
209 * @param {function} callback Optional callback.
210 *
211 * @return {object} A promise which will receive the hashed password.
212 */
213function hash(password, algo, options) {
214 algo = getAlgorithmByName(algo || DEFAULT_ALGO);
215
216 return algo.hash(
217 password,
218 Object.assign(Object.create(null), globalOptions[algo.name], options)
219 );
220}
221exports.hash = makeAsyncWrapper(hash);
222
223/**
224 * Returns information about a hash.
225 *
226 * @param {string} hash The hash you want to get information from.
227 *
228 * @return {object} Object containing information about the given
229 * hash: “algorithm”: the algorithm used, “options” the options
230 * used.
231 */
232function getInfo(hash) {
233 const info = getHashInfo(hash);
234 const algo = getAlgorithmFromId(info.id);
235 info.algorithm = algo.name;
236 info.options = algo.getOptions(hash, info);
237
238 return info;
239}
240exports.getInfo = getInfo;
241
242/**
243 * Checks whether the hash needs to be recomputed.
244 *
245 * The hash should be recomputed if it does not use the given
246 * algorithm and options.
247 *
248 * @param {string} hash The hash to analyse.
249 * @param {integer} algo The algorithm to use.
250 * @param {options} options The options to use.
251 *
252 * @return {boolean} Whether the hash needs to be recomputed.
253 */
254function needsRehash(hash, algo, options) {
255 const info = getInfo(hash);
256
257 if (info.algorithm !== (algo || DEFAULT_ALGO)) {
258 return true;
259 }
260
261 const algoNeedsRehash = getAlgorithmFromId(info.id).needsRehash;
262 const result = algoNeedsRehash && algoNeedsRehash(hash, info);
263 if (typeof result === "boolean") {
264 return result;
265 }
266
267 const expected = Object.assign(
268 Object.create(null),
269 globalOptions[info.algorithm],
270 options
271 );
272 const actual = info.options;
273
274 for (const prop in actual) {
275 const value = actual[prop];
276 if (typeof value === "number" && value < expected[prop]) {
277 return true;
278 }
279 }
280
281 return false;
282}
283exports.needsRehash = needsRehash;
284
285/**
286 * Checks whether the password and the hash match.
287 *
288 * @param {string} password The password.
289 * @param {string} hash The hash.
290 * @param {function} callback Optional callback.
291 *
292 * @return {object} A promise which will receive a boolean.
293 */
294function verify(password, hash) {
295 return getAlgorithmFromHash(hash).verify(password, hash);
296}
297exports.verify = makeAsyncWrapper(verify);