UNPKG

8.18 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 registerAlgorithm({
160 name: 'argon2',
161 ids: [ 'argon2d', 'argon2i' ],
162 defaults: require('argon2').defaults,
163
164 getOptions: function (hash, info) {
165 var rawOptions = info.options
166 var options = {}
167
168 // Since Argon2 1.3, the version number is encoded in the hash.
169 var version
170 if (rawOptions.slice(0, 2) === 'v=') {
171 version = +rawOptions.slice(2)
172
173 var index = hash.indexOf(rawOptions) + rawOptions.length + 1
174 rawOptions = hash.slice(index, hash.indexOf('$', index))
175 }
176
177 rawOptions.split(',').forEach(function (datum) {
178 var index = datum.indexOf('=')
179 if (index === -1) {
180 options[datum] = true
181 } else {
182 options[datum.slice(0, index)] = datum.slice(index + 1)
183 }
184 })
185
186 options = {
187 memoryCost: +options.m,
188 parallelism: +options.p,
189 timeCost: +options.t
190 }
191 if (version !== undefined) {
192 options.version = version
193 }
194 return options
195 },
196 hash: argon2.hash,
197 verify: function (password, hash) {
198 return argon2.verify(hash, password)
199 }
200 })
201 })(require('argon2'))
202} catch (_) {}
203
204// -------------------------------------------------------------------
205
206var getHashInfo = (function (HASH_RE) {
207 return function getHashInfo (hash) {
208 var matches = hash.match(HASH_RE)
209 if (!matches) {
210 throw new Error('invalid hash ' + hash)
211 }
212
213 return {
214 id: matches[1],
215 options: matches[2]
216 }
217 }
218})(/^\$([^$]+)\$([^$]*)\$/)
219
220function getAlgorithmByName (name) {
221 var algo = algorithmsByName[name]
222 if (!algo) {
223 throw new Error('no available algorithm with name ' + name)
224 }
225
226 return algo
227}
228
229function getAlgorithmFromId (id) {
230 var algo = algorithmsById[id]
231 if (!algo) {
232 throw new Error('no available algorithm with id ' + id)
233 }
234
235 return algo
236}
237
238function getAlgorithmFromHash (hash) {
239 return getAlgorithmFromId(getHashInfo(hash).id)
240}
241
242// ===================================================================
243
244/**
245 * Hashes a password.
246 *
247 * @param {string} password The password to hash.
248 * @param {integer} algo Identifier of the algorithm to use.
249 * @param {object} options Options for the algorithm.
250 * @param {function} callback Optional callback.
251 *
252 * @return {object} A promise which will receive the hashed password.
253 */
254function hash (password, algo, options) {
255 algo = getAlgorithmByName(algo || DEFAULT_ALGO)
256
257 return algo.hash(
258 password,
259 assign(Object.create(null), globalOptions[algo.name], options)
260 )
261}
262exports.hash = makeAsyncWrapper(hash)
263
264/**
265 * Returns information about a hash.
266 *
267 * @param {string} hash The hash you want to get information from.
268 *
269 * @return {object} Object containing information about the given
270 * hash: “algorithm”: the algorithm used, “options” the options
271 * used.
272 */
273function getInfo (hash) {
274 var info = getHashInfo(hash)
275 var algo = getAlgorithmFromId(info.id)
276 info.algorithm = algo.name
277 info.options = algo.getOptions(hash, info)
278
279 return info
280}
281exports.getInfo = getInfo
282
283/**
284 * Checks whether the hash needs to be recomputed.
285 *
286 * The hash should be recomputed if it does not use the given
287 * algorithm and options.
288 *
289 * @param {string} hash The hash to analyse.
290 * @param {integer} algo The algorithm to use.
291 * @param {options} options The options to use.
292 *
293 * @return {boolean} Whether the hash needs to be recomputed.
294 */
295function needsRehash (hash, algo, options) {
296 var info = getInfo(hash)
297
298 if (info.algorithm !== (algo || DEFAULT_ALGO)) {
299 return true
300 }
301
302 var algoNeedsRehash = getAlgorithmFromId(info.id).needsRehash
303 var result = algoNeedsRehash && algoNeedsRehash(hash, info)
304 if (typeof result === 'boolean') {
305 return result
306 }
307
308 var expected = assign(Object.create(null), globalOptions[info.algorithm], options)
309 var actual = info.options
310
311 for (var prop in actual) {
312 var value = actual[prop]
313 if (
314 typeof value === 'number' &&
315 value < expected[prop]
316 ) {
317 return true
318 }
319 }
320
321 return false
322}
323exports.needsRehash = needsRehash
324
325/**
326 * Checks whether the password and the hash match.
327 *
328 * @param {string} password The password.
329 * @param {string} hash The hash.
330 * @param {function} callback Optional callback.
331 *
332 * @return {object} A promise which will receive a boolean.
333 */
334function verify (password, hash) {
335 return getAlgorithmFromHash(hash).verify(password, hash)
336}
337exports.verify = makeAsyncWrapper(verify)