diff --git a/README.md b/README.md index 75c18a4..fdc98d9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ 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. +* Scrypt errors, which are now proper Error types in the original library but still not easily distinguishable, 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. @@ -87,6 +87,12 @@ scrypt.hash("secretpassword", {}, function(err, hash){ }); ``` +## Upgrading to 2.0.0 + +Due to changes in the underlying `scrypt` library, there has been a minor indirect change in our documented API as well. Specifically, `scrypt.scryptLib.params` is now asynchronous by default, with (poor) support for ES6 Promises. The new documentation can be found [here](https://github.com/barrysteyn/node-scrypt/blob/master/Readme.md#params). Due to its inconsistent behaviour, I recommend manual promisification using [Bluebird](https://www.npmjs.com/package/bluebird) or [`es6-promisify`](https://www.npmjs.com/package/es6-promisify). + +The other changes in `scrypt` do not affect the `scrypt-for-humans` API, other than introducing support for Node.js 4. If you were not using custom `params`, you can remain using `scrypt-for-humans` like you have done previously. + ## API ### scrypt.hash(input, [options, [callback]]) @@ -95,7 +101,7 @@ 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). + * __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. diff --git a/lib/scrypt-for-humans.coffee b/lib/scrypt-for-humans.coffee index 70e1288..1289b2d 100644 --- a/lib/scrypt-for-humans.coffee +++ b/lib/scrypt-for-humans.coffee @@ -2,45 +2,79 @@ 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} +scryptErrorMap = { + "getrlimit or sysctl(hw.usermem) failed": 1 + "clock_getres or clock_gettime failed": 2 + "error computing derived key": 3 + "could not read salt from /dev/urandom": 4 + "error in OpenSSL": 5 + "malloc failed": 6 + "data is not a valid scrypt-encrypted block": 7 + "unrecognized scrypt format": 8 + "decrypting file would take too much memory": 9 + "decrypting file would take too long": 10 + "password is incorrect": 11 + "error writing output file": 12 + "error reading input file": 13 + "error unkown": -1 +} + +defaultParameters = Promise.promisify(scrypt.params)(0.1, undefined, undefined) + +getDefaultParameters = (params) -> + # This wrapper function is to ensure that we only calculate the parameters once, but can still skip waiting for that if custom parameters were passed in anyway. + if params? + return params + else + return defaultParameters + +normalizePassword = (password) -> + if Buffer.isBuffer(password) + return password + else + return new Buffer(password) scryptHandler = (resolve, reject) -> - # This is ridiculous, but `scrypt` doesn't have proper error-handling facilities... + # Well, `scrypt` now returns real Error objects. Except now they don't have error codes anymore... 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 + errorObj = switch scryptErrorMap[err.message] + when 1, 2, 3, 4, 5, 6, 9, 10, 12, 13, -1 then errors.ScryptInternalError when 7, 8 then errors.ScryptInputError when 11 then errors.ScryptPasswordError - reject new errorObj(err.scrypt_err_message) - else + reject new errorObj(err.message) + else if result == true resolve result - + else if result == false + reject new errors.ScryptPasswordError("The password did not match.") + else + resolve result.toString("base64") 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) + # We will still manually promisify, because the behaviour of `scrypt` is not predictable. It may either synchronously throw an error or return a Promise, depending on available ECMAScript features... + Promise.try -> + getDefaultParameters(options.params) + .then (parameters) -> + new Promise (resolve, reject) -> + scrypt.kdf normalizePassword(password), parameters, scryptHandler(resolve, reject) + .nodeify(callback) + verifyHash: (password, hash, callback) -> (new Promise (resolve, reject) -> - scrypt.verify hash, password, scryptHandler(resolve, reject) + hashBuffer = new Buffer(hash, "base64") + scrypt.verifyKdf hashBuffer, normalizePassword(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 index 146f2f1..662a370 100644 --- a/lib/scrypt-for-humans.js +++ b/lib/scrypt-for-humans.js @@ -1,4 +1,4 @@ -var Promise, errors, scrypt, scryptHandler; +var Promise, defaultParameters, errors, getDefaultParameters, normalizePassword, scrypt, scryptErrorMap, scryptHandler; scrypt = require("scrypt"); @@ -6,14 +6,6 @@ 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" }); @@ -33,12 +25,47 @@ errors.create({ parents: errors.ScryptError }); +scryptErrorMap = { + "getrlimit or sysctl(hw.usermem) failed": 1, + "clock_getres or clock_gettime failed": 2, + "error computing derived key": 3, + "could not read salt from /dev/urandom": 4, + "error in OpenSSL": 5, + "malloc failed": 6, + "data is not a valid scrypt-encrypted block": 7, + "unrecognized scrypt format": 8, + "decrypting file would take too much memory": 9, + "decrypting file would take too long": 10, + "password is incorrect": 11, + "error writing output file": 12, + "error reading input file": 13, + "error unkown": -1 +}; + +defaultParameters = Promise.promisify(scrypt.params)(0.1, void 0, void 0); + +getDefaultParameters = function(params) { + if (params != null) { + return params; + } else { + return defaultParameters; + } +}; + +normalizePassword = function(password) { + if (Buffer.isBuffer(password)) { + return password; + } else { + return new Buffer(password); + } +}; + scryptHandler = function(resolve, reject) { return function(err, result) { var errorObj; if (err != null) { errorObj = (function() { - switch (err.scrypt_err_code) { + switch (scryptErrorMap[err.message]) { case 1: case 2: case 3: @@ -49,6 +76,7 @@ scryptHandler = function(resolve, reject) { case 10: case 12: case 13: + case -1: return errors.ScryptInternalError; case 7: case 8: @@ -57,9 +85,13 @@ scryptHandler = function(resolve, reject) { return errors.ScryptPasswordError; } })(); - return reject(new errorObj(err.scrypt_err_message)); - } else { + return reject(new errorObj(err.message)); + } else if (result === true) { return resolve(result); + } else if (result === false) { + return reject(new errors.ScryptPasswordError("The password did not match.")); + } else { + return resolve(result.toString("base64")); } }; }; @@ -69,16 +101,19 @@ module.exports = { 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); + return Promise["try"](function() { + return getDefaultParameters(options.params); + }).then(function(parameters) { + return new Promise(function(resolve, reject) { + return scrypt.kdf(normalizePassword(password), parameters, scryptHandler(resolve, reject)); + }); + }).nodeify(callback); }, verifyHash: function(password, hash, callback) { return (new Promise(function(resolve, reject) { - return scrypt.verify(hash, password, scryptHandler(resolve, reject)); + var hashBuffer; + hashBuffer = new Buffer(hash, "base64"); + return scrypt.verifyKdf(hashBuffer, normalizePassword(password), scryptHandler(resolve, reject)); })).nodeify(callback); }, ScryptError: errors.ScryptError, diff --git a/package.json b/package.json index 5e25c70..1c05bed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scrypt-for-humans", - "version": "1.0.2", + "version": "2.0.0", "description": "A human-friendly API wrapper for the Node.js Scrypt bindings.", "main": "index.js", "scripts": { @@ -31,6 +31,6 @@ "dependencies": { "bluebird": "^2.6.4", "errors": "^0.2.0", - "scrypt": "^4.0.7" + "scrypt": "^5.2.0" } } diff --git a/test.js b/test.js index f383176..92f0c50 100644 --- a/test.js +++ b/test.js @@ -2,7 +2,6 @@ var scrypt = require("./"); var Promise = require("bluebird"); /* Using Promises */ - var theHash; Promise.try(function(){