diff --git a/backend/.env.template b/backend/.env.template index 71fbcbf31..7d588ff49 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -40,6 +40,7 @@ COMMUNITY_SUPPORT_MAIL=$COMMUNITY_SUPPORT_MAIL # Login Server LOGIN_APP_SECRET=21ffbbc616fe LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a +USE_CRYPTO_WORKER=$USE_CRYPTO_WORKER # EMail EMAIL=$EMAIL 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/config/index.ts b/backend/src/config/index.ts index 4553d13c8..274cb34dd 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -76,6 +76,7 @@ const community = { const loginServer = { LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET ?? '21ffbbc616fe', LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY ?? 'a51ef8ac7ef1abf162fb7a65261acd7a', + USE_CRYPTO_WORKER: process.env.USE_CRYPTO_WORKER ?? false, } const email = { diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 8b5c05d5b..f9e6aa9ac 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -28,6 +28,8 @@ import { peterLustig } from '@/seeds/users/peter-lustig' import { getCommunityByUuid } from './util/communities' +jest.mock('@/password/EncryptorUtils') + // to do: We need a setup for the tests that closes the connection let mutate: ApolloServerTestClient['mutate'], query: ApolloServerTestClient['query'], diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts index 23be529d3..3a3196c85 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts @@ -22,6 +22,8 @@ import { listContributionLinks } from '@/seeds/graphql/queries' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' +jest.mock('@/password/EncryptorUtils') + let mutate: ApolloServerTestClient['mutate'], query: ApolloServerTestClient['query'], con: Connection diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts index 88d79d5c6..a82f623cc 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts @@ -27,6 +27,7 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { peterLustig } from '@/seeds/users/peter-lustig' +jest.mock('@/password/EncryptorUtils') jest.mock('@/emails/sendEmailVariants', () => { const originalModule = jest.requireActual('@/emails/sendEmailVariants') return { diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 9317b76e7..5c7a3e77d 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -59,6 +59,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking' import { getFirstDayOfPreviousNMonth } from '@/util/utilities' jest.mock('@/emails/sendEmailVariants') +jest.mock('@/password/EncryptorUtils') let mutate: ApolloServerTestClient['mutate'], query: ApolloServerTestClient['query'], diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts index 2c819f070..06430c620 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.test.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -15,6 +15,8 @@ import { userFactory } from '@/seeds/factory/user' import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +jest.mock('@/password/EncryptorUtils') + let testEnv: any, mutate: any, con: any beforeAll(async () => { diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index fe047b35a..bcea43416 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -38,6 +38,8 @@ import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { transactionLinkCode } from './TransactionLinkResolver' +jest.mock('@/password/EncryptorUtils') + // mock semaphore to allow use fake timers jest.mock('@/util/TRANSACTIONS_LOCK') TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn()) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 4bf5ab493..7cc6143e6 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -37,6 +37,8 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { peterLustig } from '@/seeds/users/peter-lustig' import { stephenHawking } from '@/seeds/users/stephen-hawking' +jest.mock('@/password/EncryptorUtils') + let mutate: ApolloServerTestClient['mutate'], con: Connection let query: ApolloServerTestClient['query'] diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 0a22172c6..58b6b33c4 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -74,6 +74,7 @@ import { objectValuesToArray } from '@/util/utilities' import { Location2Point } from './util/Location2Point' jest.mock('@/apis/humhub/HumHubClient') +jest.mock('@/password/EncryptorUtils') jest.mock('@/emails/sendEmailVariants', () => { const originalModule = jest.requireActual('@/emails/sendEmailVariants') @@ -96,6 +97,8 @@ jest.mock('@/apis/KlicktippController', () => { } }) +CONFIG.EMAIL_CODE_REQUEST_TIME = 10 + let admin: User let user: User let mutate: ApolloServerTestClient['mutate'], @@ -588,8 +591,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()) }) @@ -1547,9 +1550,9 @@ describe('UserResolver', () => { expect(bibi).toEqual( expect.objectContaining({ - password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] - .readBigUInt64LE() - .toString(), + password: ( + await SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_') + ).toString(), passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, }), ) @@ -1571,10 +1574,7 @@ describe('UserResolver', () => { }) bibi = usercontact.user bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL - bibi.password = SecretKeyCryptographyCreateKey( - 'bibi@bloxberg.de', - 'Aa12345_', - )[0].readBigUInt64LE() + bibi.password = await SecretKeyCryptographyCreateKey('bibi@bloxberg.de', 'Aa12345_') await bibi.save() }) @@ -1591,9 +1591,9 @@ describe('UserResolver', () => { expect(bibi).toEqual( expect.objectContaining({ firstName: 'Bibi', - password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] - .readBigUInt64LE() - .toString(), + password: ( + await SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_') + ).toString(), passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, }), ) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 6193e75db..f766dea46 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -68,6 +68,7 @@ import { backendLogger as logger } from '@/server/logger' import { communityDbUser } from '@/util/communityUser' import { hasElopageBuys } from '@/util/hasElopageBuys' import { getTimeDurationObject, printTimeDuration } from '@/util/time' +import { delay } from '@/util/utilities' import random from 'random-bigint' import { randombytes_random } from 'sodium-native' @@ -149,7 +150,16 @@ export class UserResolver { ): Promise { logger.info(`login with ${email}, ***, ${publisherId} ...`) email = email.trim().toLowerCase() - const dbUser = await findUserByEmail(email) + let dbUser: DbUser + + try { + dbUser = await findUserByEmail(email) + } catch (e) { + // simulate delay which occur on password encryption 650 ms +- 50 rnd + await delay(650 + Math.floor(Math.random() * 101) - 50) + throw e + } + if (dbUser.deletedAt) { throw new LogError('This user was permanently deleted. Contact support for questions', dbUser) } @@ -161,7 +171,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) } @@ -177,7 +187,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 @@ -501,7 +511,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() @@ -631,13 +641,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/graphql/resolver/semaphore.test.ts b/backend/src/graphql/resolver/semaphore.test.ts index ed58d55ec..f3dcce6f6 100644 --- a/backend/src/graphql/resolver/semaphore.test.ts +++ b/backend/src/graphql/resolver/semaphore.test.ts @@ -25,6 +25,8 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { peterLustig } from '@/seeds/users/peter-lustig' +jest.mock('@/password/EncryptorUtils') + let mutate: ApolloServerTestClient['mutate'], con: Connection let testEnv: { mutate: ApolloServerTestClient['mutate'] diff --git a/backend/src/graphql/resolver/util/creations.test.ts b/backend/src/graphql/resolver/util/creations.test.ts index 83d787dd9..8b59d0b14 100644 --- a/backend/src/graphql/resolver/util/creations.test.ts +++ b/backend/src/graphql/resolver/util/creations.test.ts @@ -12,6 +12,8 @@ import { peterLustig } from '@/seeds/users/peter-lustig' import { getOpenCreations, getUserCreation } from './creations' +jest.mock('@/password/EncryptorUtils') + let mutate: ApolloServerTestClient['mutate'], con: Connection let testEnv: { mutate: ApolloServerTestClient['mutate'] diff --git a/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts b/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts index cfdbce8f2..1cdca8429 100644 --- a/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts +++ b/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts @@ -16,6 +16,8 @@ import { peterLustig } from '@/seeds/users/peter-lustig' import { findUserByIdentifier } from './findUserByIdentifier' +jest.mock('@/password/EncryptorUtils') + let con: Connection let testEnv: { mutate: ApolloServerTestClient['mutate'] diff --git a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts index 0d85a35af..064184a9d 100644 --- a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts +++ b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts @@ -32,6 +32,8 @@ import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector' +jest.mock('@/password/EncryptorUtils') + /* // Mock the GraphQLClient jest.mock('graphql-request', () => { diff --git a/backend/src/password/EncryptionWorker.ts b/backend/src/password/EncryptionWorker.ts new file mode 100644 index 000000000..f14d5d1ff --- /dev/null +++ b/backend/src/password/EncryptionWorker.ts @@ -0,0 +1,53 @@ +import { worker } from 'workerpool' + +import { CONFIG } from '@/config' + +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, +): bigint => { + 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.readBigUInt64LE() +} + +if (CONFIG.USE_CRYPTO_WORKER) { + worker({ + SecretKeyCryptographyCreateKey, + }) +} diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 64dcb2289..27432d1a3 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, pool } from 'workerpool' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' @@ -10,61 +14,73 @@ 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' + +import { SecretKeyCryptographyCreateKey as SecretKeyCryptographyCreateKeySync } from './EncryptionWorker' + +const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') +const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') + +let encryptionWorkerPool: Pool | undefined + +if (CONFIG.USE_CRYPTO_WORKER) { + encryptionWorkerPool = pool( + path.join(__dirname, '..', '..', 'build', 'src', 'password', '/EncryptionWorker.js'), + { + // TODO: put maxQueueSize into config + 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, + ) + } + let result: Promise + if (encryptionWorkerPool) { + result = (await encryptionWorkerPool.exec('SecretKeyCryptographyCreateKey', [ + salt, + password, + configLoginAppSecret, + configLoginServerKey, + ])) as Promise + } else { + result = Promise.resolve( + SecretKeyCryptographyCreateKeySync( + salt, + password, + configLoginAppSecret, + configLoginServerKey, + ), + ) + } + return result + } 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..a97c29e7f 100644 --- a/backend/src/password/PasswordEncryptor.ts +++ b/backend/src/password/PasswordEncryptor.ts @@ -3,13 +3,12 @@ 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() - return passwordHash + return SecretKeyCryptographyCreateKey(salt, password) } -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/password/__mocks__/EncryptorUtils.ts b/backend/src/password/__mocks__/EncryptorUtils.ts new file mode 100644 index 000000000..2bc20f951 --- /dev/null +++ b/backend/src/password/__mocks__/EncryptorUtils.ts @@ -0,0 +1,114 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { User } from '@entity/User' + +import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' + +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, + crypto_pwhash_OPSLIMIT_MIN, + crypto_pwhash_MEMLIMIT_MIN, +} from 'sodium-native' + +const SecretKeyCryptographyCreateKeyMock = ( + salt: string, + password: string, + configLoginAppSecret: Buffer, + configLoginServerKey: Buffer, +): bigint => { + 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 = crypto_pwhash_OPSLIMIT_MIN + const memLimit = crypto_pwhash_MEMLIMIT_MIN + 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.readBigUInt64LE() +} + +const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') +const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') + +// 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,}$/) +} + +/** + * @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 Promise.resolve( + SecretKeyCryptographyCreateKeyMock( + salt, + password, + configLoginAppSecret, + configLoginServerKey, + ), + ) + } 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) + } +} + +export const getUserCryptographicSalt = (dbUser: User): string => { + switch (dbUser.passwordEncryptionType) { + case PasswordEncryptionType.NO_PASSWORD: + throw new LogError('User has no password set', dbUser.id) + case PasswordEncryptionType.EMAIL: + return dbUser.emailContact.email + case PasswordEncryptionType.GRADIDO_ID: + return dbUser.gradidoID + default: + throw new LogError('Unknown password encryption type', dbUser.passwordEncryptionType) + } +} 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/src/util/klicktipp.test.ts b/backend/src/util/klicktipp.test.ts index 6639a0aa4..a88e8f21a 100644 --- a/backend/src/util/klicktipp.test.ts +++ b/backend/src/util/klicktipp.test.ts @@ -19,6 +19,7 @@ import { peterLustig } from '@/seeds/users/peter-lustig' import { exportEventDataToKlickTipp } from './klicktipp' jest.mock('@/apis/KlicktippController') +jest.mock('@/password/EncryptorUtils') let mutate: ApolloServerTestClient['mutate'], con: Connection let testEnv: { diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 905cce686..4f45af023 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,3 +1,5 @@ +import { promisify } from 'util' + import { Decimal } from 'decimal.js-light' import i18n from 'i18n' @@ -30,6 +32,8 @@ export function resetInterface>(obj: T): T { return obj } +export const delay = promisify(setTimeout) + export const ensureUrlEndsWithSlash = (url: string): string => { return url.endsWith('/') ? url : url.concat('/') } 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" diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 3b7a19b6b..a07d92dde 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -41,6 +41,7 @@ DEPLOY_SEED_DATA=false # if true all email will be send to EMAIL_TEST_RECEIVER instead of email address of user EMAIL_TEST_MODUS=false EMAIL_TEST_RECEIVER=test_team@gradido.net +USE_CRYPTO_WORKER=true # Logging LOG_LEVEL=INFO