Sven Slootweg 5 years ago
parent
commit
4d96794ab9
5 changed files with 116 additions and 42 deletions
  1. 8 2
      README.md
  2. 52 18
      lib/scrypt-for-humans.coffee
  3. 54 19
      lib/scrypt-for-humans.js
  4. 2 2
      package.json
  5. 0 1
      test.js

+ 8 - 2
README.md

@ -7,7 +7,7 @@ This module will change and do the following things for you:
7 7
* Input values (passwords, usually) are expected in utf-8.
8 8
* Output/hash values are base64-encoded, and can be stored directly in your data store of choice.
9 9
* Scrypt parameters are set to `scrypt.params(0.1)`, this can be overridden on a per-hash basis (see API documentation below).
10
* 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.
10
* 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.
11 11
12 12
The API supports both Promises and nodebacks.
13 13
@ -87,6 +87,12 @@ scrypt.hash("secretpassword", {}, function(err, hash){
87 87
});
88 88
```
89 89
90
## Upgrading to 2.0.0
91
92
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).
93
94
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.
95
90 96
## API
91 97
92 98
### scrypt.hash(input, [options, [callback]])
@ -95,7 +101,7 @@ Creates a hash.
95 101
96 102
* __input__: The input to hash, usually a password.
97 103
* __options__: *Optional.* Custom options.
98
	* __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).
104
	* __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).
99 105
* __callback__: *Optional.* A nodeback to call upon completion. If omitted, the function will return a Promise.
100 106
101 107
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.

+ 52 - 18
lib/scrypt-for-humans.coffee

@ -2,45 +2,79 @@ scrypt = require "scrypt"
2 2
errors = require "errors"
3 3
Promise = require "bluebird"
4 4
5
# Scrypt input/output format configuration
6
# 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.
7
scrypt.hash.config.keyEncoding = "utf8"
8
scrypt.hash.config.outputEncoding = "base64"
9
scrypt.verify.config.keyEncoding = "utf8"
10
scrypt.verify.config.hashEncoding = "base64"
11
12 5
# Some custom error types, since the `scrypt` library doesn't have proper error handling
13 6
errors.create name: "ScryptError"
14 7
errors.create {name: "ScryptInputError", parents: errors.ScryptError}
15 8
errors.create {name: "ScryptPasswordError", parents: errors.ScryptError}
16 9
errors.create {name: "ScryptInternalError", parents: errors.ScryptError}
17 10
11
scryptErrorMap = {
12
	"getrlimit or sysctl(hw.usermem) failed": 1
13
	"clock_getres or clock_gettime failed": 2
14
	"error computing derived key": 3
15
	"could not read salt from /dev/urandom": 4
16
	"error in OpenSSL": 5
17
	"malloc failed": 6
18
	"data is not a valid scrypt-encrypted block": 7
19
	"unrecognized scrypt format": 8
20
	"decrypting file would take too much memory": 9
21
	"decrypting file would take too long": 10
22
	"password is incorrect": 11
23
	"error writing output file": 12
24
	"error reading input file": 13
25
	"error unkown": -1
26
}
27
28
defaultParameters = Promise.promisify(scrypt.params)(0.1, undefined, undefined)
29
30
getDefaultParameters = (params) ->
31
	# 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.
32
	if params?
33
		return params
34
	else
35
		return defaultParameters
36
37
normalizePassword = (password) ->
38
	if Buffer.isBuffer(password)
39
		return password
40
	else
41
		return new Buffer(password)
18 42
19 43
scryptHandler = (resolve, reject) ->
20
	# This is ridiculous, but `scrypt` doesn't have proper error-handling facilities...
44
	# Well, `scrypt` now returns real Error objects. Except now they don't have error codes anymore...
21 45
	return (err, result) ->
22 46
		if err?
23
			errorObj = switch err.scrypt_err_code
24
				when 1, 2, 3, 4, 5, 6, 9, 10, 12, 13 then errors.ScryptInternalError
47
			errorObj = switch scryptErrorMap[err.message]
48
				when 1, 2, 3, 4, 5, 6, 9, 10, 12, 13, -1 then errors.ScryptInternalError
25 49
				when 7, 8 then errors.ScryptInputError
26 50
				when 11 then errors.ScryptPasswordError
27
			reject new errorObj(err.scrypt_err_message)
28
		else
51
			reject new errorObj(err.message)
52
		else if result == true
29 53
			resolve result
30
54
		else if result == false
55
			reject new errors.ScryptPasswordError("The password did not match.")
56
		else
57
			resolve result.toString("base64")
31 58
32 59
module.exports =
33 60
	hash: (password, options = {}, callback) ->
34
		(new Promise (resolve, reject) ->
35
			options.params ?= scrypt.params(0.1)
36
			scrypt.hash password, options.params, scryptHandler(resolve, reject)
37
		).nodeify(callback)
61
		# 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...
62
		Promise.try ->
63
			getDefaultParameters(options.params)
64
		.then (parameters) ->
65
			new Promise (resolve, reject) ->
66
				scrypt.kdf normalizePassword(password), parameters, scryptHandler(resolve, reject)
67
		.nodeify(callback)
68
38 69
	verifyHash: (password, hash, callback) ->
39 70
		(new Promise (resolve, reject) ->
40
			scrypt.verify hash, password, scryptHandler(resolve, reject)
71
			hashBuffer = new Buffer(hash, "base64")
72
			scrypt.verifyKdf hashBuffer, normalizePassword(password), scryptHandler(resolve, reject)
41 73
		).nodeify(callback)
74
42 75
	ScryptError: errors.ScryptError
43 76
	InputError: errors.ScryptInputError
44 77
	PasswordError: errors.ScryptPasswordError
45 78
	InternalError: errors.ScryptInternalError
79
46 80
	scryptLib: scrypt

+ 54 - 19
lib/scrypt-for-humans.js

@ -1,4 +1,4 @@
1
var Promise, errors, scrypt, scryptHandler;
1
var Promise, defaultParameters, errors, getDefaultParameters, normalizePassword, scrypt, scryptErrorMap, scryptHandler;
2 2
3 3
scrypt = require("scrypt");
4 4
@ -6,14 +6,6 @@ errors = require("errors");
6 6
7 7
Promise = require("bluebird");
8 8
9
scrypt.hash.config.keyEncoding = "utf8";
10
11
scrypt.hash.config.outputEncoding = "base64";
12
13
scrypt.verify.config.keyEncoding = "utf8";
14
15
scrypt.verify.config.hashEncoding = "base64";
16
17 9
errors.create({
18 10
  name: "ScryptError"
19 11
});
@ -33,12 +25,47 @@ errors.create({
33 25
  parents: errors.ScryptError
34 26
});
35 27
28
scryptErrorMap = {
29
  "getrlimit or sysctl(hw.usermem) failed": 1,
30
  "clock_getres or clock_gettime failed": 2,
31
  "error computing derived key": 3,
32
  "could not read salt from /dev/urandom": 4,
33
  "error in OpenSSL": 5,
34
  "malloc failed": 6,
35
  "data is not a valid scrypt-encrypted block": 7,
36
  "unrecognized scrypt format": 8,
37
  "decrypting file would take too much memory": 9,
38
  "decrypting file would take too long": 10,
39
  "password is incorrect": 11,
40
  "error writing output file": 12,
41
  "error reading input file": 13,
42
  "error unkown": -1
43
};
44
45
defaultParameters = Promise.promisify(scrypt.params)(0.1, void 0, void 0);
46
47
getDefaultParameters = function(params) {
48
  if (params != null) {
49
    return params;
50
  } else {
51
    return defaultParameters;
52
  }
53
};
54
55
normalizePassword = function(password) {
56
  if (Buffer.isBuffer(password)) {
57
    return password;
58
  } else {
59
    return new Buffer(password);
60
  }
61
};
62
36 63
scryptHandler = function(resolve, reject) {
37 64
  return function(err, result) {
38 65
    var errorObj;
39 66
    if (err != null) {
40 67
      errorObj = (function() {
41
        switch (err.scrypt_err_code) {
68
        switch (scryptErrorMap[err.message]) {
42 69
          case 1:
43 70
          case 2:
44 71
          case 3:
@ -49,6 +76,7 @@ scryptHandler = function(resolve, reject) {
49 76
          case 10:
50 77
          case 12:
51 78
          case 13:
79
          case -1:
52 80
            return errors.ScryptInternalError;
53 81
          case 7:
54 82
          case 8:
@ -57,9 +85,13 @@ scryptHandler = function(resolve, reject) {
57 85
            return errors.ScryptPasswordError;
58 86
        }
59 87
      })();
60
      return reject(new errorObj(err.scrypt_err_message));
61
    } else {
88
      return reject(new errorObj(err.message));
89
    } else if (result === true) {
62 90
      return resolve(result);
91
    } else if (result === false) {
92
      return reject(new errors.ScryptPasswordError("The password did not match."));
93
    } else {
94
      return resolve(result.toString("base64"));
63 95
    }
64 96
  };
65 97
};
@ -69,16 +101,19 @@ module.exports = {
69 101
    if (options == null) {
70 102
      options = {};
71 103
    }
72
    return (new Promise(function(resolve, reject) {
73
      if (options.params == null) {
74
        options.params = scrypt.params(0.1);
75
      }
76
      return scrypt.hash(password, options.params, scryptHandler(resolve, reject));
77
    })).nodeify(callback);
104
    return Promise["try"](function() {
105
      return getDefaultParameters(options.params);
106
    }).then(function(parameters) {
107
      return new Promise(function(resolve, reject) {
108
        return scrypt.kdf(normalizePassword(password), parameters, scryptHandler(resolve, reject));
109
      });
110
    }).nodeify(callback);
78 111
  },
79 112
  verifyHash: function(password, hash, callback) {
80 113
    return (new Promise(function(resolve, reject) {
81
      return scrypt.verify(hash, password, scryptHandler(resolve, reject));
114
      var hashBuffer;
115
      hashBuffer = new Buffer(hash, "base64");
116
      return scrypt.verifyKdf(hashBuffer, normalizePassword(password), scryptHandler(resolve, reject));
82 117
    })).nodeify(callback);
83 118
  },
84 119
  ScryptError: errors.ScryptError,

+ 2 - 2
package.json

@ -1,6 +1,6 @@
1 1
{
2 2
  "name": "scrypt-for-humans",
3
  "version": "1.0.2",
3
  "version": "2.0.0",
4 4
  "description": "A human-friendly API wrapper for the Node.js Scrypt bindings.",
5 5
  "main": "index.js",
6 6
  "scripts": {
@ -31,6 +31,6 @@
31 31
  "dependencies": {
32 32
    "bluebird": "^2.6.4",
33 33
    "errors": "^0.2.0",
34
    "scrypt": "^4.0.7"
34
    "scrypt": "^5.2.0"
35 35
  }
36 36
}

+ 0 - 1
test.js

@ -2,7 +2,6 @@ var scrypt = require("./");
2 2
var Promise = require("bluebird");
3 3
4 4
/* Using Promises */
5
6 5
var theHash;
7 6
8 7
Promise.try(function(){