Skip to content

Commit 2488473

Browse files
recrsndefunctzombie
authored andcommitted
Add support for Promises (#458)
Return a Promise when a callback is not provided.
1 parent 27b3b74 commit 2488473

File tree

4 files changed

+280
-12
lines changed

4 files changed

+280
-12
lines changed

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,26 @@ bcrypt.compare(someOtherPlaintextPassword, hash, function(err, res) {
102102
// res == false
103103
});
104104
```
105+
### with promises
105106

107+
bcrypt uses whatever Promise implementation is available in `global.Promise`. NodeJS >= 0.12 has a native Promise implementation built in. However, this should work in any Promises/A+ compilant implementation.
108+
109+
Async methods that accept a callback, return a `Promise` when callback is not specified if Promise support is available.
110+
111+
```javascript
112+
bcrypt.hash(myPlaintextPassword, saltRounds).then(function(hash) {
113+
// Store hash in your password DB.
114+
});
115+
```
116+
```javascript
117+
// Load hash from your password DB.
118+
bcrypt.compare(myPlaintextPassword, hash).then(function(res) {
119+
// res == true
120+
});
121+
bcrypt.compare(someOtherPlaintextPassword, hash).then(function(res) {
122+
// res == false
123+
});
124+
```
106125

107126
### sync
108127

@@ -151,7 +170,7 @@ If you are using bcrypt on a simple script, using the sync mode is perfectly fin
151170
* `rounds` - [OPTIONAL] - the cost of processing the data. (default - 10)
152171
* `genSalt(rounds, cb)`
153172
* `rounds` - [OPTIONAL] - the cost of processing the data. (default - 10)
154-
* `cb` - [REQUIRED] - a callback to be fired once the salt has been generated. uses eio making it asynchronous.
173+
* `cb` - [OPTIONAL] - a callback to be fired once the salt has been generated. uses eio making it asynchronous. If `cb` is not specified, a `Promise` is returned if Promise support is available.
155174
* `err` - First parameter to the callback detailing any errors.
156175
* `salt` - Second parameter to the callback providing the generated salt.
157176
* `hashSync(data, salt)`
@@ -160,7 +179,7 @@ If you are using bcrypt on a simple script, using the sync mode is perfectly fin
160179
* `hash(data, salt, cb)`
161180
* `data` - [REQUIRED] - the data to be encrypted.
162181
* `salt` - [REQUIRED] - the salt to be used to hash the password. if specified as a number then a salt will be generated with the specified number of rounds and used (see example under **Usage**).
163-
* `cb` - [REQUIRED] - a callback to be fired once the data has been encrypted. uses eio making it asynchronous.
182+
* `cb` - [OPTIONAL] - a callback to be fired once the data has been encrypted. uses eio making it asynchronous. If `cb` is not specified, a `Promise` is returned if Promise support is available.
164183
* `err` - First parameter to the callback detailing any errors.
165184
* `encrypted` - Second parameter to the callback providing the encrypted form.
166185
* `compareSync(data, encrypted)`
@@ -169,7 +188,7 @@ If you are using bcrypt on a simple script, using the sync mode is perfectly fin
169188
* `compare(data, encrypted, cb)`
170189
* `data` - [REQUIRED] - data to compare.
171190
* `encrypted` - [REQUIRED] - data to be compared to.
172-
* `cb` - [REQUIRED] - a callback to be fired once the data has been compared. uses eio making it asynchronous.
191+
* `cb` - [OPTIONAL] - a callback to be fired once the data has been compared. uses eio making it asynchronous. If `cb` is not specified, a `Promise` is returned if Promise support is available.
173192
* `err` - First parameter to the callback detailing any errors.
174193
* `same` - Second parameter to the callback providing whether the data and encrypted forms match [true | false].
175194
* `getRounds(encrypted)` - return the number of rounds used to encrypt a given hash

bcrypt.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ var bindings = require(binding_path);
77

88
var crypto = require('crypto');
99

10+
var promises = require('./lib/promises');
11+
1012
/// generate a salt (sync)
1113
/// @param {Number} [rounds] number of rounds (default 10)
1214
/// @return {String} salt
@@ -36,6 +38,10 @@ module.exports.genSalt = function(rounds, ignore, cb) {
3638
cb = arguments[1];
3739
}
3840

41+
if (!cb) {
42+
return promises.promise(this.genSalt, this, arguments);
43+
}
44+
3945
// default 10 rounds
4046
if (!rounds) {
4147
rounds = 10;
@@ -46,10 +52,6 @@ module.exports.genSalt = function(rounds, ignore, cb) {
4652
});
4753
}
4854

49-
if (!cb) {
50-
return;
51-
}
52-
5355
crypto.randomBytes(16, function(error, randomBytes) {
5456
if (error) {
5557
cb(error);
@@ -97,6 +99,16 @@ module.exports.hash = function(data, salt, cb) {
9799
});
98100
}
99101

102+
// cb exists but is not a function
103+
// return a rejecting promise
104+
if (cb && typeof cb !== 'function') {
105+
return promises.reject(new Error('cb must be a function or null to return a Promise'));
106+
}
107+
108+
if (!cb) {
109+
return promises.promise(this.hash, this, arguments);
110+
}
111+
100112
if (data == null || salt == null) {
101113
return process.nextTick(function() {
102114
cb(new Error('data and salt arguments required'));
@@ -109,9 +121,6 @@ module.exports.hash = function(data, salt, cb) {
109121
});
110122
}
111123

112-
if (!cb || typeof cb !== 'function') {
113-
return;
114-
}
115124

116125
if (typeof salt === 'number') {
117126
return module.exports.genSalt(salt, function(err, salt) {
@@ -155,8 +164,14 @@ module.exports.compare = function(data, hash, cb) {
155164
});
156165
}
157166

158-
if (!cb || typeof cb !== 'function') {
159-
return;
167+
// cb exists but is not a function
168+
// return a rejecting promise
169+
if (cb && typeof cb !== 'function') {
170+
return promises.reject(new Error('cb must be a function or null to return a Promise'));
171+
}
172+
173+
if (!cb) {
174+
return promises.promise(this.compare, this, arguments);
160175
}
161176

162177
return bindings.compare(data, hash, cb);

lib/promises.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
3+
/// encapsulate a method with a node-style callback in a Promise
4+
/// @param {object} 'this' of the encapsulated function
5+
/// @param {function} function to be encapsulated
6+
/// @param {Array-like} args to be passed to the called function
7+
/// @return {Promise} a Promise encapuslaing the function
8+
module.exports.promise = function (fn, context, args) {
9+
10+
//can't do anything without Promise so fail silently
11+
if (typeof Promise === 'undefined') {
12+
return;
13+
}
14+
15+
if (!Array.isArray(args)) {
16+
args = Array.prototype.slice.call(args);
17+
}
18+
19+
if (typeof fn !== 'function') {
20+
return Promise.reject(new Error('fn must be a function'));
21+
}
22+
23+
return new Promise(function(resolve, reject) {
24+
args.push(function(err, data) {
25+
if (err) {
26+
reject(err);
27+
} else {
28+
resolve(data);
29+
}
30+
});
31+
32+
fn.apply(context, args);
33+
});
34+
};
35+
36+
/// @param {err} the error to be thrown
37+
module.exports.reject = function (err) {
38+
39+
// silently swallow errors if Promise is not defined
40+
// emulating old behavior
41+
if (typeof Promise === 'undefined') {
42+
return;
43+
}
44+
45+
return Promise.reject(err);
46+
};

test/promise.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
var bcrypt = require('../bcrypt');
2+
3+
var fail = function(assert, error) {
4+
assert.ok(false, error);
5+
assert.done();
6+
};
7+
8+
// only run these tests if Promise is available
9+
if (typeof Promise !== 'undefined') {
10+
module.exports = {
11+
test_salt_returns_promise_on_no_args: function(assert) {
12+
// make sure test passes with non-native implementations such as bluebird
13+
// http://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise
14+
assert.ok(typeof bcrypt.genSalt().then === 'function', "Should return a promise");
15+
assert.done();
16+
},
17+
test_salt_length: function(assert) {
18+
assert.expect(2);
19+
bcrypt.genSalt(10).then(function(salt) {
20+
assert.ok(typeof salt !== 'undefined', 'salt must not be undefined');
21+
assert.equals(29, salt.length, "Salt isn't the correct length.");
22+
assert.done();
23+
});
24+
},
25+
test_salt_rounds_is_string_number: function(assert) {
26+
assert.expect(1);
27+
bcrypt.genSalt('10').then(function() {
28+
fail(assert, "should not be resolved");
29+
}).catch(function(err) {
30+
assert.ok((err instanceof Error), "Should be an Error. genSalt requires round to be of type number.");
31+
}).then(function() {
32+
assert.done();
33+
});
34+
},
35+
test_salt_rounds_is_string_non_number: function(assert) {
36+
assert.expect(1);
37+
bcrypt.genSalt('b').then(function() {
38+
fail(assert, "should not be resolved");
39+
}).catch(function(err) {
40+
assert.ok((err instanceof Error), "Should be an Error. genSalt requires round to be of type number.");
41+
}).then(function() {
42+
assert.done();
43+
});
44+
},
45+
test_hash: function(assert) {
46+
assert.expect(1);
47+
bcrypt.genSalt(10).then(function(salt) {
48+
return bcrypt.hash('password', salt);
49+
}).then(function(res) {
50+
assert.ok(res, "Res should be defined.");
51+
assert.done();
52+
});
53+
},
54+
test_hash_rounds: function(assert) {
55+
assert.expect(1);
56+
bcrypt.hash('bacon', 8).then(function(hash) {
57+
assert.equals(bcrypt.getRounds(hash), 8, "Number of rounds should be that specified in the function call.");
58+
assert.done();
59+
});
60+
},
61+
test_hash_empty_strings: function(assert) {
62+
assert.expect(2);
63+
Promise.all([
64+
bcrypt.genSalt(10).then(function(salt) {
65+
return bcrypt.hash('', salt);
66+
}).then(function(res) {
67+
assert.ok(res, "Res should be defined even with an empty pw.");
68+
}),
69+
bcrypt.hash('', '').then(function() {
70+
fail(assert, "should not be resolved")
71+
}).catch(function(err) {
72+
assert.ok(err);
73+
}),
74+
]).then(function() {
75+
assert.done();
76+
});
77+
},
78+
test_hash_no_params: function(assert) {
79+
assert.expect(1);
80+
bcrypt.hash().then(function() {
81+
fail(assert, "should not be resolved");
82+
}).catch(function(err) {
83+
assert.ok(err, "Should be an error. No params.");
84+
}).then(function() {
85+
assert.done();
86+
});
87+
},
88+
test_hash_one_param: function(assert) {
89+
assert.expect(1);
90+
bcrypt.hash('password').then(function() {
91+
fail(assert, "should not be resolved");
92+
}).catch(function(err) {
93+
assert.ok(err, "Should be an error. No salt.");
94+
}).then(function() {
95+
assert.done();
96+
});
97+
},
98+
test_hash_salt_validity: function(assert) {
99+
assert.expect(3);
100+
Promise.all(
101+
[
102+
bcrypt.hash('password', '$2a$10$somesaltyvaluertsetrse').then(function(enc) {
103+
assert.ok(enc, "should be resolved with a value");
104+
}),
105+
bcrypt.hash('password', 'some$value').then(function() {
106+
fail(assert, "should not resolve");
107+
}).catch(function(err) {
108+
assert.notEqual(err, undefined);
109+
assert.equal(err.message, "Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue");
110+
})
111+
]).then(function() {
112+
assert.done();
113+
});
114+
},
115+
test_verify_salt: function(assert) {
116+
assert.expect(2);
117+
bcrypt.genSalt(10).then(function(salt) {
118+
var split_salt = salt.split('$');
119+
assert.ok(split_salt[1], '2a');
120+
assert.ok(split_salt[2], '10');
121+
assert.done();
122+
});
123+
},
124+
test_verify_salt_min_rounds: function(assert) {
125+
assert.expect(2);
126+
bcrypt.genSalt(1).then(function(salt) {
127+
var split_salt = salt.split('$');
128+
assert.ok(split_salt[1], '2a');
129+
assert.ok(split_salt[2], '4');
130+
assert.done();
131+
});
132+
},
133+
test_verify_salt_max_rounds: function(assert) {
134+
assert.expect(2);
135+
bcrypt.genSalt(100).then(function(salt) {
136+
var split_salt = salt.split('$');
137+
assert.ok(split_salt[1], '2a');
138+
assert.ok(split_salt[2], '31');
139+
assert.done();
140+
});
141+
},
142+
test_hash_compare: function(assert) {
143+
assert.expect(3);
144+
bcrypt.genSalt(10).then(function(salt) {
145+
assert.equals(29, salt.length, "Salt isn't the correct length.");
146+
return bcrypt.hash("test", salt);
147+
}).then(function(hash) {
148+
return Promise.all(
149+
[
150+
bcrypt.compare("test", hash).then(function(res) {
151+
assert.equal(res, true, "These hashes should be equal.");
152+
}),
153+
bcrypt.compare("blah", hash).then(function(res) {
154+
assert.equal(res, false, "These hashes should not be equal.");
155+
})
156+
]).then(function() {
157+
assert.done();
158+
});
159+
});
160+
},
161+
test_hash_compare_empty_strings: function(assert) {
162+
assert.expect(2);
163+
var hash = bcrypt.hashSync("test", bcrypt.genSaltSync(10));
164+
bcrypt.compare("", hash).then(function(res) {
165+
assert.equal(res, false, "These hashes should be equal.");
166+
return bcrypt.compare("", "");
167+
}).then(function(res) {
168+
assert.equal(res, false, "These hashes should be equal.");
169+
assert.done();
170+
});
171+
},
172+
test_hash_compare_invalid_strings: function(assert) {
173+
var fullString = 'envy1362987212538';
174+
var hash = '$2a$10$XOPbrlUPQdwdJUpSrIF6X.LbE14qsMmKGhM1A8W9iqaG3vv1BD7WC';
175+
var wut = ':';
176+
Promise.all([
177+
bcrypt.compare(fullString, hash).then(function(res) {
178+
assert.ok(res);
179+
}),
180+
bcrypt.compare(fullString, wut).then(function(res) {
181+
assert.ok(!res);
182+
})
183+
]).then(function() {
184+
assert.done();
185+
});
186+
}
187+
};
188+
}

0 commit comments

Comments
 (0)