@moped/passwords v0.0.7
passwords
This module provides safe and secure password management for web applications. It is part of the moped suite of utilities for creating composable configs for building node.js and react apps.
This module uses secure-password to provide secure salting and hashing of passwords. In addition to that, it provides built in rate limiting and type safety.
Installation
yarn add @moped/passwords
Usage
There are two ways of using the main password store. You can have an internal store that generates and id for each password automatically, or one that accepts a password (e.g. a user id or user name). The id must always be a string
or number
.
Basic Usage:
const createPasswordStore = require('@moped/passwords');
module.exports = createPasswordStore(store, options);
Options:
option name | environment variable | type | default | description |
---|---|---|---|---|
opsLimit | process.env.PASSWORD_OPS_LIMIT | number | 30 | The operations limit value passed to secure-password |
memLimit | process.env.PASSWORD_MEM_LIMIT | bytes | 80MB | The memory limit value passed to secure-password. This can either be a number of bytes or a string like 100MB |
bucketSize | process.env.PASSWORD_BUCKET_SIZE | number | 10 | The number of password attempts before rate limiting starts |
fillRate | process.env.PASSWORD_FILL_RATE | milliseconds | 10 seconds | The minimum time between password attempts once the bucket has emptied. This can either be a number of milliseconds or a string like 20 seconds . |
Each option can be passed directly, or set via an enrivonment variable.
opsLimit
and memLimit
The opsLimit
and memLimit
control how much CPU time and how much memory is required to crack a password. Higher values will be more secure, but will also put more load on your server when users are signing in. This library will start printing warnings if these values seem to be too low, and will eventually crash in production if the values get much too low, to prevent unauthorised access.
bucketSize
and fillRate
The bucketSize
and fillRate
control the rate limiting. You can think of this as matching the following metaphor:
For each password stored in the sytem, there is a bucket full of coins. bucketSize
controlls how many coins fit in the bucket. Each time you attempt to verify a password, you must remove a token from the bucket (whether the password turns out to be right or not). If there is no token, you must wait for one to be added to the bucket. Every fillRate
ms, a new token is added to the bucket, until the bucket becomes full.
Another way of looking at it, is that you can make bucketSize
password attempts in rapid succession before rate limiting starts, after that, you can then only make one password attempt every fillRate
ms.
The data needed for rate limiting is stored along with the password hash, so rate limiting survives server crashes/restarts.
N.B. if you have multiple servers, you either need to ensure that each password can only be attempted on one server at a time, or you need to ensure that the whole verify operation takes place in a single database transaction (in a database with strong transactional consistency), otherwise an attacker could exceed the rate limmit. You do not need to worry about this if you only have a single server.
PasswordData
The data you will be given to store/retrieve for each password is an object of the form:
interface PasswordData {
numberOfPasswordAttempts: number;
timeStampOfLastReset: number;
hash: string;
}
numberOfPasswordAttempts
and timeStampOfLastReset
give us the current state of that password's rate limiting bucket and the hash
is a securely salted and hashed copy of the password.
User Specified ID
Define a store:
// passwords.js
const createPasswordStore = require('@moped/passwords');
const passwordData = new Map();
module.exports = createPasswordStore({
read(id) {
return Promise.resolve(passwordData.get(id));
}
set(id, data) {
passwordData.set(id, data);
return Promise.resolve(null);
}
});
Set a password:
const passwords = require('./passwords');
passwords.write('username', 'my password').then(() => {
console.log('password written');
}, err => {
console.log('failed ', err);
});
Verify a password:
const passwords = require('./passwords');
passwords.verify('username', 'my password').then(isCorrect => {
if (isCorrect === true) {
console.log('access granted');
} else {
console.log('incorrect password, try again');
}
}, err => {
console.log('failed ', err);
});
Augo Generated ID
Define a store:
// passwords.js
const createPasswordStore = require('@moped/passwords');
const passwordData = new Map();
let nextID = 0;
module.exports = createPasswordStore({
read(id) {
return Promise.resolve(passwordData.get(id));
}
create(data) {
const id = nextID++;
passwordData.set(id, data);
return Promise.resolve(id);
}
update(id, data) {
passwordData.set(id, data);
return Promise.resolve(null);
}
});
Set a password:
const passwords = require('./passwords');
passwords.write('username', 'my password').then(id => {
console.log('password written with id ', id);
}, err => {
console.log('failed ', err);
});
Verify a password:
const passwords = require('./passwords');
passwords.verify(id, 'my password').then(isCorrect => {
if (isCorrect === true) {
console.log('access granted');
} else {
console.log('incorrect password, try again');
}
}, err => {
console.log('failed ', err);
});
Generating a Password
The generatePassword(length, encoding)
utility lets you auto-generate secure passwords. These are useful for sending in password reset e-mails or using in passwordless login environments. You can select an encoding from 4 values.
- decimal encoding is the least secure, but is useful if you want your users to enter a pass code on a numeric keypad. Only use this for relatively short lived passwords.
- hex is marginally better than decimal, but probably not the right choice.
- base32 is a good compromise of using characters that are easy to distinguish from each other in most fonts, and easy to type, but encoding a relatively secure password for the length.
- base64 gives the best security for the length of password, but it can be error prone if typed by hand.
Longer paswords are always more secure. For short lived (less than a few hours) passwords, I recommend a minimum length of 6 characters, for longer lived passwords, I recommend a minimum length of 20 characters. Caveats:
- I'm not an expert, you should consult with a security expert to determine the best values for your application.
- I'm writing this in 2017, expect these numbers to need to grow steadily over time.
const {generatePassword, Encoding} = require('@moped/passwords');
generatePassword(10, Encoding.decimal).then(result => {
// e.g. 0123456789
console.log(result)
});
generatePassword(10, Encoding.hex).then(result => {
// e.g. f1a3f5e789
console.log(result)
});
generatePassword(10, Encoding.base32).then(result => {
// e.g. 6xa3fvek89
console.log(result)
});
generatePassword(10, Encoding.base32).then(result => {
// e.g. SxaBfvGk89
console.log(result)
});
Security
If you find potential security issues in this library, please e-mail forbes@lindesay.co.uk or report it to the node security project via https://nodesecurity.io/report
Licence
MIT