Skip to content
Open
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
48 changes: 36 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- State of the art password hashing algorithm (Argon2i)
- Safe defaults for most applications
- Transparent queuing to prevent resource exhaustion
- Future-proof so work factors and hashing algorithms can be easily upgraded
- `Buffers` everywhere for safer memory management

Expand Down Expand Up @@ -57,19 +58,31 @@ following keys:
// Initialise our password policy (these are the defaults)
var pwd = securePassword({
memlimit: securePassword.MEMLIMIT_DEFAULT,
opslimit: securePassword.OPSLIMIT_DEFAULT
opslimit: securePassword.OPSLIMIT_DEFAULT,
parallel: 4
})
```

They're both constrained by the constants `SecurePassword.MEMLIMIT_MIN` -
`SecurePassword.MEMLIMIT_MAX` and
`SecurePassword.OPSLIMIT_MIN` - `SecurePassword.OPSLIMIT_MAX`. If not provided
they will be given the default values `SecurePassword.MEMLIMIT_DEFAULT` and
`SecurePassword.OPSLIMIT_DEFAULT` which should be fast enough for a general
purpose web server without your users noticing too much of a load time. However
your should set these as high as possible to make any kind of cracking as costly
as possible. A load time of 1s seems reasonable for login, so test various
settings in your production environment.
* `opts.memlimit` controls how many bytes can be used for each password. It must
be a value between `SecurePassword.MEMLIMIT_MIN` - `SecurePassword.MEMLIMIT_MAX`
* `opts.opslimit` can be viewed as how many passes are done over the memory. It
must be a value between `SecurePassword.OPSLIMIT_MIN` -
`SecurePassword.OPSLIMIT_MAX`.
* `opts.parallel` controls how many simultaneous calls to `hash` or `verify` can
be run, as to prevent your server from running out of memory.

It will default to `4`, which is the current number of worker threads available
to Node.js. If you increase this value, you should also set `UV_THREADPOOL_SIZE`
to the same value or more, as this environment variable determines the number
of workers available to Node.js. This is done at startup, like this:

```sh
UV_THREADPOOL_SIZE=8 node index.js
```

All these options should be set as high as you can afford. Take into account the
resources you have available on your production machines, and adjust
accordingly.

The settings can be easily increased at a later time as hardware most likely
improves (Moore's law) and adversaries therefore get more powerful. If a hash is
Expand All @@ -79,7 +92,7 @@ according to the updated policy. In contrast to other modules, this module will
not increase these settings automatically as this can have ill effects on
services that are not carefully monitored.

### `pwd.hash(password, function (err, hash) {})`
### `var cancel = pwd.hash(password, function (err, hash) {})`

Takes Buffer `password` and hashes it. You can call `cancel` to abort the hashing.

Expand All @@ -89,6 +102,8 @@ potential error, or the Buffer `hash`.

* `password` must be a Buffer of length `SecurePassword.PASSWORD_BYTES_MIN` - `SecurePassword.PASSWORD_BYTES_MAX`.
* `hash` will be a Buffer of length `SecurePassword.HASH_BYTES`.
* `cancel` is a method that will abort the hashing, if it has not yet started,
and invoke `cb` with and error that has `err.cancelled === true`

### `var hash = pwd.hashSync(password)`

Expand All @@ -100,7 +115,7 @@ the Buffer `hash`.
`password` must be a Buffer of length `SecurePassword.PASSWORD_BYTES_MIN` - `SecurePassword.PASSWORD_BYTES_MAX`.
`hash` will be a Buffer of length `SecurePassword.HASH_BYTES`.

### `pwd.verify(password, hash, function (err, enum) {})`
### `var cancel = pwd.verify(password, hash, function (err, enum) {})`

Takes Buffer `password` and hashes it and then safely compares it to the
Buffer `hash`. The hashing is done by a seperate worker as to not block the
Expand All @@ -126,6 +141,15 @@ The function may `throw` a potential error, or return one of the enums
`SecurePassword.VALID`, `SecurePassword.INVALID`, `SecurePassword.NEEDS_REHASH` or `SecurePassword.INVALID_UNRECOGNIZED_HASH`.
Check with strict equality for one the cases as in the example above.

### `pwd.pending`

Number of hash / verify tasks pending.

### `pwd.parallel`

Number of hash / verify tasks that can be processed simultaneously. Can be set
through the constructor, but is **read-only**.

### `SecurePassword.VALID`

The password was verified and is valid
Expand Down
36 changes: 34 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var sodium = require('sodium-universal')
var assert = require('nanoassert')
var parallelQueue = require('parallel-queue')

module.exports = SecurePassword
SecurePassword.HASH_BYTES = sodium.crypto_pwhash_STRBYTES
Expand Down Expand Up @@ -35,8 +36,21 @@ function SecurePassword (opts) {

assert(this.opslimit >= SecurePassword.OPSLIMIT_MIN, 'opts.opslimit must be at least OPSLIMIT_MIN (' + SecurePassword.OPSLIMIT_MIN + ')')
assert(this.opslimit <= SecurePassword.OPSLIMIT_MAX, 'opts.memlimit must be at most OPSLIMIT_MAX (' + SecurePassword.OPSLIMIT_MAX + ')')

if (opts.parallel == null) this.parallel = 4 // Default UV_THREADPOOL_SIZE
else this.parallel = opts.parallel

assert(this.parallel > 0, 'opts.parallel must be at least 1')

this._queue = parallelQueue(this.parallel, this._process.bind(this))
}

Object.defineProperty(SecurePassword.prototype, 'pending', {
get: function () {
return this._queue.pending
}
})

SecurePassword.prototype.hashSync = function (passwordBuf) {
assert(Buffer.isBuffer(passwordBuf), 'passwordBuf must be Buffer')
assert(passwordBuf.length >= SecurePassword.PASSWORD_BYTES_MIN, 'passwordBuf must be at least PASSWORD_BYTES_MIN (' + SecurePassword.PASSWORD_BYTES_MIN + ')')
Expand All @@ -58,6 +72,10 @@ SecurePassword.prototype.hash = function (passwordBuf, cb) {
assert(passwordBuf.length < SecurePassword.PASSWORD_BYTES_MAX, 'passwordBuf must be shorter than PASSWORD_BYTES_MAX (' + SecurePassword.PASSWORD_BYTES_MAX + ')')
assert(typeof cb === 'function', 'cb must be function')

return this._queue.push({type: 'hash', passwordBuf: passwordBuf}, cb)
}

SecurePassword.prototype._hash = function (passwordBuf, cb) {
// Unsafe is okay here since sodium will overwrite all bytes
var hashBuf = Buffer.allocUnsafe(SecurePassword.HASH_BYTES)
sodium.crypto_pwhash_str_async(hashBuf, passwordBuf, this.opslimit, this.memlimit, function (err) {
Expand Down Expand Up @@ -98,6 +116,12 @@ SecurePassword.prototype.verify = function (passwordBuf, hashBuf, cb) {
assert(Buffer.isBuffer(hashBuf), 'hashBuf must be Buffer')
assert(hashBuf.length === SecurePassword.HASH_BYTES, 'hashBuf must be HASH_BYTES (' + SecurePassword.HASH_BYTES + ')')

return this._queue.push({type: 'verify', passwordBuf: passwordBuf, hashBuf: hashBuf}, cb)
}

SecurePassword.prototype._verify = function (passwordBuf, hashBuf, cb) {
var self = this

var parameters = decodeArgon2iStr(hashBuf)
if (parameters === false) return process.nextTick(cb, null, SecurePassword.INVALID_UNRECOGNIZED_HASH)

Expand All @@ -106,12 +130,20 @@ SecurePassword.prototype.verify = function (passwordBuf, hashBuf, cb) {

if (bool === false) return cb(null, SecurePassword.INVALID)

if (parameters.memlimit < this.memlimit || parameters.opslimit < this.opslimit) {
if (parameters.memlimit < self.memlimit || parameters.opslimit < self.opslimit) {
return cb(null, SecurePassword.VALID_NEEDS_REHASH)
}

return cb(null, SecurePassword.VALID)
}.bind(this))
})
}

SecurePassword.prototype._process = function (args, done) {
switch (args.type) {
case 'verify': return this._verify(args.passwordBuf, args.hashBuf, done)
case 'hash': return this._hash(args.passwordBuf, done)
default: throw new Error('Illegal method type')
}
}

var Argon2iStr_ALG_TAG = Buffer.from('$argon2i')
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "index.js",
"dependencies": {
"nanoassert": "^1.0.0",
"parallel-queue": "^1.0.0",
"sodium-universal": "^1.0.0"
},
"devDependencies": {
Expand Down
83 changes: 83 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,86 @@ test('Can handle invalid hash async', function (assert) {
assert.end()
})
})

test('Can hash password async simultanious queued', function (assert) {
var pwd = securePassword({
version: 0,
memlimit: securePassword.MEMLIMIT_DEFAULT,
opslimit: securePassword.OPSLIMIT_DEFAULT,
parallel: 1
})

var userPassword = Buffer.from('my secrets')

var completed = 0
pwd.hash(userPassword, function (err, passwordHash) {
assert.error(err)
assert.equal(completed, 0)
assert.equal(pwd.pending, 1)
assert.notOk(userPassword.equals(passwordHash))

completed++
})

pwd.hash(userPassword, function (err, passwordHash) {
assert.error(err)
assert.equal(completed, 1)
assert.equal(pwd.pending, 0)
assert.notOk(userPassword.equals(passwordHash))
assert.end()
})
})

test('Can cancel queued hash', function (assert) {
assert.plan(3)
var pwd = securePassword({
version: 0,
memlimit: securePassword.MEMLIMIT_DEFAULT,
opslimit: securePassword.OPSLIMIT_DEFAULT,
parallel: 1
})

var userPassword = Buffer.from('my secrets')

pwd.hash(userPassword, function (err, passwordHash) {
assert.error(err, 'err')
assert.notOk(userPassword.equals(passwordHash), 'did hash')
})

// Cancel now
setImmediate(pwd.hash(userPassword, function (err, passwordHash) {
assert.ok(err, 'has err')
}))
})

test('Can interleave cancel queued hash', function (assert) {
assert.plan(6)
var pwd = securePassword({
version: 0,
memlimit: securePassword.MEMLIMIT_DEFAULT,
opslimit: securePassword.OPSLIMIT_DEFAULT,
parallel: 1
})

var userPassword = Buffer.from('my secrets')

pwd.hash(userPassword, function (err, passwordHash) {
assert.error(err, 'err')
assert.notOk(userPassword.equals(passwordHash), 'did hash')

pwd.verify(userPassword, passwordHash, function (err, res) {
assert.error(err)
assert.ok(res === securePassword.VALID)
})

// Cancel now
setImmediate(pwd.verify(userPassword, passwordHash, function (err, res) {
assert.ok(err, 'has err')
}))
})

// Cancel now
setImmediate(pwd.hash(userPassword, function (err, passwordHash) {
assert.ok(err, 'has err')
}))
})