mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 14:17:22 +01:00
* remove some unused dependencies * update mongoose version * make common tests pass * Make unit tests pass * make api v3 integration tests pass * fix lint issues * fix issue with package-lock * fix(lint): we don't need no .js * fix(lint): update to latest config-habitrpg * chore(npm): update package locks * fix(test): replace deprecated fn * chore(package): update eslint-habitrpg again * fix(lint): server linting * fix(lint): client linting * fix(client): correct mangled common imports * chore(npm): update package-locks * fix(lint): punctuation, module --------- Co-authored-by: SabreCat <sabrecat@gmail.com> Co-authored-by: SabreCat <sabe@habitica.com>
101 lines
3.5 KiB
JavaScript
101 lines
3.5 KiB
JavaScript
// Utilities for working with passwords
|
|
import crypto from 'crypto';
|
|
import bcrypt from 'bcrypt';
|
|
import moment from 'moment';
|
|
import { decrypt } from './encryption';
|
|
import { model as User } from '../models/user';
|
|
|
|
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
|
|
// Used for legacy passwords that have not yet been migrated to bcrypt
|
|
export function sha1Encrypt (password, salt) {
|
|
return crypto
|
|
.createHmac('sha1', salt)
|
|
.update(password)
|
|
.digest('hex');
|
|
}
|
|
|
|
// Create a salt, default length is 10
|
|
export function sha1MakeSalt (len = 10) {
|
|
return crypto
|
|
.randomBytes(Math.ceil(len / 2))
|
|
.toString('hex')
|
|
.substring(0, len);
|
|
}
|
|
|
|
// Compare the password for an user
|
|
// Works with bcrypt and sha1 independently
|
|
// 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.');
|
|
|
|
const { passwordHashMethod } = user.auth.local;
|
|
const passwordHash = user.auth.local.hashed_password;
|
|
const passwordSalt = user.auth.local.salt; // Only used for SHA1
|
|
|
|
if (passwordHashMethod === 'bcrypt') {
|
|
return bcryptCompare(passwordToCheck, passwordHash);
|
|
// default to sha1 if the user has a salt but no passwordHashMethod
|
|
} if (passwordHashMethod === 'sha1' || (!passwordHashMethod && passwordSalt)) {
|
|
return passwordHash === sha1Encrypt(passwordToCheck, passwordSalt);
|
|
}
|
|
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, max-len
|
|
}
|
|
|
|
// Returns the user if a valid password reset code is supplied, otherwise false
|
|
export async function validatePasswordResetCodeAndFindUser (code) {
|
|
let isCodeValid = true;
|
|
|
|
let userId;
|
|
let user;
|
|
let decryptedPasswordResetCode;
|
|
|
|
// wrapping the code in a try to be able to handle the error here
|
|
try {
|
|
decryptedPasswordResetCode = JSON.parse(decrypt(code || 'invalid')); // also catches missing code
|
|
userId = decryptedPasswordResetCode.userId;
|
|
const { expiresAt } = decryptedPasswordResetCode;
|
|
|
|
if (moment(expiresAt).isBefore(moment())) throw new Error();
|
|
} catch (err) {
|
|
isCodeValid = false;
|
|
}
|
|
|
|
if (isCodeValid) {
|
|
user = await User.findById(userId).exec();
|
|
// check if user is found
|
|
if (!user) {
|
|
isCodeValid = false;
|
|
} else if (code !== user.auth.local.passwordResetCode) {
|
|
// Make sure only the last code can be used
|
|
isCodeValid = false;
|
|
}
|
|
}
|
|
|
|
return isCodeValid ? user : false;
|
|
}
|