Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Promises #458

Merged
merged 8 commits into from
Dec 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,26 @@ bcrypt.compare(someOtherPlaintextPassword, hash, function(err, res) {
// res == false
});
```
### with promises

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.

Async methods that accept a callback, return a `Promise` when callback is not specified if Promise support is available.

```javascript
bcrypt.hash(myPlaintextPassword, saltRounds).then(function(hash) {
// Store hash in your password DB.
});
```
```javascript
// Load hash from your password DB.
bcrypt.compare(myPlaintextPassword, hash).then(function(res) {
// res == true
});
bcrypt.compare(someOtherPlaintextPassword, hash).then(function(res) {
// res == false
});
```

### sync

Expand Down Expand Up @@ -151,7 +170,7 @@ If you are using bcrypt on a simple script, using the sync mode is perfectly fin
* `rounds` - [OPTIONAL] - the cost of processing the data. (default - 10)
* `genSalt(rounds, cb)`
* `rounds` - [OPTIONAL] - the cost of processing the data. (default - 10)
* `cb` - [REQUIRED] - a callback to be fired once the salt has been generated. uses eio making it asynchronous.
* `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.
* `err` - First parameter to the callback detailing any errors.
* `salt` - Second parameter to the callback providing the generated salt.
* `hashSync(data, salt)`
Expand All @@ -160,7 +179,7 @@ If you are using bcrypt on a simple script, using the sync mode is perfectly fin
* `hash(data, salt, cb)`
* `data` - [REQUIRED] - the data to be encrypted.
* `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**).
* `cb` - [REQUIRED] - a callback to be fired once the data has been encrypted. uses eio making it asynchronous.
* `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.
* `err` - First parameter to the callback detailing any errors.
* `encrypted` - Second parameter to the callback providing the encrypted form.
* `compareSync(data, encrypted)`
Expand All @@ -169,7 +188,7 @@ If you are using bcrypt on a simple script, using the sync mode is perfectly fin
* `compare(data, encrypted, cb)`
* `data` - [REQUIRED] - data to compare.
* `encrypted` - [REQUIRED] - data to be compared to.
* `cb` - [REQUIRED] - a callback to be fired once the data has been compared. uses eio making it asynchronous.
* `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.
* `err` - First parameter to the callback detailing any errors.
* `same` - Second parameter to the callback providing whether the data and encrypted forms match [true | false].
* `getRounds(encrypted)` - return the number of rounds used to encrypt a given hash
Expand Down
33 changes: 24 additions & 9 deletions bcrypt.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ var bindings = require(binding_path);

var crypto = require('crypto');

var promises = require('./lib/promises');

/// generate a salt (sync)
/// @param {Number} [rounds] number of rounds (default 10)
/// @return {String} salt
Expand Down Expand Up @@ -36,6 +38,10 @@ module.exports.genSalt = function(rounds, ignore, cb) {
cb = arguments[1];
}

if (!cb) {
return promises.promise(this.genSalt, this, arguments);
}

// default 10 rounds
if (!rounds) {
rounds = 10;
Expand All @@ -46,10 +52,6 @@ module.exports.genSalt = function(rounds, ignore, cb) {
});
}

if (!cb) {
return;
}

crypto.randomBytes(16, function(error, randomBytes) {
if (error) {
cb(error);
Expand Down Expand Up @@ -97,6 +99,16 @@ module.exports.hash = function(data, salt, cb) {
});
}

// cb exists but is not a function
// return a rejecting promise
if (cb && typeof cb !== 'function') {
return promises.reject(new Error('cb must be a function or null to return a Promise'));
}

if (!cb) {
return promises.promise(this.hash, this, arguments);
}

if (data == null || salt == null) {
return process.nextTick(function() {
cb(new Error('data and salt arguments required'));
Expand All @@ -109,9 +121,6 @@ module.exports.hash = function(data, salt, cb) {
});
}

if (!cb || typeof cb !== 'function') {
return;
}

if (typeof salt === 'number') {
return module.exports.genSalt(salt, function(err, salt) {
Expand Down Expand Up @@ -155,8 +164,14 @@ module.exports.compare = function(data, hash, cb) {
});
}

if (!cb || typeof cb !== 'function') {
return;
// cb exists but is not a function
// return a rejecting promise
if (cb && typeof cb !== 'function') {
return promises.reject(new Error('cb must be a function or null to return a Promise'));
}

if (!cb) {
return promises.promise(this.compare, this, arguments);
}

return bindings.compare(data, hash, cb);
Expand Down
46 changes: 46 additions & 0 deletions lib/promises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';

/// encapsulate a method with a node-style callback in a Promise
/// @param {object} 'this' of the encapsulated function
/// @param {function} function to be encapsulated
/// @param {Array-like} args to be passed to the called function
/// @return {Promise} a Promise encapuslaing the function
module.exports.promise = function (fn, context, args) {

//can't do anything without Promise so fail silently
if (typeof Promise === 'undefined') {
return;
}

if (!Array.isArray(args)) {
args = Array.prototype.slice.call(args);
}

if (typeof fn !== 'function') {
return Promise.reject(new Error('fn must be a function'));
}

return new Promise(function(resolve, reject) {
args.push(function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});

fn.apply(context, args);
});
};

/// @param {err} the error to be thrown
module.exports.reject = function (err) {

// silently swallow errors if Promise is not defined
// emulating old behavior
if (typeof Promise === 'undefined') {
return;
}

return Promise.reject(err);
};
188 changes: 188 additions & 0 deletions test/promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
var bcrypt = require('../bcrypt');

var fail = function(assert, error) {
assert.ok(false, error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to require assert? Or is it already present globally?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert is passed to test cases by nodeunit

assert.done();
};

// only run these tests if Promise is available
if (typeof Promise !== 'undefined') {
module.exports = {
test_salt_returns_promise_on_no_args: function(assert) {
// make sure test passes with non-native implementations such as bluebird
// http://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise
assert.ok(typeof bcrypt.genSalt().then === 'function', "Should return a promise");
assert.done();
},
test_salt_length: function(assert) {
assert.expect(2);
bcrypt.genSalt(10).then(function(salt) {
assert.ok(typeof salt !== 'undefined', 'salt must not be undefined');
assert.equals(29, salt.length, "Salt isn't the correct length.");
assert.done();
});
},
test_salt_rounds_is_string_number: function(assert) {
assert.expect(1);
bcrypt.genSalt('10').then(function() {
fail(assert, "should not be resolved");
}).catch(function(err) {
assert.ok((err instanceof Error), "Should be an Error. genSalt requires round to be of type number.");
}).then(function() {
assert.done();
});
},
test_salt_rounds_is_string_non_number: function(assert) {
assert.expect(1);
bcrypt.genSalt('b').then(function() {
fail(assert, "should not be resolved");
}).catch(function(err) {
assert.ok((err instanceof Error), "Should be an Error. genSalt requires round to be of type number.");
}).then(function() {
assert.done();
});
},
test_hash: function(assert) {
assert.expect(1);
bcrypt.genSalt(10).then(function(salt) {
return bcrypt.hash('password', salt);
}).then(function(res) {
assert.ok(res, "Res should be defined.");
assert.done();
});
},
test_hash_rounds: function(assert) {
assert.expect(1);
bcrypt.hash('bacon', 8).then(function(hash) {
assert.equals(bcrypt.getRounds(hash), 8, "Number of rounds should be that specified in the function call.");
assert.done();
});
},
test_hash_empty_strings: function(assert) {
assert.expect(2);
Promise.all([
bcrypt.genSalt(10).then(function(salt) {
return bcrypt.hash('', salt);
}).then(function(res) {
assert.ok(res, "Res should be defined even with an empty pw.");
}),
bcrypt.hash('', '').then(function() {
fail(assert, "should not be resolved")
}).catch(function(err) {
assert.ok(err);
}),
]).then(function() {
assert.done();
});
},
test_hash_no_params: function(assert) {
assert.expect(1);
bcrypt.hash().then(function() {
fail(assert, "should not be resolved");
}).catch(function(err) {
assert.ok(err, "Should be an error. No params.");
}).then(function() {
assert.done();
});
},
test_hash_one_param: function(assert) {
assert.expect(1);
bcrypt.hash('password').then(function() {
fail(assert, "should not be resolved");
}).catch(function(err) {
assert.ok(err, "Should be an error. No salt.");
}).then(function() {
assert.done();
});
},
test_hash_salt_validity: function(assert) {
assert.expect(3);
Promise.all(
[
bcrypt.hash('password', '$2a$10$somesaltyvaluertsetrse').then(function(enc) {
assert.ok(enc, "should be resolved with a value");
}),
bcrypt.hash('password', 'some$value').then(function() {
fail(assert, "should not resolve");
}).catch(function(err) {
assert.notEqual(err, undefined);
assert.equal(err.message, "Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue");
})
]).then(function() {
assert.done();
});
},
test_verify_salt: function(assert) {
assert.expect(2);
bcrypt.genSalt(10).then(function(salt) {
var split_salt = salt.split('$');
assert.ok(split_salt[1], '2a');
assert.ok(split_salt[2], '10');
assert.done();
});
},
test_verify_salt_min_rounds: function(assert) {
assert.expect(2);
bcrypt.genSalt(1).then(function(salt) {
var split_salt = salt.split('$');
assert.ok(split_salt[1], '2a');
assert.ok(split_salt[2], '4');
assert.done();
});
},
test_verify_salt_max_rounds: function(assert) {
assert.expect(2);
bcrypt.genSalt(100).then(function(salt) {
var split_salt = salt.split('$');
assert.ok(split_salt[1], '2a');
assert.ok(split_salt[2], '31');
assert.done();
});
},
test_hash_compare: function(assert) {
assert.expect(3);
bcrypt.genSalt(10).then(function(salt) {
assert.equals(29, salt.length, "Salt isn't the correct length.");
return bcrypt.hash("test", salt);
}).then(function(hash) {
return Promise.all(
[
bcrypt.compare("test", hash).then(function(res) {
assert.equal(res, true, "These hashes should be equal.");
}),
bcrypt.compare("blah", hash).then(function(res) {
assert.equal(res, false, "These hashes should not be equal.");
})
]).then(function() {
assert.done();
});
});
},
test_hash_compare_empty_strings: function(assert) {
assert.expect(2);
var hash = bcrypt.hashSync("test", bcrypt.genSaltSync(10));
bcrypt.compare("", hash).then(function(res) {
assert.equal(res, false, "These hashes should be equal.");
return bcrypt.compare("", "");
}).then(function(res) {
assert.equal(res, false, "These hashes should be equal.");
assert.done();
});
},
test_hash_compare_invalid_strings: function(assert) {
var fullString = 'envy1362987212538';
var hash = '$2a$10$XOPbrlUPQdwdJUpSrIF6X.LbE14qsMmKGhM1A8W9iqaG3vv1BD7WC';
var wut = ':';
Promise.all([
bcrypt.compare(fullString, hash).then(function(res) {
assert.ok(res);
}),
bcrypt.compare(fullString, wut).then(function(res) {
assert.ok(!res);
})
]).then(function() {
assert.done();
});
}
};
}