1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | "use strict";
|
8 |
|
9 |
|
10 |
|
11 | const promiseToolbox = require("promise-toolbox");
|
12 |
|
13 | const asCallback = promiseToolbox.asCallback;
|
14 | const promisifyAll = promiseToolbox.promisifyAll;
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | const 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 |
|
43 | const algorithmsById = Object.create(null);
|
44 | const algorithmsByName = Object.create(null);
|
45 |
|
46 | const globalOptions = Object.create(null);
|
47 | exports.options = globalOptions;
|
48 |
|
49 | let DEFAULT_ALGO;
|
50 | Object.defineProperty(exports, "DEFAULT_ALGO", {
|
51 | enumerable: true,
|
52 | get: function() {
|
53 | return DEFAULT_ALGO;
|
54 | },
|
55 | });
|
56 |
|
57 | function 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 |
|
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 |
|
149 | },
|
150 | verify: function(password, hash) {
|
151 |
|
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 |
|
163 | const 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 |
|
177 | function 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 |
|
186 | function 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 |
|
195 | function getAlgorithmFromHash(hash) {
|
196 | return getAlgorithmFromId(getHashInfo(hash).id);
|
197 | }
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 | function 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 | }
|
219 | exports.hash = makeAsyncWrapper(hash);
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 | function 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 | }
|
238 | exports.getInfo = getInfo;
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 | function 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 | }
|
281 | exports.needsRehash = needsRehash;
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 | function verify(password, hash) {
|
293 | return getAlgorithmFromHash(hash).verify(password, hash);
|
294 | }
|
295 | exports.verify = makeAsyncWrapper(verify);
|