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

@ -12,8 +12,10 @@
},
"dependencies": {
"@prisma/client": "4.5.0",
"bcryptjs": "^2.4.3",
"crypto": "^1.0.1",
"express": "4.18.2"
"express": "4.18.2",
"pino": "^8.7.0"
},
"devDependencies": {
"mocha": "^10.1.0",

View file

@ -27,7 +27,7 @@ if (filter) {
// Dump filtered data from raw data
const file = 'freesewing-dump.json'
data = filterData(JSON.parse(fs.readFileSync(path.resolve(dir, file), { encoding: 'utf-8' })))
console.log(JSON.stringify(data, null ,2))
console.log(JSON.stringify(data, null, 2))
} else {
// Load filtered data for migration
const file = 'freesewing-filtered.json'
@ -47,6 +47,8 @@ if (filter) {
console.log()
await migratePatterns(data.patterns)
console.log()
await migrateSubscribers(data.subscribers)
console.log()
}
function progress(text) {
@ -55,6 +57,27 @@ function progress(text) {
process.stdout.write(text)
}
async function migrateSubscribers(subscribers) {
const total = subscribers.length
let i = 0
for (const sub of subscribers) {
i++
progress(` 📰 subscriber ${i}/${total}`)
await createSubscriber(sub)
}
}
async function createSubscriber(sub) {
const record = await prisma.subscriber.create({
data: {
createdAt: sub.createdAt,
data: JSON.stringify({}),
ehash: hash(clean(sub.email)),
email: encrypt(clean(sub.email)),
},
})
}
async function migratePatterns(patterns) {
const total = Object.keys(patterns).length
let i = 0
@ -109,7 +132,7 @@ async function migrateUsers(users) {
}
async function createUser(user) {
const ehash = hash(user.email)
const ehash = hash(clean(user.email))
const record = await prisma.user.create({
data: {
consent: user.consent,
@ -122,7 +145,7 @@ async function createUser(user) {
newsletter: user.newsletter,
password: JSON.stringify({
type: 'v2',
data: user.password
data: user.password,
}),
patron: user.patron,
role: user.role,
@ -133,13 +156,12 @@ async function createUser(user) {
data.userhandles[user.handle] = record.id
}
/*
* Only migrate user data if the user was active in the last 6 months
* Unless they are patrons. Keep patrons regardless coz patrons rock.
*/
function filterData(data) {
let i=0
let i = 0
const filtered = {
users: {},
people: {},
@ -171,9 +193,9 @@ function filterData(data) {
* Migrates role field
*/
function getRole(entry) {
if (entry.handle === 'joost') return "admin"
if (entry.patron > 0) return "patron"
return "user"
if (entry.handle === 'joost') return 'admin'
if (entry.patron > 0) return 'patron'
return 'user'
}
/*
@ -225,6 +247,6 @@ function migratePattern(entry) {
function migrateSubscriber(entry) {
return {
createdAt: entry.created,
email: entry.email
email: entry.email,
}
}

View file

@ -4,7 +4,7 @@ import jwt from 'jsonwebtoken'
//import fs from 'fs'
import Zip from 'jszip'
//import rimraf from 'rimraf'
import { ehash } from '../utils/crypto.mjs'
import { clean, hash } from '../utils/crypto.mjs'
export function UserController() {}
@ -16,15 +16,16 @@ UserController.prototype.signup = async (req, res, tools) => {
if (!req.body.language) return res.status(400).send('languageMissing')
// Requests looks ok - does the user exist?
const hash = ehash(req.body.email)
const emailhash = hash(clean(req.body.email))
// Destructure what we need from tools
const { prisma, config, encrypt } = tools
if ((await prisma.user.findUnique({ where: { ehash: hash } }))) return res.status(400).send('emailExists')
if (await prisma.user.findUnique({ where: { ehash: emailhash } }))
return res.status(400).send('emailExists')
// It does not. Creating user entry
const username = `user-${hash.slice(0,6)}-${time().slice(-6)}` // Temporary username
const username = `user-${hash.slice(0, 6)}-${time().slice(-6)}` // Temporary username
//const user = await.prisma.user.create({
// ehash: hash, // Hash of the email to search on
// ihash: hash, // Hash of the (initial) email to search on
@ -37,7 +38,7 @@ UserController.prototype.signup = async (req, res, tools) => {
password String
*/
return res.status(200).send({})
/*
/*
(err, user) => {
if (err) return res.sendStatus(500)
if (user !== null) return res.status(400).send('userExists')

View file

@ -12,7 +12,7 @@ import { verifyConfig } from './config.mjs'
// Middleware
import { loadExpressMiddleware, loadPassportMiddleware } from './middleware.mjs'
// Encryption
import { ehash, encryption } from './utils/crypto.mjs'
import { encryption } from './utils/crypto.mjs'
// Bootstrap
const config = verifyConfig()

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

103
yarn.lock
View file

@ -6104,6 +6104,13 @@ abbrev@1, abbrev@^1.0.0:
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
@ -6903,6 +6910,11 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
atomic-sleep@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
attr-accept@^1.1.0:
version "1.1.3"
resolved "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
@ -11367,12 +11379,17 @@ event-source-polyfill@1.0.25:
resolved "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.25.tgz#d8bb7f99cb6f8119c2baf086d9f6ee0514b6d9c8"
integrity sha512-hQxu6sN1Eq4JjoI7ITdQeGGUN193A2ra83qC0Ltm9I2UJVAten3OFVN6k5RX4YWeCS0BoC8xg/5czOCIHVosQg==
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter3@^4.0.0, eventemitter3@^4.0.4:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@^3.0.0:
events@^3.0.0, events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
@ -11694,6 +11711,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fast-redact@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
fast-safe-stringify@^2.0.7:
version "2.1.1"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
@ -18839,6 +18861,11 @@ omit.js@^2.0.2:
resolved "https://registry.yarnpkg.com/omit.js/-/omit.js-2.0.2.tgz#dd9b8436fab947a5f3ff214cb2538631e313ec2f"
integrity sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==
on-exit-leak-free@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4"
integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==
on-finished@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
@ -19766,6 +19793,36 @@ pinkie@^2.0.0:
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==
pino-abstract-transport@v1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3"
integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==
dependencies:
readable-stream "^4.0.0"
split2 "^4.0.0"
pino-std-serializers@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz#4c20928a1bafca122fdc2a7a4a171ca1c5f9c526"
integrity sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==
pino@^8.7.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/pino/-/pino-8.7.0.tgz#58621608a3d8540ae643cdd9194cdd94130c78d9"
integrity sha512-l9sA5uPxmZzwydhMWUcm1gI0YxNnYl8MfSr2h8cwLvOAzQLBLewzF247h/vqHe3/tt6fgtXeG9wdjjoetdI/vA==
dependencies:
atomic-sleep "^1.0.0"
fast-redact "^3.1.1"
on-exit-leak-free "^2.1.0"
pino-abstract-transport v1.0.0
pino-std-serializers "^6.0.0"
process-warning "^2.0.0"
quick-format-unescaped "^4.0.3"
real-require "^0.2.0"
safe-stable-stringify "^2.3.1"
sonic-boom "^3.1.0"
thread-stream "^2.0.0"
pirates@^4.0.0, pirates@^4.0.5:
version "4.0.5"
resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
@ -20747,6 +20804,11 @@ process-on-spawn@^1.0.0:
dependencies:
fromentries "^1.2.0"
process-warning@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.0.0.tgz#341dbeaac985b90a04ebcd844d50097c7737b2ee"
integrity sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@ -21034,6 +21096,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quick-format-unescaped@^4.0.3:
version "4.0.4"
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
quick-lru@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
@ -21580,6 +21647,16 @@ readable-stream@1.1.x:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readable-stream@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.2.0.tgz#a7ef523d3b39e4962b0db1a1af22777b10eeca46"
integrity sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A==
dependencies:
abort-controller "^3.0.0"
buffer "^6.0.3"
events "^3.3.0"
process "^0.11.10"
readdir-glob@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.2.tgz#b185789b8e6a43491635b6953295c5c5e3fd224c"
@ -21613,6 +21690,11 @@ readdirp@^3.4.0, readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
real-require@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
recursive-readdir@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
@ -23352,6 +23434,13 @@ socks@^2.7.1:
ip "^2.0.0"
smart-buffer "^4.2.0"
sonic-boom@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.0.tgz#ce9f2de7557e68be2e52c8df6d9b052e7d348143"
integrity sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA==
dependencies:
atomic-sleep "^1.0.0"
sort-keys-length@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188"
@ -23525,6 +23614,11 @@ split2@^3.0.0, split2@^3.2.2:
dependencies:
readable-stream "^3.0.0"
split2@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809"
integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==
split@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
@ -24365,6 +24459,13 @@ thenify-all@^1.0.0:
dependencies:
any-promise "^1.0.0"
thread-stream@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8"
integrity sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ==
dependencies:
real-require "^0.2.0"
throttle-debounce@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"