Coverage

100%
316
316
0

/Users/sebastiansandqvist/Documents/Sites & Projects/apps/modules/s-salt-pepper/index.js

100%
48
48
0
LineHitsSource
11'use strict';
2
3// ----- dependencies
4// ---------------------------------------
51var crypto = require('crypto');
61var helpers = require('./src/helpers.js');
7
8
9// ----- exported object
10// ---------------------------------------
111var password = {};
12
13
14// ----- defaults
15// ---------------------------------------
161password.defaults = {
17 // ~ three fourths of actual hashLength
18 // (see: http://stackoverflow.com/questions/13378815/base64-length-calculation)
19 hashLength: 128,
20 iterations: [12000, 15000],
21 key: 'ENCRYPTION KEY',
22 unencryptedSaltMinLength: 32
23};
24
25// helper...
261var toStr = Object.prototype.toString;
27
28
29// ----- allow user to set config options
30// -- but restrict them to props in `password.defaults`
31// -- and make sure types are correct
32// ---------------------------------------
331password.configure = function(obj) {
34
3518 for (var prop in obj) {
3658 if (obj.hasOwnProperty(prop) && password.defaults.hasOwnProperty(prop)) {
3757 if (toStr.call(obj[prop]) !== toStr.call(password.defaults[prop])) {
384 throw(new Error(prop + ' must be of type ' + typeof password.defaults[prop]));
39 }
4053 password[prop] = obj[prop];
41 }
42 }
43
4414 return obj;
45
46};
47
48
49// ----- set defaults
50// ---------------------------------------
511password.configure(password.defaults);
52
53
54// --------------------------------- methods ---------------------------------
55
56// ----- hash password
57// -- @param input {string}
58// -- @param fn {function} callback
59// -- @return callback(err, salt, hash);
60// ---------------------------------------
611password.hash = function(input, fn) {
62
63
6425 if (toStr.call(input) !== '[object String]' || !input && toStr.call(fn) === '[object Function]') {
656 return fn(new TypeError('invalid input for hash method'));
66 }
67
6819 if (toStr.call(fn) !== '[object Function]' || toStr.call(input) !== '[object String]' || arguments.length !== 2) {
692 throw(new TypeError('hash method takes two parameters: input and callback'));
70 }
71
7217 helpers._random(password.iterations[0], password.iterations[1], function(err, iterations) {
73
7417 if (err) {
752 return fn(err);
76 }
77
7815 helpers._salt(iterations, password.unencryptedSaltMinLength, function(err, unencryptedSalt) {
79
8015 if (err) {
811 return fn(err);
82 }
83
8414 try {
8514 var salt = helpers._encrypt('aes256', password.key, unencryptedSalt);
86 }
87 catch(e) {
881 return fn(e);
89 }
90
9113 try {
92
9313 var hash = crypto.pbkdf2Sync(input, salt, iterations, password.hashLength)
94
9512 return fn(null, salt, hash.toString('base64'));
96
97 }
98 catch(e) {
991 return fn(e);
100 }
101
102 }); // end _salt
103
104 }); // end _random
105
106}; // end hash
107
108
109
110// ----- compare hashes
111// -- @param input {string}
112// -- @param salt {string}
113// -- @param fn {function} callback
114// -- @return callback(err, hash)
115// ---------------------------------------
1161password.compare = function(input, salt, fn) {
117
11818 if (toStr.call(input) !== '[object String]' || !input && toStr.call(fn) === '[object Function]') {
1192 return fn(new TypeError('invalid input for compare method'));
120 }
121
12216 if (toStr.call(salt) !== '[object String]' || !salt && toStr.call(fn) === '[object Function]') {
1233 return fn(new TypeError('invalid salt for compare method'));
124 }
125
12613 if (toStr.call(fn) !== '[object Function]' || toStr.call(input) !== '[object String]' || toStr.call(salt) !== '[object String]' || arguments.length !== 3) {
1271 throw(new TypeError('compare method takes three parameters: input, salt, and callback'));
128 }
129
13012 try {
131
13212 var decrypted = helpers._decrypt('aes256', password.key, salt);
13311 var iterations = helpers._getIterations(decrypted, password.unencryptedSaltMinLength);
134
13511 if (isNaN(iterations)) {
1361 return fn(new Error('could not get hash iterations'));
137 }
138
13910 var hash = crypto.pbkdf2Sync(input, salt, iterations, password.hashLength)
140
1419 return fn(null, hash.toString('base64'));
142
143 }
144 catch(e) {
1452 return fn(e);
146 }
147
148};
149
150
1511module.exports = password;

/Users/sebastiansandqvist/Documents/Sites & Projects/apps/modules/s-salt-pepper/src/helpers.js

100%
30
30
0
LineHitsSource
11'use strict';
2
3// ----- dependencies
4// ---------------------------------------
51var crypto = require('crypto');
6
7// ----- exported object
8// ---------------------------------------
91var helpers = {};
10
11
12// --------------------------------- methods ---------------------------------
13
14// ----- encrypt strings
15// -- @param algorithm {string}
16// -- @param key {string}
17// -- @param text {string}
18// -- @return encrypted {string}
19// ---------------------------------------
201helpers._encrypt = function(algorithm, key, text) {
21
2217 var cipher = crypto.createCipher(algorithm, key);
2316 var encrypted = cipher.update(text, 'utf8', 'base64');
24
2516 encrypted += cipher.final('base64');
26
2716 return encrypted;
28
29};
30
31
32// ----- decrypt strings
33// -- @param algorithm {string}
34// -- @param key {string}
35// -- @param text {string}
36// -- @return decrypted {string}
37// ---------------------------------------
381helpers._decrypt = function(algorithm, key, encrypted) {
39
4014 var decipher = crypto.createDecipher('aes256', key);
4113 var decrypted = decipher.update(encrypted, 'base64', 'utf8');
42
4313 decrypted += decipher.final('utf8');
44
4513 return decrypted;
46
47};
48
49
50// ----- random number in range
51// -- @param min {number}
52// -- @param max {number}
53// -- @param fn {function}
54// -- @return fn(err, number)
55// ---------------------------------------
561helpers._random = function(min, max, fn) {
57
5820 if (typeof min !== 'number' || typeof max !== 'number') {
592 return fn(new TypeError('numbers expected for min and max values of random number in range'));
60 }
61
6218 if (min > max) {
632 return fn(new Error('invalid min and max values for random number within range'));
64 }
65
6616 var randomNumber = Math.floor(Math.random() * (max - min + 1) + min);
67
6816 return fn(null, randomNumber);
69
70};
71
72
73
74// ----- unencrypted salt creation
75// -- @param iterations {number} pbkdf2 iterations concatenated to salt
76// -- @param len {number} length of salt before concatenation ^
77// -- @param fn {function} callback
78// -- @return callback(err, salt)
79// ---------------------------------------
801helpers._salt = function(iterations, len, fn) {
81
8220 try {
83
8420 var randomBytes = crypto.randomBytes(len);
85
8617 var salt = randomBytes
87 .toString('base64')
88 .substr(0, len) + iterations.toString();
89
9017 return fn(null, salt);
91
92 }
93 catch(e) {
943 return fn(e);
95 }
96
97};
98
99
100// ----- retrieve iterations concatenated to salt
101// -- @param salt {string}
102// -- @param len {number}
103// ---------------------------------------
1041helpers._getIterations = function(salt, len) {
105
10613 var iterations = salt.substr(len);
10713 return parseInt(iterations, 10);
108
109};
110
1111module.exports = helpers;

/Users/sebastiansandqvist/Documents/Sites & Projects/apps/modules/s-salt-pepper/test/public.js

100%
238
238
0
LineHitsSource
1// ----- dependencies
2// ---------------------------------------
31var expect = require('chai').expect;
41var password = require('../index.js');
51var helpers = require('../src/helpers.js');
6
7
8// ----- tests
9// ---------------------------------------
101describe('defaults', function() {
11
121 it('should have correct defaults', function() {
131 expect(password.defaults.hashLength).to.equal(128);
141 expect(password.defaults.iterations).to.deep.equal([12000, 15000]);
151 expect(password.defaults.key).to.equal('ENCRYPTION KEY');
161 expect(password.defaults.unencryptedSaltMinLength).to.equal(32);
17 });
18
191 it('should have correct defaults configured', function() {
201 expect(password.hashLength).to.equal(128);
211 expect(password.iterations).to.deep.equal([12000, 15000]);
221 expect(password.key).to.equal('ENCRYPTION KEY');
231 expect(password.unencryptedSaltMinLength).to.equal(32);
24 });
25
26});
27
281describe('configure', function() {
29
301 it('should be possible to change defaults', function() {
31
321 expect(password.configure).to.be.a.function;
33
341 password.configure({
35 hashLength: 256,
36 iterations: [12500, 13500],
37 key: 'test key',
38 unencryptedSaltMinLength: 64
39 });
40
411 expect(password.hashLength).to.equal(256);
421 expect(password.iterations).to.deep.equal([12500, 13500]);
431 expect(password.key).to.equal('test key');
441 expect(password.unencryptedSaltMinLength).to.equal(64);
45
46 });
47
481 it('should not be possible to create new keys in defaults', function() {
49
501 password.configure({
51 foo: 'bar',
52 key: 'test key 2'
53 });
54
551 expect(password.foo).to.be.undefined;
561 expect(password.key).to.equal('test key 2');
57
58 });
59
601 it('should not be possible to set defaults to wrong type', function() {
61
621 expect(function() {
631 password.configure({ hashLength: 'foo' });
64 }).to.throw();
65
661 expect(function() {
671 password.configure({ iterations: {} });
68 }).to.throw();
69
701 expect(function() {
711 password.configure({ key: 123 });
72 }).to.throw();
73
741 expect(function() {
751 password.configure({ unencryptedSaltMinLength: 'foo' });
76 }).to.throw();
77
78 });
79
80});
81
82
831describe('hash', function() {
84
851 it('should throw if missing parameters', function() {
86
871 expect(function() {
881 password.hash();
89 }).to.throw();
90
911 expect(function() {
921 password.hash('foo', 'bar', function() {});
93 }).to.throw();
94
95
96 });
97
981 it('should not throw for empty input', function() {
99
1001 expect(function() {
1011 password.hash(null, function() {});
102 }).to.not.throw();
103
1041 expect(function() {
1051 password.hash('', function() {});
106 }).to.not.throw();
107
108 });
109
1101 it('should return function with error for null input', function(done) {
1111 password.hash(null, function(err, salt, hash) {
1121 expect(err.message).to.include('invalid input');
1131 expect(salt).to.not.exist;
1141 expect(hash).to.not.exist;
1151 done();
116 });
117 });
118
1191 it('should return function with error for undefined input', function(done) {
1201 password.hash(undefined, function(err, salt, hash) {
1211 expect(err.message).to.include('invalid input');
1221 expect(salt).to.not.exist;
1231 expect(hash).to.not.exist;
1241 done();
125 });
126 });
127
1281 it('should return function with error for other input', function(done) {
1291 password.hash(12, function(err, salt, hash) {
1301 expect(err.message).to.include('invalid input');
1311 expect(salt).to.not.exist;
1321 expect(hash).to.not.exist;
1331 done();
134 });
135 });
136
1371 it('should return a hash and salt', function(done) {
1381 password.hash('foo', function(err, salt, hash) {
1391 expect(err).to.not.exist;
1401 expect(salt).to.be.a.string;
1411 expect(salt.length).to.be.at.least(32);
1421 expect(hash).to.be.a.string;
1431 expect(hash).to.not.equal('foo');
1441 expect(hash.length).to.be.at.least(128);
1451 done();
146 });
147 });
148
1491 it('should return a salt that includes iteration count in range', function(done) {
150
1511 password.hash('foo', function(err, salt, hash) {
1521 var unencryptedSalt = helpers._decrypt('aes256', password.key, salt);
1531 var iterations = helpers._getIterations(unencryptedSalt, password.unencryptedSaltMinLength);
1541 expect(iterations).to.be.a.number;
1551 expect(iterations).to.be.at.least(password.iterations[0]);
1561 expect(iterations).to.be.at.most(password.iterations[1]);
1571 done();
158 });
159
160 });
161
1621 it('should return an error if iterations are incorrect', function(done) {
1631 password.iterations = [100, 50];
1641 password.hash('foo', function(err, salt, hash) {
1651 expect(err.message).to.include('invalid min and max values');
1661 expect(salt).to.not.exist;
1671 expect(hash).to.not.exist;
1681 done();
169 });
170 });
171
1721 it('should return an error if iterations are not numbers', function(done) {
1731 password.iterations = [undefined, 'foo'];
1741 password.hash('foo', function(err, salt, hash) {
1751 expect(err.message).to.include('min and max values');
1761 expect(salt).to.not.exist;
1771 expect(hash).to.not.exist;
1781 done();
179 });
180 });
181
1821 it('should return an error if randomBytes fails in helpers._salt', function(done) {
1831 password.iterations = [12000, 15000];
1841 password.unencryptedSaltMinLength = null;
1851 password.hash('foo', function(err, salt, hash) {
1861 expect(err.message).exist;
1871 expect(salt).to.not.exist;
1881 expect(hash).to.not.exist;
1891 done()
190 });
191 });
192
1931 it('should return an error if helpers._encrypt fails', function(done) {
1941 password.unencryptedSaltMinLength = 32;
1951 password.key = 12;
1961 password.hash('foo', function(err, salt, hash) {
1971 expect(err.message).to.include('key');
1981 expect(salt).to.not.exist;
1991 expect(hash).to.not.exist;
2001 done();
201 });
202 });
203
2041 it('should return an error if pbkdf2 fails', function(done) {
2051 password.configure({
206 hashLength: 128,
207 iterations: [12000, 15000],
208 key: 'ENCRYPTION KEY',
209 unencryptedSaltMinLength: 32
210 });
2111 password.hashLength = null;
2121 password.hash('foo', function(err, salt, hash) {
2131 expect(err.message).to.include('Key length not a number');
2141 expect(salt).to.not.exist;
2151 expect(hash).to.not.exist;
2161 done();
217 });
218 });
219
2201 it('should throw if no callback', function() {
2211 expect(function() {
2221 password.hash('input', null);
223 }).to.throw();
224 });
225
226});
227
2281describe('compare', function() {
229
2301 it('should return an error if not given an input', function(done) {
2311 password.compare(null, 'salt', function(err, hash) {
2321 expect(err.message).to.include('invalid input');
2331 expect(hash).to.not.exist;
2341 done();
235 });
236 });
237
2381 it('should return an error if not given a salt', function(done) {
2391 password.compare('input', null, function(err, hash) {
2401 expect(err.message).to.include('invalid salt');
2411 expect(hash).to.not.exist;
2421 done();
243 });
244 });
245
2461 it ('should return an error if input is empty string', function(done) {
2471 password.compare('', 'salt', function(err, hash) {
2481 expect(err.message).to.include('invalid input');
2491 expect(hash).to.not.exist;
2501 done();
251 });
252 });
253
2541 it ('should return an error if salt is empty string', function(done) {
2551 password.compare('input', '', function(err, hash) {
2561 expect(err.message).to.include('invalid salt');
2571 expect(hash).to.not.exist;
2581 done();
259 });
260 });
261
2621 it('should throw if no callback', function() {
2631 expect(function() {
2641 password.compare('input', 'salt', null);
265 }).to.throw();
266 });
267
2681 it('should throw if no input', function() {
2691 expect(function() {
2701 password.compare('salt', function(err, hash) {});
271 }).to.throw();
272 });
273
2741 it('should return an error if decryption fails', function(done) {
2751 password.key = null;
2761 password.compare('input', 'salt', function(err, hash) {
2771 expect(err.message).to.include('cipher');
2781 expect(hash).to.not.exist;
2791 done();
280 });
281 });
282
2831 it('should return an error if getIterations fails', function(done) {
2841 var test = helpers._encrypt('aes256', 'encryption_key', 'test');
2851 password.key = 'encryption_key';
2861 password.compare('input', test, function(err, hash) {
2871 expect(err.message).to.include('iterations');
2881 expect(hash).to.not.exist;
2891 done();
290 });
291 });
292
2931 it('should hash correctly', function(done) {
2941 password.configure({
295 hashLength: 128,
296 iterations: [12000, 15000],
297 key: 'ENCRYPTION KEY',
298 unencryptedSaltMinLength: 32
299 });
3001 password.hash('test', function(err, salt, hash) {
3011 password.compare('test', salt, function(err, hash2) {
3021 expect(err).to.not.exist;
3031 expect(hash2).to.equal(hash);
3041 done();
305 });
306 });
307 });
308
3091 it('should return an error if pbkdf2 fails', function(done) {
3101 password.configure({
311 hashLength: 128,
312 iterations: [12000, 15000],
313 key: 'ENCRYPTION KEY',
314 unencryptedSaltMinLength: 32
315 });
3161 password.hash('test', function(err, salt, hash) {
3171 password.hashLength = null;
3181 password.compare('test', salt, function(err, hash2) {
3191 expect(err.message).to.include('length');
3201 expect(hash2).to.not.exist;
3211 done();
322 });
323 });
324 });
325
326});
327
3281describe('using other minLengths', function() {
329
3301 it('should work with a minLength of 0 (but... this is still bad)', function(done) {
3311 password.configure({
332 hashLength: 128,
333 iterations: [12000, 15000],
334 key: 'ENCRYPTION KEY',
335 unencryptedSaltMinLength: 0
336 });
337
3381 password.hash('test', function(err, salt, hash) {
3391 password.compare('test', salt, function(err, hash2) {
3401 expect(err).to.not.exist;
3411 expect(hash2).to.equal(hash);
3421 done();
343 });
344 });
345
346 });
347
3481 it('should work with a high minLength', function(done) {
3491 password.configure({
350 hashLength: 128,
351 iterations: [10, 20],
352 key: 'ENCRYPTION KEY',
353 unencryptedSaltMinLength: 1000000
354 });
3551 password.hash('test', function(err, salt, hash) {
3561 password.compare('test', salt, function(err, hash2) {
3571 expect(err).to.not.exist;
3581 expect(hash2).to.equal(hash);
3591 done();
360 });
361 });
362
363 });
364
365});
366
3671describe('using other hashLengths', function() {
368
3691 it('should work with a hashLength of 0 (but... this is still bad)', function(done) {
3701 password.configure({
371 hashLength: 0,
372 iterations: [12000, 15000],
373 key: 'ENCRYPTION KEY',
374 unencryptedSaltMinLength: 32
375 });
3761 password.hash('test', function(err, salt, hash) {
3771 password.compare('test', salt, function(err, hash2) {
3781 expect(err).to.not.exist;
3791 expect(hash2).to.equal(hash);
3801 expect(hash).to.equal('');
3811 done();
382 });
383 });
384 });
385
3861 it('should work with a high hashLength', function(done) {
3871 password.configure({
388 hashLength: 1000,
389 iterations: [100, 200],
390 key: 'ENCRYPTION KEY',
391 unencryptedSaltMinLength: 32
392 });
3931 password.hash('test', function(err, salt, hash) {
3941 password.compare('test', salt, function(err, hash2) {
3951 expect(err).to.not.exist;
3961 expect(hash2).to.equal(hash);
3971 done();
398 });
399 });
400 });
401
402});
403
4041describe('using other encryption keys', function() {
405
4061 it('should work with symbols in the key', function(done) {
4071 password.configure({
408 hashLength: 128,
409 iterations: [100, 200],
410 key: '~!@#$%^&*()_+`1234567890-=\'][{}|,./<>?"asd"',
411 unencryptedSaltMinLength: 32
412 });
4131 password.hash('test', function(err, salt, hash) {
4141 password.compare('test', salt, function(err, hash2) {
4151 expect(err).to.not.exist;
4161 expect(hash2).to.equal(hash);
4171 done();
418 });
419 });
420 });
421
4221 it('should work with UTF-8 characters in the key', function(done) {
4231 password.configure({
424 hashLength: 128,
425 iterations: [100, 200],
426 key: '° © ® ™ • ½ ¼ ¾ ⅓ ⅔ † ‡ µ ¢ £ € « » ♤ ♧ ♥ ♢ ¿ � 汉语 漢語 华语 華語 中文',
427 unencryptedSaltMinLength: 32
428 });
4291 password.hash('test', function(err, salt, hash) {
4301 password.compare('test', salt, function(err, hash2) {
4311 expect(err).to.not.exist;
4321 expect(hash2).to.equal(hash);
4331 done();
434 });
435 });
436 });
437
4381 it('should work with a short key', function(done) {
4391 password.configure({
440 hashLength: 128,
441 iterations: [100, 200],
442 key: '0',
443 unencryptedSaltMinLength: 32
444 });
4451 password.hash('test', function(err, salt, hash) {
4461 password.compare('test', salt, function(err, hash2) {
4471 expect(err).to.not.exist;
4481 expect(hash2).to.equal(hash);
4491 done();
450 });
451 });
452 });
453
4541 it('should work with a long key', function(done) {
4551 var key = new Array(10000).join('test ');
4561 password.configure({
457 hashLength: 128,
458 iterations: [100, 200],
459 key: key,
460 unencryptedSaltMinLength: 32
461 });
4621 password.hash('test', function(err, salt, hash) {
4631 password.compare('test', salt, function(err, hash2) {
4641 expect(err).to.not.exist;
4651 expect(hash2).to.equal(hash);
4661 done();
467 });
468 });
469 });
470
471});