From af0d0ae973977d197343ad43b04efe1113337e07 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Tue, 13 Jan 2015 00:34:20 +0100 Subject: [PATCH] v1.0.0 --- README.md | 137 +++++++++++++++++++++++++++++++++++ index.coffee | 1 + index.js | 1 + lib/scrypt-for-humans.coffee | 46 ++++++++++++ lib/scrypt-for-humans.js | 89 +++++++++++++++++++++++ package.json | 2 + test.js | 51 +++++++++++++ 7 files changed, 327 insertions(+) create mode 100644 README.md create mode 100644 index.coffee create mode 100644 index.js create mode 100644 lib/scrypt-for-humans.coffee create mode 100644 lib/scrypt-for-humans.js create mode 100644 test.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee67f11 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# scrypt-for-humans + +A human-friendly API wrapper for the Node.js Scrypt bindings, because the default bindings kind of suck. + +This module will change and do the following things for you: + +* Input values (passwords, usually) are expected in utf-8. +* Output/hash values are base64-encoded, and can be stored directly in your data store of choice. +* Scrypt parameters are set to `scrypt.params(0.1)`, this can be overridden on a per-hash basis (see API documentation below). +* Scrypt errors, which are not proper Error types in the original library, are caught and rethrown as one of three correctly-inheriting Error types (see API documentation below). This means you can handle them like any other kind of Error. + +The API supports both Promises and nodebacks. + +## License + +[WTFPL](http://www.wtfpl.net/txt/copying/) or [CC0](https://creativecommons.org/publicdomain/zero/1.0/), whichever you prefer. + +## Donate + +My income consists entirely of donations for my projects. If this module is useful to you, consider [making a donation](http://cryto.net/~joepie91/donate.html)! + +You can donate using Bitcoin, PayPal, Gratipay, Flattr, cash-in-mail, SEPA transfers, and pretty much anything else. + +## Contributing + +Pull requests welcome. Please make sure your modifications are in line with the overall code style, and ensure that you're editing the `.coffee` files, not the `.js` files. + +As this module could potentially deal with authentication, tests are needed; a pull request for those would be especially welcome. + +Build tool of choice is `gulp`; simply run `gulp` while developing, and it will watch for changes. + +## Usage + +```javascript +scrypt = require("scrypt-for-humans"); +Promise = require("bluebird"); + +/* Using Promises */ + +var theHash; + +Promise.try(function(){ + return scrypt.hash("secretpassword"); +}).then(function(hash){ + console.log("The hash is " + hash); + theHash = hash; + + /* Now let's see if it verifies - number 1 is correct. */ + return scrypt.verifyHash("secretpassword", theHash); +}).then(function(){ + console.log("Number 1 was correct!"); +}).catch(scrypt.PasswordError, function(err){ + console.log("Number 1 was wrong!"); +}).then(function(){ + /* And let's see if it fails correctly - number 2 is wrong. */ + return scrypt.verifyHash("wrongpassword", theHash); +}).then(function(){ + console.log("Number 2 was correct!"); +}).catch(scrypt.PasswordError, function(err){ + console.log("Number 2 was wrong!"); +}); + +/* Using nodebacks */ + +scrypt.hash("secretpassword", {}, function(err, hash){ + console.log("The hash is " + hash); + + /* Now let's see if it verifies - number 1 is correct. */ + scrypt.verifyHash("secretpassword", hash, function(err, result){ + if(err) { + console.log("Number 1 was wrong!", err); + } else { + console.log("Number 1 was correct!"); + } + + /* And let's see if it fails correctly - number 2 is wrong. */ + scrypt.verifyHash("wrongpassword", hash, function(err, result){ + if(err) { + console.log("Number 2 was wrong!", err); + } else { + console.log("Number 2 was correct!"); + } + }); + }); +}); +``` + +## API + +### scrypt.hash(input, [options, [callback]]) + +Creates a hash. + +* __input__: The input to hash, usually a password. +* __options__: *Optional.* Custom options. + * __options.params__: Sets the Scrypt parameters to use. Defaults to `scrypt.params(0.1)`. If you want to change these, you'll probably need scrypt.scryptLib (documented below). +* __callback__: *Optional.* A nodeback to call upon completion. If omitted, the function will return a Promise. + +If this is successful, the hash is returned as either the resolved Promise value or the second callback parameter, depending on the API you use. + +If an error occurs, either the Promise will reject with it, or it will be passed as the first callback parameter, depending on the API you use. All errors correctly inherit from `Error`, and are documented below. + +### scrypt.verifyHash(input, hash, [callback]) + +Creates a hash. + +* __input__: The input to hash, usually a password. +* __hash__: The hash to verify against, in base64 encoding (the default output format of `scrypt.hash`). +* __callback__: *Optional.* A nodeback to call upon completion. If omitted, the function will return a Promise. + +If the input is correct and matches the hash, the Promise will resolve or the callback will be called with `true` as the value. + +__If the input does *not* match the hash, this is considered a PasswordError, *not* a `false` value!__ + +If an error occurs, either the Promise will reject with it, or it will be passed as the first callback parameter, depending on the API you use. All errors correctly inherit from `Error`, and are documented below. + +### scrypt.PasswordError + +This error is thrown if the input did not match the specified hash. The original error message is retained. + +### scrypt.InputError + +This error is thrown if there is a different problem with the input (either the to-be-hashed value, or the hash), such as a malformed hash. The original error message is retained. + +### scrypt.OperationalError + +This error is thrown when an internal error of some other kind occurs in the `scrypt` library. The original error message is retained. + +### scrypt.scryptLib + +Provides access to the underlying `scrypt` library that is used. Useful if you want to eg. specify custom Scrypt parameters. + +## Changelog + +### v1.0.0 + +Initial release. diff --git a/index.coffee b/index.coffee new file mode 100644 index 0000000..1057d8a --- /dev/null +++ b/index.coffee @@ -0,0 +1 @@ +module.exports = require "./lib/scrypt-for-humans" diff --git a/index.js b/index.js new file mode 100644 index 0000000..c974d58 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require("./lib/scrypt-for-humans"); diff --git a/lib/scrypt-for-humans.coffee b/lib/scrypt-for-humans.coffee new file mode 100644 index 0000000..70e1288 --- /dev/null +++ b/lib/scrypt-for-humans.coffee @@ -0,0 +1,46 @@ +scrypt = require "scrypt" +errors = require "errors" +Promise = require "bluebird" + +# Scrypt input/output format configuration +# FIXME: Figure out how to isolate this, so that there is a guarantee these changes won't affect any other `scrypt` imports outside of the module. +scrypt.hash.config.keyEncoding = "utf8" +scrypt.hash.config.outputEncoding = "base64" +scrypt.verify.config.keyEncoding = "utf8" +scrypt.verify.config.hashEncoding = "base64" + +# Some custom error types, since the `scrypt` library doesn't have proper error handling +errors.create name: "ScryptError" +errors.create {name: "ScryptInputError", parents: errors.ScryptError} +errors.create {name: "ScryptPasswordError", parents: errors.ScryptError} +errors.create {name: "ScryptInternalError", parents: errors.ScryptError} + + +scryptHandler = (resolve, reject) -> + # This is ridiculous, but `scrypt` doesn't have proper error-handling facilities... + return (err, result) -> + if err? + errorObj = switch err.scrypt_err_code + when 1, 2, 3, 4, 5, 6, 9, 10, 12, 13 then errors.ScryptInternalError + when 7, 8 then errors.ScryptInputError + when 11 then errors.ScryptPasswordError + reject new errorObj(err.scrypt_err_message) + else + resolve result + + +module.exports = + hash: (password, options = {}, callback) -> + (new Promise (resolve, reject) -> + options.params ?= scrypt.params(0.1) + scrypt.hash password, options.params, scryptHandler(resolve, reject) + ).nodeify(callback) + verifyHash: (password, hash, callback) -> + (new Promise (resolve, reject) -> + scrypt.verify hash, password, scryptHandler(resolve, reject) + ).nodeify(callback) + ScryptError: errors.ScryptError + InputError: errors.ScryptInputError + PasswordError: errors.ScryptPasswordError + InternalError: errors.ScryptInternalError + scryptLib: scrypt diff --git a/lib/scrypt-for-humans.js b/lib/scrypt-for-humans.js new file mode 100644 index 0000000..146f2f1 --- /dev/null +++ b/lib/scrypt-for-humans.js @@ -0,0 +1,89 @@ +var Promise, errors, scrypt, scryptHandler; + +scrypt = require("scrypt"); + +errors = require("errors"); + +Promise = require("bluebird"); + +scrypt.hash.config.keyEncoding = "utf8"; + +scrypt.hash.config.outputEncoding = "base64"; + +scrypt.verify.config.keyEncoding = "utf8"; + +scrypt.verify.config.hashEncoding = "base64"; + +errors.create({ + name: "ScryptError" +}); + +errors.create({ + name: "ScryptInputError", + parents: errors.ScryptError +}); + +errors.create({ + name: "ScryptPasswordError", + parents: errors.ScryptError +}); + +errors.create({ + name: "ScryptInternalError", + parents: errors.ScryptError +}); + +scryptHandler = function(resolve, reject) { + return function(err, result) { + var errorObj; + if (err != null) { + errorObj = (function() { + switch (err.scrypt_err_code) { + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 9: + case 10: + case 12: + case 13: + return errors.ScryptInternalError; + case 7: + case 8: + return errors.ScryptInputError; + case 11: + return errors.ScryptPasswordError; + } + })(); + return reject(new errorObj(err.scrypt_err_message)); + } else { + return resolve(result); + } + }; +}; + +module.exports = { + hash: function(password, options, callback) { + if (options == null) { + options = {}; + } + return (new Promise(function(resolve, reject) { + if (options.params == null) { + options.params = scrypt.params(0.1); + } + return scrypt.hash(password, options.params, scryptHandler(resolve, reject)); + })).nodeify(callback); + }, + verifyHash: function(password, hash, callback) { + return (new Promise(function(resolve, reject) { + return scrypt.verify(hash, password, scryptHandler(resolve, reject)); + })).nodeify(callback); + }, + ScryptError: errors.ScryptError, + InputError: errors.ScryptInputError, + PasswordError: errors.ScryptPasswordError, + InternalError: errors.ScryptInternalError, + scryptLib: scrypt +}; diff --git a/package.json b/package.json index 27ebe61..0bc9102 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "gulp-util": "~2.2.17" }, "dependencies": { + "bluebird": "^2.6.4", + "errors": "^0.2.0", "scrypt": "^3.0.1" } } diff --git a/test.js b/test.js new file mode 100644 index 0000000..29a45de --- /dev/null +++ b/test.js @@ -0,0 +1,51 @@ +scrypt = require("./"); +Promise = require("bluebird"); + +/* Using Promises */ + +var theHash; + +Promise.try(function(){ + return scrypt.hash("secretpassword"); +}).then(function(hash){ + console.log("The hash is " + hash); + theHash = hash; + + /* Now let's see if it verifies - number 1 is correct. */ + return scrypt.verifyHash("secretpassword", theHash); +}).then(function(){ + console.log("Number 1 was correct!"); +}).catch(scrypt.PasswordError, function(err){ + console.log("Number 1 was wrong!"); +}).then(function(){ + /* And let's see if it fails correctly - number 2 is wrong. */ + return scrypt.verifyHash("wrongpassword", theHash); +}).then(function(){ + console.log("Number 2 was correct!"); +}).catch(scrypt.PasswordError, function(err){ + console.log("Number 2 was wrong!"); +}); + +/* Using nodebacks */ + +scrypt.hash("secretpassword", {}, function(err, hash){ + console.log("The hash is " + hash); + + /* Now let's see if it verifies - number 1 is correct. */ + scrypt.verifyHash("secretpassword", hash, function(err, result){ + if(err) { + console.log("Number 1 was wrong!", err); + } else { + console.log("Number 1 was correct!"); + } + + /* And let's see if it fails correctly - number 2 is wrong. */ + scrypt.verifyHash("wrongpassword", hash, function(err, result){ + if(err) { + console.log("Number 2 was wrong!", err); + } else { + console.log("Number 2 was correct!"); + } + }); + }); +});