diff --git a/backend/package.json b/backend/package.json index db89dfbe7..df138106e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -51,7 +51,8 @@ "type-graphql": "^1.1.1", "typed-rest-client": "^1.8.11", "uuid": "^8.3.2", - "xregexp": "^5.1.1" + "xregexp": "^5.1.1", + "workerpool": "^9.2.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^3.2.1", diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index c0a707577..b8e4a5750 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -587,8 +587,8 @@ describe('UserResolver', () => { expect(newUser.emailContact.emailChecked).toBeTruthy() }) - it('updates the password', () => { - const encryptedPass = encryptPassword(newUser, 'Aa12345_') + it('updates the password', async () => { + const encryptedPass = await encryptPassword(newUser, 'Aa12345_') expect(newUser.password.toString()).toEqual(encryptedPass.toString()) }) @@ -1546,7 +1546,9 @@ describe('UserResolver', () => { expect(bibi).toEqual( expect.objectContaining({ - password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] + password: Buffer.from( + (await SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_'))[0], + ) .readBigUInt64LE() .toString(), passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, @@ -1570,10 +1572,9 @@ describe('UserResolver', () => { }) bibi = usercontact.user bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL - bibi.password = SecretKeyCryptographyCreateKey( - 'bibi@bloxberg.de', - 'Aa12345_', - )[0].readBigUInt64LE() + bibi.password = Buffer.from( + (await SecretKeyCryptographyCreateKey('bibi@bloxberg.de', 'Aa12345_'))[0], + ).readBigUInt64LE() await bibi.save() }) @@ -1590,7 +1591,9 @@ describe('UserResolver', () => { expect(bibi).toEqual( expect.objectContaining({ firstName: 'Bibi', - password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] + password: Buffer.from( + (await SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_'))[0], + ) .readBigUInt64LE() .toString(), passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 0302c7860..4b5fd3118 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -162,7 +162,7 @@ export class UserResolver { // TODO we want to catch this on the frontend and ask the user to check his emails or resend code throw new LogError('The User has not set a password yet', dbUser) } - if (!verifyPassword(dbUser, password)) { + if (!(await verifyPassword(dbUser, password))) { throw new LogError('No user with this credentials', dbUser) } @@ -178,7 +178,7 @@ export class UserResolver { if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) { dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID - dbUser.password = encryptPassword(dbUser, password) + dbUser.password = await encryptPassword(dbUser, password) await dbUser.save() } // add pubKey in logger-context for layout-pattern X{user} to print it in each logging message @@ -502,7 +502,7 @@ export class UserResolver { // Update Password user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID - user.password = encryptPassword(user, password) + user.password = await encryptPassword(user, password) logger.debug('User credentials updated ...') const queryRunner = getConnection().createQueryRunner() @@ -632,13 +632,13 @@ export class UserResolver { ) } - if (!verifyPassword(user, password)) { + if (!(await verifyPassword(user, password))) { throw new LogError(`Old password is invalid`) } // Save new password hash and newly encrypted private key user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID - user.password = encryptPassword(user, passwordNew) + user.password = await encryptPassword(user, passwordNew) } // Save hideAmountGDD value diff --git a/backend/src/password/EncryptionWorker.ts b/backend/src/password/EncryptionWorker.ts new file mode 100644 index 000000000..2299a1a9b --- /dev/null +++ b/backend/src/password/EncryptionWorker.ts @@ -0,0 +1,50 @@ +import { worker } from 'workerpool' + +import { + crypto_box_SEEDBYTES, + crypto_hash_sha512_init, + crypto_hash_sha512_update, + crypto_hash_sha512_final, + crypto_hash_sha512_BYTES, + crypto_hash_sha512_STATEBYTES, + crypto_shorthash_BYTES, + crypto_pwhash_SALTBYTES, + crypto_pwhash, + crypto_shorthash, +} from 'sodium-native' + +export const SecretKeyCryptographyCreateKey = ( + salt: string, + password: string, + configLoginAppSecret: Buffer, + configLoginServerKey: Buffer, +): Buffer[] => { + const state = Buffer.alloc(crypto_hash_sha512_STATEBYTES) + crypto_hash_sha512_init(state) + crypto_hash_sha512_update(state, Buffer.from(salt)) + crypto_hash_sha512_update(state, configLoginAppSecret) + const hash = Buffer.alloc(crypto_hash_sha512_BYTES) + crypto_hash_sha512_final(state, hash) + + const encryptionKey = Buffer.alloc(crypto_box_SEEDBYTES) + const opsLimit = 10 + const memLimit = 33554432 + const algo = 2 + crypto_pwhash( + encryptionKey, + Buffer.from(password), + hash.slice(0, crypto_pwhash_SALTBYTES), + opsLimit, + memLimit, + algo, + ) + + const encryptionKeyHash = Buffer.alloc(crypto_shorthash_BYTES) + crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) + + return [encryptionKeyHash, encryptionKey] +} + +worker({ + SecretKeyCryptographyCreateKey, +}) diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 64dcb2289..74b9f91d0 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -2,7 +2,11 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { cpus } from 'os' +import path from 'path' + import { User } from '@entity/User' +import { pool } from 'workerpool' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' @@ -10,61 +14,54 @@ import { CONFIG } from '@/config' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' -import { - crypto_shorthash_KEYBYTES, - crypto_box_SEEDBYTES, - crypto_hash_sha512_init, - crypto_hash_sha512_update, - crypto_hash_sha512_final, - crypto_hash_sha512_BYTES, - crypto_hash_sha512_STATEBYTES, - crypto_shorthash_BYTES, - crypto_pwhash_SALTBYTES, - crypto_pwhash, - crypto_shorthash, -} from 'sodium-native' +import { crypto_shorthash_KEYBYTES } from 'sodium-native' + +const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') +const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') + +// TODO: put maxQueueSize into config +const encryptionWorkerPool = pool( + path.join(__dirname, '..', '..', 'build', 'src', 'password', '/EncryptionWorker.js'), + { + maxQueueSize: 30 * cpus().length, + }, +) // We will reuse this for changePassword export const isValidPassword = (password: string): boolean => { return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/) } -export const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => { - logger.trace('SecretKeyCryptographyCreateKey...') - const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') - const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') - if (configLoginServerKey.length !== crypto_shorthash_KEYBYTES) { - throw new LogError( - 'ServerKey has an invalid size', - configLoginServerKey.length, - crypto_shorthash_KEYBYTES, - ) +/** + * @param salt + * @param password + * @returns can throw an exception if worker pool is full, if more than 30 * cpu core count logins happen in a time range of 30 seconds + */ +export const SecretKeyCryptographyCreateKey = async ( + salt: string, + password: string, +): Promise => { + try { + logger.trace('call worker for: SecretKeyCryptographyCreateKey') + if (configLoginServerKey.length !== crypto_shorthash_KEYBYTES) { + throw new LogError( + 'ServerKey has an invalid size', + configLoginServerKey.length, + crypto_shorthash_KEYBYTES, + ) + } + return (await encryptionWorkerPool.exec('SecretKeyCryptographyCreateKey', [ + salt, + password, + configLoginAppSecret, + configLoginServerKey, + ])) as Promise + } catch (e) { + // pool is throwing this error + // throw new Error('Max queue size of ' + this.maxQueueSize + ' reached'); + // will be shown in frontend to user + throw new LogError('Server is full, please try again in 10 minutes.', e) } - - const state = Buffer.alloc(crypto_hash_sha512_STATEBYTES) - crypto_hash_sha512_init(state) - crypto_hash_sha512_update(state, Buffer.from(salt)) - crypto_hash_sha512_update(state, configLoginAppSecret) - const hash = Buffer.alloc(crypto_hash_sha512_BYTES) - crypto_hash_sha512_final(state, hash) - - const encryptionKey = Buffer.alloc(crypto_box_SEEDBYTES) - const opsLimit = 10 - const memLimit = 33554432 - const algo = 2 - crypto_pwhash( - encryptionKey, - Buffer.from(password), - hash.slice(0, crypto_pwhash_SALTBYTES), - opsLimit, - memLimit, - algo, - ) - - const encryptionKeyHash = Buffer.alloc(crypto_shorthash_BYTES) - crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) - - return [encryptionKeyHash, encryptionKey] } export const getUserCryptographicSalt = (dbUser: User): string => { diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts index 1c7457a40..1aa740672 100644 --- a/backend/src/password/PasswordEncryptor.ts +++ b/backend/src/password/PasswordEncryptor.ts @@ -3,13 +3,14 @@ import { User } from '@entity/User' // import { logger } from '@test/testSetup' getting error "jest is not defined" import { getUserCryptographicSalt, SecretKeyCryptographyCreateKey } from './EncryptorUtils' -export const encryptPassword = (dbUser: User, password: string): bigint => { +export const encryptPassword = async (dbUser: User, password: string): Promise => { const salt = getUserCryptographicSalt(dbUser) - const keyBuffer = SecretKeyCryptographyCreateKey(salt, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() + const keyBuffer = await SecretKeyCryptographyCreateKey(salt, password) // return short and long hash + const passwordHash = Buffer.from(keyBuffer[0]).readBigUInt64LE() return passwordHash } -export const verifyPassword = (dbUser: User, password: string): boolean => { - return dbUser.password.toString() === encryptPassword(dbUser, password).toString() +export const verifyPassword = async (dbUser: User, password: string): Promise => { + const encryptedPassword = await encryptPassword(dbUser, password) + return dbUser.password.toString() === encryptedPassword.toString() } diff --git a/backend/src/seeds/factory/creation.ts b/backend/src/seeds/factory/creation.ts index 5ad1b2e4e..5e6e058f9 100644 --- a/backend/src/seeds/factory/creation.ts +++ b/backend/src/seeds/factory/creation.ts @@ -23,7 +23,6 @@ export const creationFactory = async ( mutation: login, variables: { email: creation.email, password: 'Aa12345_' }, }) - const { data: { createContribution: contribution }, } = await mutate({ mutation: createContribution, variables: { ...creation } }) diff --git a/backend/yarn.lock b/backend/yarn.lock index effdd77f2..571e2eb59 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3709,7 +3709,7 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== "gradido-database@file:../database": - version "2.3.1" + version "2.4.1" dependencies: "@types/uuid" "^8.3.4" cross-env "^7.0.3" @@ -7369,6 +7369,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +workerpool@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.2.0.tgz#f74427cbb61234708332ed8ab9cbf56dcb1c4371" + integrity sha512-PKZqBOCo6CYkVOwAxWxQaSF2Fvb5Iv2fCeTP7buyWI2GiynWr46NcXSgK/idoV6e60dgCBfgYc+Un3HMvmqP8w== + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"