Merge pull request #3410 from gradido/backend_passwort_encryption_parallel

feat(backend): run password encryption in worker thread
This commit is contained in:
einhornimmond 2025-01-14 13:30:29 +01:00 committed by GitHub
commit 7f2f1dfc68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 300 additions and 75 deletions

View File

@ -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

View File

@ -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",

View File

@ -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 = {

View File

@ -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'],

View File

@ -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

View File

@ -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 {

View File

@ -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'],

View File

@ -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 () => {

View File

@ -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())

View File

@ -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']

View File

@ -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,
}),
)

View File

@ -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<User> {
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

View File

@ -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']

View File

@ -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']

View File

@ -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']

View File

@ -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', () => {

View File

@ -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,
})
}

View File

@ -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<bigint> => {
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<bigint>
if (encryptionWorkerPool) {
result = (await encryptionWorkerPool.exec('SecretKeyCryptographyCreateKey', [
salt,
password,
configLoginAppSecret,
configLoginServerKey,
])) as Promise<bigint>
} 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 => {

View File

@ -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<bigint> => {
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<boolean> => {
const encryptedPassword = await encryptPassword(dbUser, password)
return dbUser.password.toString() === encryptedPassword.toString()
}

View File

@ -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<bigint> => {
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)
}
}

View File

@ -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 } })

View File

@ -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: {

View File

@ -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<T extends Record<string, any>>(obj: T): T {
return obj
}
export const delay = promisify(setTimeout)
export const ensureUrlEndsWithSlash = (url: string): string => {
return url.endsWith('/') ? url : url.concat('/')
}

View File

@ -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"

View File

@ -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