compare passwords against haveibeenpwned

This commit is contained in:
Andris Reinman 2018-09-12 13:00:36 +03:00
parent 9c7da7cd6e
commit 2a7b3db231
7 changed files with 56 additions and 11 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-09-11T12:11:47.264Z", "url": "http://apidocjs.com", "version": "0.17.6" } });
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-09-12T10:00:20.826Z", "url": "http://apidocjs.com", "version": "0.17.6" } });

View file

@ -1 +1 @@
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-09-11T12:11:47.264Z", "url": "http://apidocjs.com", "version": "0.17.6" } }
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-09-12T10:00:20.826Z", "url": "http://apidocjs.com", "version": "0.17.6" } }

View file

@ -12,6 +12,7 @@ const libmime = require('libmime');
const consts = require('../consts');
const roles = require('../roles');
const util = require('util');
const pwnedpasswords = require('pwnedpasswords');
module.exports = (db, server, userHandler) => {
const createUser = util.promisify(userHandler.create.bind(userHandler));
@ -311,6 +312,7 @@ module.exports = (db, server, userHandler) => {
* @apiParam {String} [name] Name of the User
* @apiParam {String} password Password for the account. Set to boolean <code>false</code> to disable password usage
* @apiParam {Boolean} [hashedPassword] If <code>true</code> then password is already hashed, so store as. Hash needs to be bcrypt <code>$2a</code>, <code>$2y</code> or <code>$2b</code>. Additionally md5 hashes <code>$1</code> are allowed but these are rehashed on first successful authentication
* @apiParam {Boolean} [allowUnsafe=true] If <code>false</code> then validates provided passwords against Have I Been Pwned API
* @apiParam {String} [address] Default email address for the User (autogenerated if not set)
* @apiParam {Boolean} [emptyAddress] If true then do not autogenerate missing email address for the User. Only needed if you want to create an user account that does not have any email address associated
* @apiParam {Boolean} [requirePasswordChange] If true then requires the user to change password, useful if password for the account was autogenerated
@ -392,13 +394,17 @@ module.exports = (db, server, userHandler) => {
.max(128)
.required(),
password: Joi.string()
.allow(false)
.max(256)
.allow([false, ''])
.required(),
hashedPassword: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.default(false),
allowUnsafe: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.default(true),
address: Joi.string().email(),
emptyAddress: Joi.boolean()
@ -499,6 +505,21 @@ module.exports = (db, server, userHandler) => {
return next();
}
if (result.value.password && !result.value.hashedPassword && !result.value.allowUnsafe) {
try {
let count = await pwnedpasswords(result.value.password);
if (count) {
res.json({
error: 'Provided password is not secure',
code: 'InsecurePasswordError'
});
return next();
}
} catch (E) {
// ignore errors, soft check only
}
}
// permissions check
req.validate(roles.can(req.role).createAny('users'));
@ -1050,6 +1071,7 @@ module.exports = (db, server, userHandler) => {
* @apiParam {String} [existingPassword] If provided then validates against account password before applying any changes
* @apiParam {String} [password] New password for the account. Set to boolean <code>false</code> to disable password usage
* @apiParam {Boolean} [hashedPassword] If <code>true</code> then password is already hashed, so store as. Hash needs to be bcrypt <code>$2a</code>, <code>$2y</code> or <code>$2b</code>. Additionally md5 hashes <code>$1</code> are allowed but these are rehashed on first successful authentication
* @apiParam {Boolean} [allowUnsafe=true] If <code>false</code> then validates provided passwords against Have I Been Pwned API
* @apiParam {String[]} [tags] A list of tags associated with this user
* @apiParam {Number} [retention] Default retention time in ms. Set to <code>0</code> to disable
* @apiParam {Boolean} [encryptMessages] If <code>true</code> then received messages are encrypted
@ -1110,9 +1132,16 @@ module.exports = (db, server, userHandler) => {
.min(1)
.max(256),
password: Joi.string()
.min(8)
.max(256)
.allow(false),
.allow([false, '']),
hashedPassword: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.default(false),
allowUnsafe: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.default(true),
language: Joi.string()
.min(2)
@ -1199,6 +1228,21 @@ module.exports = (db, server, userHandler) => {
req.validate(roles.can(req.role).updateAny('users'));
}
if (result.value.password && !result.value.hashedPassword && !result.value.allowUnsafe) {
try {
let count = await pwnedpasswords(result.value.password);
if (count) {
res.json({
error: 'Provided password is not secure',
code: 'InsecurePasswordError'
});
return next();
}
} catch (E) {
// ignore errors, soft check only
}
}
let user = new ObjectID(result.value.user);
let targets = result.value.targets;

View file

@ -1042,7 +1042,7 @@ class UserHandler {
let hashPassword = done => {
if (!data.password) {
// Users with an empty password can not log in
return done();
return done(null, '');
}
if (data.hashedPassword) {
@ -2534,7 +2534,7 @@ class UserHandler {
let flushKeys = [];
Object.keys(data).forEach(key => {
if (['user', 'existingPassword', 'ip', 'sess'].includes(key)) {
if (['user', 'existingPassword', 'hashedPassword', 'allowUnsafe', 'ip', 'sess'].includes(key)) {
return;
}
@ -2543,7 +2543,7 @@ class UserHandler {
}
if (key === 'password') {
if (data[key] === false) {
if (!data[key]) {
// removes current password (if set)
$set.password = '';
} else {

View file

@ -64,7 +64,8 @@
"nodemailer": "4.6.8",
"npmlog": "4.1.2",
"openpgp": "4.0.1",
"pem": "1.12.6",
"pem": "1.13.0",
"pwnedpasswords": "1.0.4",
"qrcode": "1.2.2",
"restify": "7.2.1",
"restify-logger": "2.0.1",