diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 2d92e196c..d3bfb1c66 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -36,6 +36,10 @@ import { UserContact } from '@entity/UserContact' import { OptInType } from '../enum/OptInType' import { UserContactType } from '../enum/UserContactType' import { bobBaumeister } from '@/seeds/users/bob-baumeister' +import { encryptPassword } from '@/password/PasswordEncryptr' +import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' +import { find } from 'lodash' +import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -491,7 +495,8 @@ describe('UserResolver', () => { }) it('updates the password', () => { - expect(newUser.password).toEqual('3917921995996627700') + const encryptedPass = encryptPassword(newUser, 'Aa12345_') + expect(newUser.password.toString()).toEqual(encryptedPass.toString()) }) /* @@ -1159,6 +1164,64 @@ describe('UserResolver', () => { }) }) }) + + describe('password encryption type', () => { + describe('user just registered', () => { + let bibi: User + + it('password type should be gradido id', async () => { + const users = await User.find() + bibi = users[1] + + expect(bibi).toEqual( + expect.objectContaining({ + password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] + .readBigUInt64LE() + .toString(), + passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, + }), + ) + }) + }) + + describe('user has encryption type email', () => { + const variables = { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + publisherId: 1234, + } + + let bibi: User + beforeAll(async () => { + const users = await User.find() + bibi = users[1] + + bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL + bibi.password = SecretKeyCryptographyCreateKey( + 'bibi@bloxberg.de', + 'Aa12345_', + )[0].readBigUInt64LE() + + await bibi.save() + }) + + it('changes to gradidoID on login', async () => { + await mutate({ mutation: login, variables: variables }) + + const users = await User.find() + bibi = users[0] + + expect(bibi).toEqual( + expect.objectContaining({ + password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] + .readBigUInt64LE() + .toString(), + passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, + }), + ) + }) + }) + }) }) describe('printTimeDuration', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 2287ede98..78f8a96f8 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -39,17 +39,15 @@ import { SearchAdminUsersResult } from '@model/AdminUser' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { v4 as uuidv4 } from 'uuid' +import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' +import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptr' +import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') // eslint-disable-next-line @typescript-eslint/no-var-requires const random = require('random-bigint') -// We will reuse this for changePassword -const isPassword = (password: string): boolean => { - return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/) -} - const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl'] const DEFAULT_LANGUAGE = 'de' const isLanguage = (language: string): boolean => { @@ -106,48 +104,6 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => { return [pubKey, privKey] } -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 !== sodium.crypto_shorthash_KEYBYTES) { - logger.error( - `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, - ) - throw new Error( - `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, - ) - } - - const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES) - sodium.crypto_hash_sha512_init(state) - sodium.crypto_hash_sha512_update(state, Buffer.from(salt)) - sodium.crypto_hash_sha512_update(state, configLoginAppSecret) - const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES) - sodium.crypto_hash_sha512_final(state, hash) - - const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES) - const opsLimit = 10 - const memLimit = 33554432 - const algo = 2 - sodium.crypto_pwhash( - encryptionKey, - Buffer.from(password), - hash.slice(0, sodium.crypto_pwhash_SALTBYTES), - opsLimit, - memLimit, - algo, - ) - - const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) - sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) - - logger.debug( - `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`, - ) - return [encryptionKeyHash, encryptionKey] -} - /* const getEmailHash = (email: string): Buffer => { logger.trace('getEmailHash...') @@ -343,12 +299,16 @@ 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 Error('User has no private or publicKey') } - const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash - const loginUserPassword = BigInt(dbUser.password.toString()) - if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) { + + if (!verifyPassword(dbUser, password)) { logger.error('The User has no valid credentials.') throw new Error('No user with this credentials') } + + if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) { + dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID + dbUser.password = encryptPassword(dbUser, password) + } // add pubKey in logger-context for layout-pattern X{user} to print it in each logging message logger.addContext('user', dbUser.id) logger.debug('validation of login credentials successful...') @@ -623,7 +583,7 @@ export class UserResolver { ): Promise { logger.info(`setPassword(${code}, ***)...`) // Validate Password - if (!isPassword(password)) { + if (!isValidPassword(password)) { logger.error('Password entered is lexically invalid') throw new Error( 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', @@ -681,10 +641,11 @@ export class UserResolver { userContact.emailChecked = true // Update Password + user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) - user.password = passwordHash[0].readBigUInt64LE() // using the shorthash + user.password = encryptPassword(user, password) user.pubKey = keyPair[0] user.privKey = encryptedPrivkey logger.debug('User credentials updated ...') @@ -789,7 +750,7 @@ export class UserResolver { if (password && passwordNew) { // Validate Password - if (!isPassword(passwordNew)) { + if (!isValidPassword(passwordNew)) { logger.error('newPassword does not fullfil the rules') throw new Error( 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', @@ -801,7 +762,7 @@ export class UserResolver { userEntity.emailContact.email, password, ) - if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) { + if (!verifyPassword(userEntity, password)) { logger.error(`Old password is invalid`) throw new Error(`Old password is invalid`) } @@ -817,7 +778,7 @@ export class UserResolver { logger.debug('PrivateKey encrypted...') // Save new password hash and newly encrypted private key - userEntity.password = newPasswordHash[0].readBigUInt64LE() + userEntity.password = encryptPassword(userEntity, passwordNew) userEntity.privKey = encryptedPrivkey } diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts index 24dc7f352..b8ef2de31 100644 --- a/backend/src/password/PasswordEncryptr.ts +++ b/backend/src/password/PasswordEncryptr.ts @@ -1,30 +1,28 @@ import { User } from '@entity/User' -import { logger } from '@test/testSetup' +// import { logger } from '@test/testSetup' getting error "jest is not defined" import { getBasicCryptographicKey, SecretKeyCryptographyCreateKey } from './EncryptorUtils' -export class PasswordEncryptr { - async encryptPassword(dbUser: User, password: string): Promise { - const basicKey = getBasicCryptographicKey(dbUser) - if (!basicKey) logger.error('Password not set for user ' + dbUser.id) - else { - const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - return passwordHash - } - - throw new Error('Password not set for user ' + dbUser.id) // user has no password - } - - async verifyPassword(dbUser: User, password: string): Promise { - const basicKey = getBasicCryptographicKey(dbUser) - if (!basicKey) logger.error('Password not set for user ' + dbUser.id) - else { - if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { - return false - } - return true - } - +export const encryptPassword = (dbUser: User, password: string): bigint => { + const basicKey = getBasicCryptographicKey(dbUser) + if (!basicKey) { + // logger.error('Password not set for user ' + dbUser.id) throw new Error('Password not set for user ' + dbUser.id) // user has no password + } else { + const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + return passwordHash + } +} + +export const verifyPassword = (dbUser: User, password: string): boolean => { + const basicKey = getBasicCryptographicKey(dbUser) + if (!basicKey) { + // logger.error('Password not set for user ' + dbUser.id) + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } else { + if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { + return false + } + return true } } diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 87d0432dc..9913418fb 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -27,7 +27,7 @@ const communityDbUser: dbUser = { publisherId: 0, passphrase: '', // default password encryption type - passwordEncryptionType: 2, + passwordEncryptionType: 0, hasId: function (): boolean { throw new Error('Function not implemented.') },