Migrate to bcrypt (#8446)

* start migrating to bcrypt

* added method to convert the password to bcrypt when logging in, added method to compare password without knowing the hashing algorhytm, remove default

* travis: try to upgrade to container based infrastructure

* travis: add deps to build bcrypt.js

* travis: add deps to build bcrypt.js

* travis: add deps to build bcrypt.js

* travis: add deps to build bcrypt.js

* use bcryptjs until bcrypt can be installed on travis, see https://github.com/kelektiv/node.bcrypt.js/issues/476

* correct sha1 unit tests

* try different mongodb repo

* try without mognodb services

* try again with bcrypt

* disable request logging in travis

* migrate missing routes

* simplify code

* remove bcryptjs

* fix typo

* fix typo

* fix typo in comment

* add unit tests for new passwords utility emthods

* travis: back to old infrastructure, containers often have timeouts

* add integration test for passwordHashMethod

* update shrinkwrap

* clarify code and add comments

* add integration tests

* fix linting

* fix integration tests
This commit is contained in:
Matteo Pagliazzi
2017-01-24 12:28:42 +01:00
committed by GitHub
parent 04f4eb8490
commit acad3b8873
16 changed files with 1439 additions and 722 deletions

View File

@@ -1,7 +1,15 @@
language: node_js language: node_js
node_js: node_js:
- '6' - '6'
sudo: required
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
before_install: before_install:
- $CXX --version
- npm install -g npm@4 - npm install -g npm@4
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi - if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
before_script: before_script:
@@ -12,6 +20,9 @@ after_script:
- ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js - ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js
script: npm run $TEST script: npm run $TEST
env: env:
global:
- CXX=g++-4.8
- DISABLE_REQUEST_LOGGING=true
matrix: matrix:
- TEST="lint" - TEST="lint"
- TEST="test:api-v3" REQUIRES_SERVER=true - TEST="test:api-v3" REQUIRES_SERVER=true

1562
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@
"babel-register": "^6.6.0", "babel-register": "^6.6.0",
"babel-runtime": "^6.11.6", "babel-runtime": "^6.11.6",
"babelify": "^7.2.0", "babelify": "^7.2.0",
"bcrypt": "^1.0.2",
"bluebird": "^3.3.5", "bluebird": "^3.3.5",
"body-parser": "^1.15.0", "body-parser": "^1.15.0",
"bower": "~1.3.12", "bower": "~1.3.12",

View File

@@ -11,6 +11,10 @@ import {
map, map,
} from 'lodash'; } from 'lodash';
import Bluebird from 'bluebird'; import Bluebird from 'bluebird';
import {
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../website/server/libs/password';
describe('DELETE /user', () => { describe('DELETE /user', () => {
let user; let user;
@@ -67,6 +71,30 @@ describe('DELETE /user', () => {
await expect(checkExistence('users', user._id)).to.eventually.eql(false); await expect(checkExistence('users', user._id)).to.eventually.eql(false);
}); });
it('deletes the user with a legacy sha1 password', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// delete the user
await user.del('/user', {
password: textPassword,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
context('last member of a party', () => { context('last member of a party', () => {
let party; let party;

View File

@@ -23,6 +23,7 @@ describe('GET /user', () => {
let returnedUser = await user.get('/user'); let returnedUser = await user.get('/user');
expect(returnedUser.auth.local.hashed_password).to.not.exist; expect(returnedUser.auth.local.hashed_password).to.not.exist;
expect(returnedUser.auth.local.passwordHashMethod).to.not.exist;
expect(returnedUser.auth.local.salt).to.not.exist; expect(returnedUser.auth.local.salt).to.not.exist;
expect(returnedUser.apiToken).to.not.exist; expect(returnedUser.apiToken).to.not.exist;
}); });

View File

@@ -3,6 +3,11 @@ import {
requester, requester,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import {
bcryptCompare,
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../../website/server/libs/password';
describe('POST /user/auth/local/login', () => { describe('POST /user/auth/local/login', () => {
let api; let api;
@@ -72,4 +77,35 @@ describe('POST /user/auth/local/login', () => {
message: t('invalidReqParams'), message: t('invalidReqParams'),
}); });
}); });
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// login
await api.post(endpoint, {
username: user.auth.local.email,
password: textPassword,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
expect(user.auth.local.salt).to.be.undefined;
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
expect(isValidPassword).to.equal(true);
});
}); });

View File

@@ -2,6 +2,10 @@ import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import {
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../../website/server/libs/password';
describe('POST /user/reset-password', async () => { describe('POST /user/reset-password', async () => {
let endpoint = '/user/reset-password'; let endpoint = '/user/reset-password';
@@ -21,6 +25,33 @@ describe('POST /user/reset-password', async () => {
expect(user.auth.local.hashed_password).to.not.eql(previousPassword); expect(user.auth.local.hashed_password).to.not.eql(previousPassword);
}); });
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// update email
await user.post(endpoint, {
email: user.auth.local.email,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
expect(user.auth.local.salt).to.be.undefined;
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
});
it('same message on error as on success', async () => { it('same message on error as on success', async () => {
let response = await user.post(endpoint, { let response = await user.post(endpoint, {
email: 'nonExistent@email.com', email: 'nonExistent@email.com',

View File

@@ -2,6 +2,11 @@ import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../../helpers/api-v3-integration.helper'; } from '../../../../../helpers/api-v3-integration.helper';
import {
bcryptCompare,
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../../website/server/libs/password';
const ENDPOINT = '/user/auth/update-email'; const ENDPOINT = '/user/auth/update-email';
@@ -66,6 +71,41 @@ describe('PUT /user/auth/update-email', () => {
message: t('cannotFulfillReq'), message: t('cannotFulfillReq'),
}); });
}); });
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
let myNewEmail = 'my-new-random-email@example.net';
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// update email
let response = await user.put(ENDPOINT, {
newEmail: myNewEmail,
password: textPassword,
});
expect(response).to.eql({ email: myNewEmail });
await user.sync();
expect(user.auth.local.email).to.equal(myNewEmail);
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
expect(user.auth.local.salt).to.be.undefined;
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
expect(isValidPassword).to.equal(true);
});
}); });
context('Social Login User', async () => { context('Social Login User', async () => {

View File

@@ -2,6 +2,11 @@ import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../../helpers/api-v3-integration.helper'; } from '../../../../../helpers/api-v3-integration.helper';
import {
bcryptCompare,
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../../website/server/libs/password';
const ENDPOINT = '/user/auth/update-password'; const ENDPOINT = '/user/auth/update-password';
@@ -89,4 +94,36 @@ describe('PUT /user/auth/update-password', async () => {
message: t('invalidReqParams'), message: t('invalidReqParams'),
}); });
}); });
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// update email
await user.put(ENDPOINT, {
password: textPassword,
newPassword,
confirmPassword: newPassword,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
expect(user.auth.local.salt).to.be.undefined;
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
let isValidPassword = await bcryptCompare(newPassword, user.auth.local.hashed_password);
expect(isValidPassword).to.equal(true);
});
}); });

View File

@@ -2,6 +2,11 @@ import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../../helpers/api-v3-integration.helper'; } from '../../../../../helpers/api-v3-integration.helper';
import {
bcryptCompare,
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../../website/server/libs/password';
const ENDPOINT = '/user/auth/update-username'; const ENDPOINT = '/user/auth/update-username';
@@ -24,6 +29,41 @@ describe('PUT /user/auth/update-username', async () => {
expect(user.auth.local.username).to.eql(newUsername); expect(user.auth.local.username).to.eql(newUsername);
}); });
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
let myNewUsername = 'my-new-username';
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// update email
let response = await user.put(ENDPOINT, {
username: myNewUsername,
password: textPassword,
});
expect(response).to.eql({ username: myNewUsername });
await user.sync();
expect(user.auth.local.username).to.eql(myNewUsername);
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
expect(user.auth.local.salt).to.be.undefined;
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
expect(isValidPassword).to.equal(true);
});
context('errors', async () => { context('errors', async () => {
it('prevents username update if new username is already taken', async () => { it('prevents username update if new username is already taken', async () => {
let existingUsername = 'existing-username'; let existingUsername = 'existing-username';

View File

@@ -1,41 +1,240 @@
/* eslint-disable camelcase */
import { import {
encrypt as encryptPassword, sha1Encrypt as sha1EncryptPassword,
makeSalt, sha1MakeSalt,
bcryptHash,
bcryptCompare,
compare,
convertToBcrypt,
} from '../../../../../website/server/libs/password'; } from '../../../../../website/server/libs/password';
describe('Password Utilities', () => { describe('Password Utilities', () => {
describe('Encrypt', () => { describe('compare', () => {
it('always encrypt the same password to the same value when using the same salt', () => { it('can compare a correct password hashed with SHA1', async () => {
let textPassword = 'mySecretPassword'; let textPassword = 'mySecretPassword';
let salt = makeSalt(); let salt = sha1MakeSalt();
let encryptedPassword = encryptPassword(textPassword, salt); let hashedPassword = sha1EncryptPassword(textPassword, salt);
expect(encryptPassword(textPassword, salt)).to.eql(encryptedPassword); let user = {
auth: {
local: {
hashed_password: hashedPassword,
salt,
passwordHashMethod: 'sha1',
},
},
};
let isValidPassword = await compare(user, textPassword);
expect(isValidPassword).to.eql(true);
}); });
it('never encrypt the same password to the same value when using a different salt', () => { it('can compare an invalid password hashed with SHA1', async () => {
let textPassword = 'mySecretPassword'; let textPassword = 'mySecretPassword';
let aSalt = makeSalt(); let salt = sha1MakeSalt();
let anotherSalt = makeSalt(); let hashedPassword = sha1EncryptPassword(textPassword, salt);
let anEncryptedPassword = encryptPassword(textPassword, aSalt);
let anotherEncryptedPassword = encryptPassword(textPassword, anotherSalt);
expect(anEncryptedPassword).not.to.eql(anotherEncryptedPassword); let user = {
auth: {
local: {
hashed_password: hashedPassword,
salt,
passwordHashMethod: 'sha1',
},
},
};
let isValidPassword = await compare(user, 'wrongPassword');
expect(isValidPassword).to.eql(false);
});
it('can compare a correct password hashed with bcrypt', async () => {
let textPassword = 'mySecretPassword';
let hashedPassword = await bcryptHash(textPassword);
let user = {
auth: {
local: {
hashed_password: hashedPassword,
passwordHashMethod: 'bcrypt',
},
},
};
let isValidPassword = await compare(user, textPassword);
expect(isValidPassword).to.eql(true);
});
it('can compare an invalid password hashed with bcrypt', async () => {
let textPassword = 'mySecretPassword';
let hashedPassword = await bcryptHash(textPassword);
let user = {
auth: {
local: {
hashed_password: hashedPassword,
passwordHashMethod: 'bcrypt',
},
},
};
let isValidPassword = await compare(user, 'wrongPassword');
expect(isValidPassword).to.eql(false);
});
it('throws an error if user is missing', async () => {
try {
await compare(null, 'some password');
} catch (e) {
expect(e.toString()).to.equal('Error: user and passwordToCheck are required parameters.');
}
});
it('throws an error if passwordToCheck is missing', async () => {
try {
await compare({a: true});
} catch (e) {
expect(e.toString()).to.equal('Error: user and passwordToCheck are required parameters.');
}
});
it('throws an error if an invalid hashing method is used', async () => {
try {
await compare({
auth: {
local: {
passwordHashMethod: 'invalid',
},
},
}, 'pass');
} catch (e) {
expect(e.toString()).to.equal('Error: Invalid password hash method.');
}
});
it('returns true if comparing the same password', async () => {
let textPassword = 'mySecretPassword';
let hashedPassword = await bcryptHash(textPassword);
let isValidPassword = await bcryptCompare(textPassword, hashedPassword);
expect(isValidPassword).to.eql(true);
});
it('returns true if comparing a different password', async () => {
let textPassword = 'mySecretPassword';
let hashedPassword = await bcryptHash(textPassword);
let isValidPassword = await bcryptCompare('anotherPassword', hashedPassword);
expect(isValidPassword).to.eql(false);
}); });
}); });
describe('Make Salt', () => { describe('convertToBcrypt', () => {
it('creates a salt with length 10 by default', () => { it('converts an user password hashed with sha1 to bcrypt', async () => {
let salt = makeSalt(); let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let hashedPassword = sha1EncryptPassword(textPassword, salt);
expect(salt.length).to.eql(10); let user = {
auth: {
local: {
hashed_password: hashedPassword,
salt,
passwordHashMethod: 'sha1',
},
},
};
await convertToBcrypt(user, textPassword);
expect(user.auth.local.salt).to.be.undefined;
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
expect(user.auth.local.hashed_password).to.be.a.string;
let isValidPassword = await compare(user, textPassword);
expect(isValidPassword).to.eql(true);
}); });
it('can create a salt of any length', () => { it('throws an error if user is missing', async () => {
let length = 24; try {
let salt = makeSalt(length); await convertToBcrypt(null, 'string');
} catch (e) {
expect(e.toString()).to.equal('Error: user and plainTextPassword are required parameters.');
}
});
expect(salt.length).to.eql(length); it('throws an error if plainTextPassword is missing', async () => {
try {
await convertToBcrypt({a: true});
} catch (e) {
expect(e.toString()).to.equal('Error: user and plainTextPassword are required parameters.');
}
});
});
describe('bcrypt', () => {
describe('Hash', () => {
it('returns a hashed string', async () => {
let textPassword = 'mySecretPassword';
let hashedPassword = await bcryptHash(textPassword);
expect(hashedPassword).to.be.a.string;
});
});
describe('Compare', () => {
it('returns true if comparing the same password', async () => {
let textPassword = 'mySecretPassword';
let hashedPassword = await bcryptHash(textPassword);
let isValidPassword = await bcryptCompare(textPassword, hashedPassword);
expect(isValidPassword).to.eql(true);
});
it('returns true if comparing a different password', async () => {
let textPassword = 'mySecretPassword';
let hashedPassword = await bcryptHash(textPassword);
let isValidPassword = await bcryptCompare('anotherPassword', hashedPassword);
expect(isValidPassword).to.eql(false);
});
});
});
describe('SHA1', () => {
describe('Encrypt', () => {
it('always encrypt the same password to the same value when using the same salt', () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let encryptedPassword = sha1EncryptPassword(textPassword, salt);
expect(sha1EncryptPassword(textPassword, salt)).to.eql(encryptedPassword);
});
it('never encrypt the same password to the same value when using a different salt', () => {
let textPassword = 'mySecretPassword';
let aSalt = sha1MakeSalt();
let anotherSalt = sha1MakeSalt();
let anEncryptedPassword = sha1EncryptPassword(textPassword, aSalt);
let anotherEncryptedPassword = sha1EncryptPassword(textPassword, anotherSalt);
expect(anEncryptedPassword).not.to.eql(anotherEncryptedPassword);
});
});
describe('Make Salt', () => {
it('creates a salt with length 10 by default', () => {
let salt = sha1MakeSalt();
expect(salt.length).to.eql(10);
});
it('can create a salt of any length', () => {
let length = 24;
let salt = sha1MakeSalt(length);
expect(salt.length).to.eql(length);
});
}); });
}); });
}); });

View File

@@ -55,8 +55,8 @@ export async function resetHabiticaDB () {
username: 'username', username: 'username',
lowerCaseUsername: 'username', lowerCaseUsername: 'username',
email: 'username@email.com', email: 'username@email.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase hashed_password: 'hashed_password', // eslint-disable-line camelcase
passwordHashMethod: 'bcrypt',
}, },
}, },
}, (insertErr) => { }, (insertErr) => {

View File

@@ -115,17 +115,15 @@ api.registerLocal = {
if (lowerCaseUsername === user.auth.local.lowerCaseUsername) throw new NotAuthorized(res.t('usernameTaken')); if (lowerCaseUsername === user.auth.local.lowerCaseUsername) throw new NotAuthorized(res.t('usernameTaken'));
} }
let salt = passwordUtils.makeSalt(); let hashed_password = await passwordUtils.bcryptHash(password); // eslint-disable-line camelcase
let hashed_password = passwordUtils.encrypt(password, salt); // eslint-disable-line camelcase
let newUser = { let newUser = {
auth: { auth: {
local: { local: {
username, username,
lowerCaseUsername, lowerCaseUsername,
email, email,
salt, hashed_password, // eslint-disable-line camelcase,
hashed_password, // eslint-disable-line camelcase passwordHashMethod: 'bcrypt',
passwordHashMethod: 'sha1',
}, },
}, },
preferences: { preferences: {
@@ -223,6 +221,7 @@ api.loginLocal = {
let login; let login;
let username = req.body.username; let username = req.body.username;
let password = req.body.password;
if (validator.isEmail(username)) { if (validator.isEmail(username)) {
login = {'auth.local.email': username.toLowerCase()}; // Emails are stored lowercase login = {'auth.local.email': username.toLowerCase()}; // Emails are stored lowercase
@@ -230,10 +229,25 @@ api.loginLocal = {
login = {'auth.local.username': username}; login = {'auth.local.username': username};
} }
let user = await User.findOne(login, {auth: 1, apiToken: 1}).exec(); // load the entire user because we may have to save it to convert the password to bcrypt
let isValidPassword = user && user.auth.local.hashed_password === passwordUtils.encrypt(req.body.password, user.auth.local.salt); let user = await User.findOne(login).exec();
let isValidPassword;
if (!user) {
isValidPassword = false;
} else {
isValidPassword = await passwordUtils.compare(user, password);
}
if (!isValidPassword) throw new NotAuthorized(res.t('invalidLoginCredentialsLong')); if (!isValidPassword) throw new NotAuthorized(res.t('invalidLoginCredentialsLong'));
// convert the hashed password to bcrypt from sha1
if (user.auth.local.passwordHashMethod === 'sha1') {
await passwordUtils.convertToBcrypt(user, password);
await user.save();
}
res.analytics.track('login', { res.analytics.track('login', {
category: 'behaviour', category: 'behaviour',
type: 'local', type: 'local',
@@ -433,12 +447,18 @@ api.updateUsername = {
if (!user.auth.local.username) throw new BadRequest(res.t('userHasNoLocalRegistration')); if (!user.auth.local.username) throw new BadRequest(res.t('userHasNoLocalRegistration'));
let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt); let password = req.body.password;
if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword')); let isValidPassword = await passwordUtils.compare(user, password);
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
let count = await User.count({ 'auth.local.lowerCaseUsername': req.body.username.toLowerCase() }); let count = await User.count({ 'auth.local.lowerCaseUsername': req.body.username.toLowerCase() });
if (count > 0) throw new BadRequest(res.t('usernameTaken')); if (count > 0) throw new BadRequest(res.t('usernameTaken'));
// if password is using old sha1 encryption, change it
if (user.auth.local.passwordHashMethod === 'sha1') {
await passwordUtils.convertToBcrypt(user, password); // user is saved a few lines below
}
// save username // save username
user.auth.local.lowerCaseUsername = req.body.username.toLowerCase(); user.auth.local.lowerCaseUsername = req.body.username.toLowerCase();
user.auth.local.username = req.body.username; user.auth.local.username = req.body.username;
@@ -487,13 +507,17 @@ api.updatePassword = {
throw validationErrors; throw validationErrors;
} }
let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt); let oldPassword = req.body.password;
if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword')); let isValidPassword = await passwordUtils.compare(user, oldPassword);
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
if (req.body.newPassword !== req.body.confirmPassword) throw new NotAuthorized(res.t('passwordConfirmationMatch')); let newPassword = req.body.newPassword;
if (newPassword !== req.body.confirmPassword) throw new NotAuthorized(res.t('passwordConfirmationMatch'));
user.auth.local.hashed_password = passwordUtils.encrypt(req.body.newPassword, user.auth.local.salt); // eslint-disable-line camelcase // set new password and make sure it's using bcrypt for hashing
await passwordUtils.convertToBcrypt(user, newPassword);
await user.save(); await user.save();
res.respond(200, {}); res.respond(200, {});
}, },
}; };
@@ -522,15 +546,15 @@ api.resetPassword = {
if (validationErrors) throw validationErrors; if (validationErrors) throw validationErrors;
let email = req.body.email.toLowerCase(); let email = req.body.email.toLowerCase();
let salt = passwordUtils.makeSalt();
let newPassword = passwordUtils.makeSalt(); // use a salt as the new password too (they'll change it later)
let hashedPassword = passwordUtils.encrypt(newPassword, salt);
let user = await User.findOne({ 'auth.local.email': email }).exec(); let user = await User.findOne({ 'auth.local.email': email }).exec();
if (user) { if (user) {
user.auth.local.salt = salt; // use a salt as the new password too (they'll change it later)
user.auth.local.hashed_password = hashedPassword; // eslint-disable-line camelcase let newPassword = passwordUtils.sha1MakeSalt();
// set new password and make sure it's using bcrypt for hashing
await passwordUtils.convertToBcrypt(user, newPassword); // user is saved a few lines below
sendEmail({ sendEmail({
from: 'Habitica <admin@habitica.com>', from: 'Habitica <admin@habitica.com>',
to: email, to: email,
@@ -585,8 +609,14 @@ api.updateEmail = {
if (emailAlreadyInUse) throw new NotAuthorized(res.t('cannotFulfillReq')); if (emailAlreadyInUse) throw new NotAuthorized(res.t('cannotFulfillReq'));
let candidatePassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt); let password = req.body.password;
if (candidatePassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword')); let isValidPassword = await passwordUtils.compare(user, password);
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
// if password is using old sha1 encryption, change it
if (user.auth.local.passwordHashMethod === 'sha1') {
await passwordUtils.convertToBcrypt(user, password);
}
user.auth.local.email = req.body.newEmail; user.auth.local.email = req.body.newEmail;
await user.save(); await user.save();

View File

@@ -200,8 +200,9 @@ api.deleteUser = {
let validationErrors = req.validationErrors(); let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors; if (validationErrors) throw validationErrors;
let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt); let password = req.body.password;
if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword')); let isValidPassword = await passwordUtils.compare(user, password);
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
if (plan && plan.customerId && !plan.dateTerminated) { if (plan && plan.customerId && !plan.dateTerminated) {
throw new NotAuthorized(res.t('cannotDeleteActiveAccount')); throw new NotAuthorized(res.t('cannotDeleteActiveAccount'));
@@ -256,6 +257,7 @@ api.getUserAnonymized = {
if (user.auth) { if (user.auth) {
delete user.auth.local; delete user.auth.local;
delete user.auth.facebook; delete user.auth.facebook;
delete user.auth.google;
} }
delete user.newMessages; delete user.newMessages;
delete user.profile; delete user.profile;

View File

@@ -1,8 +1,22 @@
// Utilities for working with passwords // Utilities for working with passwords
import crypto from 'crypto'; import crypto from 'crypto';
import bcrypt from 'bcrypt';
const BCRYPT_SALT_ROUNDS = 10;
// Hash a plain text password
export function bcryptHash (passwordToHash) {
return bcrypt.hash(passwordToHash, BCRYPT_SALT_ROUNDS); // returns a promise
}
// Check if a plain text password matches a hash
export function bcryptCompare (passwordToCheck, hashedPassword) {
return bcrypt.compare(passwordToCheck, hashedPassword); // returns a promise
}
// Return the encrypted version of a password (using sha1) given a salt // Return the encrypted version of a password (using sha1) given a salt
export function encrypt (password, salt) { // Used for legacy passwords that have not yet been migrated to bcrypt
export function sha1Encrypt (password, salt) {
return crypto return crypto
.createHmac('sha1', salt) .createHmac('sha1', salt)
.update(password) .update(password)
@@ -10,9 +24,42 @@ export function encrypt (password, salt) {
} }
// Create a salt, default length is 10 // Create a salt, default length is 10
export function makeSalt (len = 10) { export function sha1MakeSalt (len = 10) {
return crypto return crypto
.randomBytes(Math.ceil(len / 2)) .randomBytes(Math.ceil(len / 2))
.toString('hex') .toString('hex')
.substring(0, len); .substring(0, len);
} }
// Compare the password for an user
// Works with bcrypt and sha1 indipendently
// An async function is used so that a promise is always returned
// even for comparing sha1 hashed passwords that use a sync method
export async function compare (user, passwordToCheck) {
if (!user || !passwordToCheck) throw new Error('user and passwordToCheck are required parameters.');
let passwordHashMethod = user.auth.local.passwordHashMethod;
let passwordHash = user.auth.local.hashed_password;
let passwordSalt = user.auth.local.salt; // Only used for SHA1
if (passwordHashMethod === 'bcrypt') {
return await bcryptCompare(passwordToCheck, passwordHash);
// default to sha1 if the user has a salt but no passwordHashMethod
} else if (passwordHashMethod === 'sha1' || !passwordHashMethod && passwordSalt) {
return passwordHash === sha1Encrypt(passwordToCheck, passwordSalt);
} else {
throw new Error('Invalid password hash method.');
}
}
// Convert an user to use bcrypt from sha1 for password hashing
// needs to save the user separately.
// NOTE: before calling this method it should be verified that the supplied plain text password
// is indeed hashed with sha1 and is valid
export async function convertToBcrypt (user, plainTextPassword) {
if (!user || !plainTextPassword) throw new Error('user and plainTextPassword are required parameters.');
user.auth.local.salt = undefined;
user.auth.local.passwordHashMethod = 'bcrypt';
user.auth.local.hashed_password = await bcryptHash(plainTextPassword); // eslint-disable-line camelcase
}

View File

@@ -28,7 +28,7 @@ import {
} from './language'; } from './language';
const IS_PROD = nconf.get('IS_PROD'); const IS_PROD = nconf.get('IS_PROD');
const DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING'); const DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING') === 'true';
const PUBLIC_DIR = path.join(__dirname, '/../../client-old'); const PUBLIC_DIR = path.join(__dirname, '/../../client-old');
const SESSION_SECRET = nconf.get('SESSION_SECRET'); const SESSION_SECRET = nconf.get('SESSION_SECRET');