UNPKG

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