use worker

This commit is contained in:
einhornimmond 2024-12-18 14:45:41 +01:00
parent 02a2b46cf5
commit c7e24b4b82
8 changed files with 125 additions and 69 deletions

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

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

View File

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

View File

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

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

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

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