1
0
Fork 0

feat(backend): Added new and legacy password handling

This commit is contained in:
joostdecock 2022-10-31 08:36:36 +01:00
parent 34549d5c71
commit d8134314c6
8 changed files with 227 additions and 62 deletions

View file

@ -1,4 +1,6 @@
import bcrypt from 'bcryptjs' // Required for legacy password hashes
import { createHash, createCipheriv, createDecipheriv, scryptSync, randomBytes } from 'crypto'
import { log } from './log.mjs'
/*
* Cleans a string (typically email) for hashing
@ -100,3 +102,82 @@ export const encryption = (stringKey, salt = 'FreeSewing') => {
},
}
}
/*
* Salts and hashes a password
*/
function hashPassword(input, salt = false) {
if (salt === false) salt = Buffer.from(randomBytes(16))
else salt = Buffer.from(salt, 'hex')
const hash = scryptSync(input, salt, 64)
return {
hash: hash.toString('hex'),
salt: salt.toString('hex'),
}
}
/*
* Verifies a (user-provided) password against the stored hash + salt
*
* Note that:
* - For legacy password hashes, the password field will hold serialized
* JSON with a 'type' field set to 'v2' and a 'data' field holding the
* legacy hash info to pass to the verifyLegacyPassword() method below.
* - For new password hashes, the password field will hold serialized
* JSON with a 'type' field set to 'v3' and a 'hash' and 'salt' field.
* - When legacy passwords are confirmed, they will be re-hashed and
* updated in the database. The database update is not handled here but
* prepared, by returning the new value for the password field as the
* second element in the returned array.
*/
export function verifyPassword(input, passwordField) {
let data
try {
data = JSON.parse(passwordField)
} catch {
/*
* This should not happen. Let's just log a warning and return false
*/
log.warn(passwordField, 'Unable to parse JSON in password field')
return [false, false]
}
// Is this a legacy password field?
if (data.type === 'v2') {
const result = verifyLegacyPassword(input, data.data)
if (result) {
// Correct password for legacy password. Re-hash and return.
return [true, hashPassword(input)]
}
} else if (data.type === 'v3') {
if (data.hash && data.salt) {
const verify = hashPassword(input, data.salt)
if (data.hash === verify.hash && data.salt === verify.salt) {
// Son of a bitch, you're in
return [true, false]
}
}
}
return [false, false]
}
/*
* Verifies a legacy password hash
*
* Legacy means that an account was imported from the v2 FreeSewing backend
* which used MongoDB as a database with Mongoose as an ORM.
* Passwords were handles with the mongoose-bcrypt plugin and have been
* imported from a database dump.
*
* So to verify these passwords, we need to verify the original logic of
* the mongoose plugin which uses the bcryptjs library.
*
* Each time a user with a legacy password field logs in with the correct
* password, we re-hash the password field with the new (crypto) hasing method.
* This way, in a while all users will be migrated, and we can drop this method
* and the cryptojs dependency
*/
function verifyLegacyPassword(password, hash) {
return bcrypt.compareSync(password, hash)
}

View file

@ -0,0 +1,3 @@
import logger from 'pino'
export const log = logger()

View file

@ -1,45 +0,0 @@
import dateFormat from 'dateformat'
// FIXME: This needs work
const now = () => dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss')
const logWorthy = (msg, data) => {
let d = { at: now() }
switch (msg) {
case 'ping':
case 'login':
case 'wrongPassword':
case 'passwordSet':
case 'dataExport':
d.user = data.user.handle
d.from = data.req.ip
d.with = data.req.headers['user-agent']
break
case 'signupRequest':
d.email = data.email
d.confirmation = data.confirmation
break
case 'accountRemovalFailed':
d.err = data.err
d.user = data.user.handle
d.from = data.req.ip
d.with = data.req.headers['user-agent']
break
default:
d.data = data
break
}
return d
}
const log = (type, msg, data) => {
console.log(type, msg, logWorthy(msg, data))
}
log.info = (msg, data) => log('info', msg, data)
log.warning = (msg, data) => log('warning', msg, data)
log.error = (msg, data) => log('error', msg, data)
export default log