mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3410 from gradido/backend_passwort_encryption_parallel
feat(backend): run password encryption in worker thread
This commit is contained in:
commit
7f2f1dfc68
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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']
|
||||
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
53
backend/src/password/EncryptionWorker.ts
Normal file
53
backend/src/password/EncryptionWorker.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@ -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 => {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
114
backend/src/password/__mocks__/EncryptorUtils.ts
Normal file
114
backend/src/password/__mocks__/EncryptorUtils.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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 } })
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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('/')
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user