Files
NodeBB/src/user/create.js
Julian Lam 512f6de6de feat: allow passwords with length > 73 characters (#8818)
* feat: allow passwords longer than 73 characters

Context: A bcrypt/blowfish limitation means that password length is capped at 72 characters. We can get around this without compromising on security
by hashing all incoming passwords with SHA512, and then sending that to bcrypt.

https://dropbox.tech/security/how-dropbox-securely-stores-your-passwords

* feat: add additional test for passwords > 73 chars

* fix: remove 'password-too-long' error message and all invocations

* test: added test to show that a super long password won't bring down NodeBB

* fix: remove debug log

* Revert "fix: remove 'password-too-long' error message and all invocations"

This reverts commit 1e312bf7ef.

* fix: added back password length checks, but at 512 chars

As processing a large string still uses a lot of memory
2020-11-06 08:40:00 -05:00

196 lines
5.5 KiB
JavaScript

'use strict';
const zxcvbn = require('zxcvbn');
const db = require('../database');
const utils = require('../utils');
const slugify = require('../slugify');
const plugins = require('../plugins');
const groups = require('../groups');
const meta = require('../meta');
module.exports = function (User) {
User.create = async function (data) {
data.username = data.username.trim();
data.userslug = slugify(data.username);
if (data.email !== undefined) {
data.email = String(data.email).trim();
}
try {
await lock(data.username, '[[error:username-taken]]');
if (data.email) {
await lock(data.email, '[[error:email-taken]]');
}
await User.isDataValid(data);
return await create(data);
} finally {
await db.deleteObjectFields('locks', [data.username, data.email]);
}
};
async function lock(value, error) {
const count = await db.incrObjectField('locks', value);
if (count > 1) {
throw new Error(error);
}
}
async function create(data) {
const timestamp = data.timestamp || Date.now();
let userData = {
username: data.username,
userslug: data.userslug,
email: data.email || '',
joindate: timestamp,
lastonline: timestamp,
status: 'online',
};
['picture', 'fullname', 'location', 'birthday'].forEach((field) => {
if (data[field]) {
userData[field] = data[field];
}
});
if (data.gdpr_consent === true) {
userData.gdpr_consent = 1;
}
if (data.acceptTos === true) {
userData.acceptTos = 1;
}
const renamedUsername = await User.uniqueUsername(userData);
const userNameChanged = !!renamedUsername;
if (userNameChanged) {
userData.username = renamedUsername;
userData.userslug = slugify(renamedUsername);
}
const results = await plugins.fireHook('filter:user.create', { user: userData, data: data });
userData = results.user;
const uid = await db.incrObjectField('global', 'nextUid');
userData.uid = uid;
await db.setObject('user:' + uid, userData);
const bulkAdd = [
['username:uid', userData.uid, userData.username],
['user:' + userData.uid + ':usernames', timestamp, userData.username + ':' + timestamp],
['username:sorted', 0, userData.username.toLowerCase() + ':' + userData.uid],
['userslug:uid', userData.uid, userData.userslug],
['users:joindate', timestamp, userData.uid],
['users:online', timestamp, userData.uid],
['users:postcount', 0, userData.uid],
['users:reputation', 0, userData.uid],
];
if (userData.email) {
bulkAdd.push(['email:uid', userData.uid, userData.email.toLowerCase()]);
bulkAdd.push(['email:sorted', 0, userData.email.toLowerCase() + ':' + userData.uid]);
bulkAdd.push(['user:' + userData.uid + ':emails', timestamp, userData.email + ':' + timestamp]);
}
if (userData.fullname) {
bulkAdd.push(['fullname:sorted', 0, userData.fullname.toLowerCase() + ':' + userData.uid]);
}
const groupsToJoin = ['registered-users'].concat(
parseInt(userData.uid, 10) !== 1 ?
'unverified-users' : 'verified-users'
);
await Promise.all([
db.incrObjectField('global', 'userCount'),
db.sortedSetAddBulk(bulkAdd),
groups.join(groupsToJoin, userData.uid),
User.notifications.sendWelcomeNotification(userData.uid),
storePassword(userData.uid, data.password),
User.updateDigestSetting(userData.uid, meta.config.dailyDigestFreq),
]);
if (userData.email && userData.uid > 1 && meta.config.requireEmailConfirmation) {
User.email.sendValidationEmail(userData.uid, {
email: userData.email,
});
}
if (userNameChanged) {
await User.notifications.sendNameChangeNotification(userData.uid, userData.username);
}
plugins.fireHook('action:user.create', { user: userData, data: data });
return userData.uid;
}
async function storePassword(uid, password) {
if (!password) {
return;
}
const hash = await User.hashPassword(password);
await Promise.all([
User.setUserFields(uid, {
password: hash,
'password:shaWrapped': 1,
}),
User.reset.updateExpiry(uid),
]);
}
User.isDataValid = async function (userData) {
if (userData.email && !utils.isEmailValid(userData.email)) {
throw new Error('[[error:invalid-email]]');
}
if (!utils.isUserNameValid(userData.username) || !userData.userslug) {
throw new Error('[[error:invalid-username, ' + userData.username + ']]');
}
if (userData.password) {
User.isPasswordValid(userData.password);
}
if (userData.email) {
const available = await User.email.available(userData.email);
if (!available) {
throw new Error('[[error:email-taken]]');
}
}
};
User.isPasswordValid = function (password, minStrength) {
minStrength = (minStrength || minStrength === 0) ? minStrength : meta.config.minimumPasswordStrength;
// Sanity checks: Checks if defined and is string
if (!password || !utils.isPasswordValid(password)) {
throw new Error('[[error:invalid-password]]');
}
if (password.length < meta.config.minimumPasswordLength) {
throw new Error('[[reset_password:password_too_short]]');
}
if (password.length > 512) {
throw new Error('[[error:password-too-long]]');
}
const strength = zxcvbn(password);
if (strength.score < minStrength) {
throw new Error('[[user:weak_password]]');
}
};
User.uniqueUsername = async function (userData) {
let numTries = 0;
let username = userData.username;
while (true) {
/* eslint-disable no-await-in-loop */
const exists = await meta.userOrGroupExists(username);
if (!exists) {
return numTries ? username : null;
}
username = userData.username + ' ' + numTries.toString(32);
numTries += 1;
}
};
};