-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy patheasy-pbkdf2.js
271 lines (244 loc) · 7.91 KB
/
easy-pbkdf2.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
var crypto = require("crypto"),
isPlainObject = require("lodash.isplainobject"),
each = require("lodash.foreach"),
isFunction = require("lodash.isfunction"),
isString = require("lodash.isstring");
var EasyPbkdf2 = module.exports = function( options ) {
if( !( this instanceof EasyPbkdf2 ) ){
return new EasyPbkdf2( options );
}
if ( isPlainObject( options ) ) {
each( options, function( value, key ){
this[ key ] = value;
}.bind(this));
}
};
EasyPbkdf2.prototype = {
/**
* @constant The default number of iterations used by the hash method.
*/
"DEFAULT_HASH_ITERATIONS": 512,
/**
* @default Salt sizes throughout the system
*/
"SALT_SIZE": 256/8,
/**
* @default The length of the key to derive when hashing
*/
"KEY_LENGTH": 256,
/**
* @default The maximum length of the password to accept for hashing
*/
"MAX_PASSWORD_LENGTH": 4096,
/**
* Cranks out a collision resistant hash, relatively quickly.
*
* Not suitable for passwords, or sensitive information.
*
* Synchronous
*
* @param {String|Object} value The data to hash. The value is converted to
* a string via JSON.stringify(). Do NOT pass a function.
* @returns {String} Base64 encoded sha1 hash of `value`
*/
"weakHash": function( value ) {
var hasher = crypto.createHash("sha1"),
bytes = value != null ? Buffer.from( JSON.stringify( value ), "utf8" ) : Buffer.allocUnsafe(0);
hasher.update( bytes, "binary" );
return hasher.digest("base64");
},
/**
* Cranks out a secure hash with a specific salt.
*
* Asynchronous
*
* @param {String} value
* @param {String} salt (optional)
* @param {Function} callback
*/
"secureHash": function(){
return this.hash.apply( this, arguments );
},
/**
* Universal random provider. Generates cryptographically strong pseudo-random data.
*
* Synchronous or Asynchronous
*
* @param {Number} bytes
* @param {Function=} callback (optional)
* @returns {SlowBuffer} (optional)
*/
"random": function( bytes, callback ) {
if ( isFunction( callback ) ) {
crypto.randomBytes( bytes, function( err, buffer ) {
if ( err ) {
console.log( err );
}
callback.call( this, buffer );
});
}
else {
try {
var buffer = crypto.randomBytes( bytes );
return buffer;
}
catch ( err ) {
return null;
}
}
},
/**
* Convenience wrapper around .random to grab a new salt value.
* Treat this value as opaque, as it captures iterations.
*
* Synchronous or Asynchronous
*
* @param {Number=} explicitIterations An integer (optional)
* @param {Function=} callback (optional)
* @returns {String} Return iterations and salt together as one string ({hex-iterations}.{base64-salt}) (optional)
*/
"generateSalt": function( explicitIterations, callback ) {
var defaultHashIterations = this.DEFAULT_HASH_ITERATIONS,
saltSize = this.SALT_SIZE;
if ( !callback && isFunction( explicitIterations ) ) {
callback = explicitIterations;
explicitIterations = null;
}
if ( explicitIterations != null ) {
// make sure explicitIterations is an integer
var explicitIterationsInt = parseInt( explicitIterations, 10 );
if ( explicitIterationsInt != explicitIterations || isNaN( explicitIterationsInt ) ) {
throw new Error("explicitIterations must be an integer");
}
explicitIterations = explicitIterationsInt;
// and that it is not smaller than our default hash iterations
if ( explicitIterations < defaultHashIterations ) {
throw new Error( "explicitIterations cannot be less than " + defaultHashIterations );
}
}
// convert iterations to Hexadecimal
var iterations = ( explicitIterations || defaultHashIterations ).toString( 16 );
// get some random bytes
if ( isFunction( callback ) ) {
this.random( saltSize, function( bytes ) {
callback( concat( bytes ) );
});
}
else {
var bytes = this.random( saltSize );
return concat( bytes );
}
function concat ( bytes ) {
// concat the iterations and random bytes together.
var base64 = binaryToBase64( bytes );
return iterations + "." + base64;
}
},
/**
* Backs Secure hashes.
*
* Uses PBKDF2 internally, as implemented by the node's native crypto library.
*
* See http://en.wikipedia.org/wiki/PBKDF2
* and http://code.google.com/p/crypto-js/
*
* If the salt param is omitted, generates salt automatically
*
* Asynchronous
*
* @param {String} value MUST be a string, unless, of course, you want to explode.
* @param {String} salt (should include iterations). (optional)
* @param {Function} callback fn( err, {String} A secure hash (base64 encoded), salt w/ iterations )
*/
"hash": function( value, salt, callback ) {
// if salt was not supplied, generate it now.
if ( isFunction( salt ) || salt == null ) {
callback = callback || salt;
salt = this.generateSalt();
}
if ( !isFunction( callback ) ) {
throw new Error("callback is required (as Function)");
}
if ( !value || typeof value !== "string" ) {
callback(new Error("value is required (as String)"));
return;
}
if ( value.length > this.MAX_PASSWORD_LENGTH ) {
callback(new Error("Password exceeds maximum length of " + this.MAX_PASSWORD_LENGTH));
return;
}
var keySize = this.KEY_LENGTH,
i = (salt).indexOf("."),
iterations = parseInt( salt.substring( 0, i ), 16 );
crypto.pbkdf2( value, salt.substring( i + 1 ), iterations, keySize, "sha1", function( err, derivedKey ) {
var base64;
if ( !err ) {
base64 = binaryToBase64( derivedKey );
}
callback( err, base64, salt );
});
},
/**
* Verify that a plaintext value matches the given hash
* by hashing the value using the provided salt then comparing the two hashes
* using constant-time string comparison to prevent timing attacks.
*
* Asynchronous
*
* @param {String} salt The salt used to hash to `priorHash`.
* @param {String} priorHash The base64 encoded hash previously generated by the hash method.
* @param {String} value A plaintext string to compare against the `priorHash`.
* @param {Function} callback fn( err, {Boolean} True if the `value` matches the `priorHash`, false if not.
*/
"verify": function( salt, priorHash, value, callback ) {
// calculate the original key length by checking the binary length of the base64 encoded priorHash
var keyLength,
easyPbkdf2;
if ( !priorHash || !isString( priorHash ) ) {
callback( new Error("priorHash is required (as String)") );
return;
}
keyLength = base64toBinary( priorHash ).length;
easyPbkdf2 = new EasyPbkdf2({ "KEY_LENGTH": keyLength, "MAX_PASSWORD_LENGTH": this.MAX_PASSWORD_LENGTH });
easyPbkdf2.hash( value, salt, function( err, valueHash ) {
var valid;
if ( !err ) {
valid = constantTimeStringCompare( priorHash, valueHash );
}
callback( err, valid );
});
}
};
EasyPbkdf2.EasyPbkdf2 = EasyPbkdf2;
/**
* This method performs a constant-time (relevant to `constStr` only!) string equality check that can be used to prevent
* timing attacks when comparing sensitive data.
*
* This method does not perform in constant-time when the variableStr is an empty string.
*
* @param {String} constStr The comparison string that this the constant-time function should be relative to.
* @param {String} variableStr The string to check for equality
* @returns {Boolean} True if the strings are equal. False if not.
*/
function constantTimeStringCompare( constStr, variableStr ) {
with ( Object.create({}) ) { // disables compiler optimizations
var aLength = constStr.length,
bLength = variableStr.length,
match = aLength === bLength ? 1 : 0,
i = aLength;
while ( i-- ) {
var aChar = constStr.charCodeAt( i % aLength ),
bChar = variableStr.charCodeAt( i % bLength ),
equ = aChar === bChar,
asInt = equ ? 1 : 0;
match = match & equ;
}
return match === 1;
}
}
function binaryToBase64( binary ){
return Buffer.from( binary, "binary" ).toString("base64");
}
function base64toBinary( base64 ){
return Buffer.from( base64, "base64" ).toString("binary");
}