mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into jest_is_dev_depenency
This commit is contained in:
commit
bd28a888a6
@ -10,7 +10,7 @@ Decimal.set({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0052-add_updated_at_to_contributions',
|
DB_VERSION: '0053-change_password_encryption',
|
||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||||
LOG4JS_CONFIG: 'log4js-config.json',
|
LOG4JS_CONFIG: 'log4js-config.json',
|
||||||
// default log level on production should be info
|
// default log level on production should be info
|
||||||
|
|||||||
12
backend/src/graphql/enum/PasswordEncryptionType.ts
Normal file
12
backend/src/graphql/enum/PasswordEncryptionType.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { registerEnumType } from 'type-graphql'
|
||||||
|
|
||||||
|
export enum PasswordEncryptionType {
|
||||||
|
NO_PASSWORD = 0,
|
||||||
|
EMAIL = 1,
|
||||||
|
GRADIDO_ID = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEnumType(PasswordEncryptionType, {
|
||||||
|
name: 'PasswordEncryptionType', // this one is mandatory
|
||||||
|
description: 'Type of the password encryption', // this one is optional
|
||||||
|
})
|
||||||
@ -36,6 +36,9 @@ import { UserContact } from '@entity/UserContact'
|
|||||||
import { OptInType } from '../enum/OptInType'
|
import { OptInType } from '../enum/OptInType'
|
||||||
import { UserContactType } from '../enum/UserContactType'
|
import { UserContactType } from '../enum/UserContactType'
|
||||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||||
|
import { encryptPassword } from '@/password/PasswordEncryptor'
|
||||||
|
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
|
||||||
|
import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
|
||||||
|
|
||||||
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
|
|
||||||
@ -146,6 +149,7 @@ describe('UserResolver', () => {
|
|||||||
publisherId: 1234,
|
publisherId: 1234,
|
||||||
referrerId: null,
|
referrerId: null,
|
||||||
contributionLinkId: null,
|
contributionLinkId: null,
|
||||||
|
passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
const valUUID = validateUUID(user[0].gradidoID)
|
const valUUID = validateUUID(user[0].gradidoID)
|
||||||
@ -491,7 +495,8 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('updates the password', () => {
|
it('updates the password', () => {
|
||||||
expect(newUser.password).toEqual('3917921995996627700')
|
const encryptedPass = encryptPassword(newUser, 'Aa12345_')
|
||||||
|
expect(newUser.password.toString()).toEqual(encryptedPass.toString())
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -1159,6 +1164,93 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('password encryption type', () => {
|
||||||
|
describe('user just registered', () => {
|
||||||
|
let bibi: User
|
||||||
|
|
||||||
|
it('has password type 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 usercontact = await UserContact.findOneOrFail(
|
||||||
|
{ email: 'bibi@bloxberg.de' },
|
||||||
|
{ relations: ['user'] },
|
||||||
|
)
|
||||||
|
bibi = usercontact.user
|
||||||
|
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 usercontact = await UserContact.findOneOrFail(
|
||||||
|
{ email: 'bibi@bloxberg.de' },
|
||||||
|
{ relations: ['user'] },
|
||||||
|
)
|
||||||
|
bibi = usercontact.user
|
||||||
|
|
||||||
|
expect(bibi).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
firstName: 'Bibi',
|
||||||
|
password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0]
|
||||||
|
.readBigUInt64LE()
|
||||||
|
.toString(),
|
||||||
|
passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can login after password change', async () => {
|
||||||
|
resetToken()
|
||||||
|
expect(await mutate({ mutation: login, variables: variables })).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
login: {
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
firstName: 'Bibi',
|
||||||
|
hasElopage: false,
|
||||||
|
id: expect.any(Number),
|
||||||
|
isAdmin: null,
|
||||||
|
klickTipp: {
|
||||||
|
newsletterState: false,
|
||||||
|
},
|
||||||
|
language: 'de',
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
publisherId: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('printTimeDuration', () => {
|
describe('printTimeDuration', () => {
|
||||||
|
|||||||
@ -40,17 +40,15 @@ import { SearchAdminUsersResult } from '@model/AdminUser'
|
|||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
|
||||||
|
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
|
||||||
|
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const sodium = require('sodium-native')
|
const sodium = require('sodium-native')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const random = require('random-bigint')
|
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 LANGUAGES = ['de', 'en', 'es', 'fr', 'nl']
|
||||||
const DEFAULT_LANGUAGE = 'de'
|
const DEFAULT_LANGUAGE = 'de'
|
||||||
const isLanguage = (language: string): boolean => {
|
const isLanguage = (language: string): boolean => {
|
||||||
@ -107,48 +105,6 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
|
|||||||
return [pubKey, privKey]
|
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 => {
|
const getEmailHash = (email: string): Buffer => {
|
||||||
logger.trace('getEmailHash...')
|
logger.trace('getEmailHash...')
|
||||||
@ -346,12 +302,17 @@ export class UserResolver {
|
|||||||
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
|
// 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')
|
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 (!verifyPassword(dbUser, password)) {
|
||||||
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
|
|
||||||
logger.error('The User has no valid credentials.')
|
logger.error('The User has no valid credentials.')
|
||||||
throw new Error('No user with this 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)
|
||||||
|
await dbUser.save()
|
||||||
|
}
|
||||||
// add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
|
// add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
|
||||||
logger.addContext('user', dbUser.id)
|
logger.addContext('user', dbUser.id)
|
||||||
logger.debug('validation of login credentials successful...')
|
logger.debug('validation of login credentials successful...')
|
||||||
@ -481,6 +442,7 @@ export class UserResolver {
|
|||||||
dbUser.lastName = lastName
|
dbUser.lastName = lastName
|
||||||
dbUser.language = language
|
dbUser.language = language
|
||||||
dbUser.publisherId = publisherId
|
dbUser.publisherId = publisherId
|
||||||
|
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
|
||||||
dbUser.passphrase = passphrase.join(' ')
|
dbUser.passphrase = passphrase.join(' ')
|
||||||
logger.debug('new dbUser=' + dbUser)
|
logger.debug('new dbUser=' + dbUser)
|
||||||
if (redeemCode) {
|
if (redeemCode) {
|
||||||
@ -634,7 +596,7 @@ export class UserResolver {
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
logger.info(`setPassword(${code}, ***)...`)
|
logger.info(`setPassword(${code}, ***)...`)
|
||||||
// Validate Password
|
// Validate Password
|
||||||
if (!isPassword(password)) {
|
if (!isValidPassword(password)) {
|
||||||
logger.error('Password entered is lexically invalid')
|
logger.error('Password entered is lexically invalid')
|
||||||
throw new Error(
|
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!',
|
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||||
@ -692,10 +654,11 @@ export class UserResolver {
|
|||||||
userContact.emailChecked = true
|
userContact.emailChecked = true
|
||||||
|
|
||||||
// Update Password
|
// Update Password
|
||||||
|
user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||||
const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash
|
const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash
|
||||||
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
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.pubKey = keyPair[0]
|
||||||
user.privKey = encryptedPrivkey
|
user.privKey = encryptedPrivkey
|
||||||
logger.debug('User credentials updated ...')
|
logger.debug('User credentials updated ...')
|
||||||
@ -801,7 +764,7 @@ export class UserResolver {
|
|||||||
|
|
||||||
if (password && passwordNew) {
|
if (password && passwordNew) {
|
||||||
// Validate Password
|
// Validate Password
|
||||||
if (!isPassword(passwordNew)) {
|
if (!isValidPassword(passwordNew)) {
|
||||||
logger.error('newPassword does not fullfil the rules')
|
logger.error('newPassword does not fullfil the rules')
|
||||||
throw new Error(
|
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!',
|
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||||
@ -813,7 +776,7 @@ export class UserResolver {
|
|||||||
userEntity.emailContact.email,
|
userEntity.emailContact.email,
|
||||||
password,
|
password,
|
||||||
)
|
)
|
||||||
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
if (!verifyPassword(userEntity, password)) {
|
||||||
logger.error(`Old password is invalid`)
|
logger.error(`Old password is invalid`)
|
||||||
throw new Error(`Old password is invalid`)
|
throw new Error(`Old password is invalid`)
|
||||||
}
|
}
|
||||||
@ -829,7 +792,8 @@ export class UserResolver {
|
|||||||
logger.debug('PrivateKey encrypted...')
|
logger.debug('PrivateKey encrypted...')
|
||||||
|
|
||||||
// Save new password hash and newly encrypted private key
|
// Save new password hash and newly encrypted private key
|
||||||
userEntity.password = newPasswordHash[0].readBigUInt64LE()
|
userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||||
|
userEntity.password = encryptPassword(userEntity, passwordNew)
|
||||||
userEntity.privKey = encryptedPrivkey
|
userEntity.privKey = encryptedPrivkey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
71
backend/src/password/EncryptorUtils.ts
Normal file
71
backend/src/password/EncryptorUtils.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import CONFIG from '@/config'
|
||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import { User } from '@entity/User'
|
||||||
|
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const sodium = require('sodium-native')
|
||||||
|
|
||||||
|
// 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 !== 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)
|
||||||
|
|
||||||
|
return [encryptionKeyHash, encryptionKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserCryptographicSalt = (dbUser: User): string => {
|
||||||
|
switch (dbUser.passwordEncryptionType) {
|
||||||
|
case PasswordEncryptionType.NO_PASSWORD: {
|
||||||
|
logger.error('Password not set for user ' + dbUser.id)
|
||||||
|
throw new Error('Password not set for user ' + dbUser.id) // user has no password
|
||||||
|
}
|
||||||
|
case PasswordEncryptionType.EMAIL: {
|
||||||
|
return dbUser.emailContact.email
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case PasswordEncryptionType.GRADIDO_ID: {
|
||||||
|
return dbUser.gradidoID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logger.error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
|
||||||
|
throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
backend/src/password/PasswordEncryptor.ts
Normal file
14
backend/src/password/PasswordEncryptor.ts
Normal file
@ -0,0 +1,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 => {
|
||||||
|
const salt = getUserCryptographicSalt(dbUser)
|
||||||
|
const keyBuffer = SecretKeyCryptographyCreateKey(salt, password) // return short and long hash
|
||||||
|
const passwordHash = keyBuffer[0].readBigUInt64LE()
|
||||||
|
return passwordHash
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyPassword = (dbUser: User, password: string): boolean => {
|
||||||
|
return dbUser.password.toString() === encryptPassword(dbUser, password).toString()
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
|
import { PasswordEncryptionType } from '@/graphql/enum/PasswordEncryptionType'
|
||||||
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
import { UserContact } from '@entity/UserContact'
|
import { UserContact } from '@entity/UserContact'
|
||||||
@ -26,6 +27,8 @@ const communityDbUser: dbUser = {
|
|||||||
isAdmin: null,
|
isAdmin: null,
|
||||||
publisherId: 0,
|
publisherId: 0,
|
||||||
passphrase: '',
|
passphrase: '',
|
||||||
|
// default password encryption type
|
||||||
|
passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,
|
||||||
hasId: function (): boolean {
|
hasId: function (): boolean {
|
||||||
throw new Error('Function not implemented.')
|
throw new Error('Function not implemented.')
|
||||||
},
|
},
|
||||||
|
|||||||
127
database/entity/0053-change_password_encryption/User.ts
Normal file
127
database/entity/0053-change_password_encryption/User.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
DeleteDateColumn,
|
||||||
|
OneToMany,
|
||||||
|
JoinColumn,
|
||||||
|
OneToOne,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { Contribution } from '../Contribution'
|
||||||
|
import { ContributionMessage } from '../ContributionMessage'
|
||||||
|
import { UserContact } from '../UserContact'
|
||||||
|
|
||||||
|
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||||
|
export class User extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'gradido_id',
|
||||||
|
length: 36,
|
||||||
|
nullable: false,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
gradidoID: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'alias',
|
||||||
|
length: 20,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
alias: string
|
||||||
|
|
||||||
|
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
pubKey: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
|
||||||
|
privKey: Buffer
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'text',
|
||||||
|
name: 'passphrase',
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
passphrase: string
|
||||||
|
|
||||||
|
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
|
||||||
|
@JoinColumn({ name: 'email_id' })
|
||||||
|
emailContact: UserContact
|
||||||
|
|
||||||
|
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||||
|
emailId: number | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'first_name',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
firstName: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'last_name',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
lastName: string
|
||||||
|
|
||||||
|
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
|
||||||
|
deletedAt: Date | null
|
||||||
|
|
||||||
|
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||||
|
password: BigInt
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'password_encryption_type',
|
||||||
|
type: 'int',
|
||||||
|
unsigned: true,
|
||||||
|
nullable: false,
|
||||||
|
default: 0,
|
||||||
|
})
|
||||||
|
passwordEncryptionType: number
|
||||||
|
|
||||||
|
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||||
|
language: string
|
||||||
|
|
||||||
|
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
|
||||||
|
isAdmin: Date | null
|
||||||
|
|
||||||
|
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||||
|
referrerId?: number | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'contribution_link_id',
|
||||||
|
type: 'int',
|
||||||
|
unsigned: true,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
contributionLinkId?: number | null
|
||||||
|
|
||||||
|
@Column({ name: 'publisher_id', default: 0 })
|
||||||
|
publisherId: number
|
||||||
|
|
||||||
|
@OneToMany(() => Contribution, (contribution) => contribution.user)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
contributions?: Contribution[]
|
||||||
|
|
||||||
|
@OneToMany(() => ContributionMessage, (message) => message.user)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
messages?: ContributionMessage[]
|
||||||
|
|
||||||
|
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
userContacts?: UserContact[]
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
DeleteDateColumn,
|
||||||
|
OneToOne,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { User } from './User'
|
||||||
|
|
||||||
|
@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||||
|
export class UserContact extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'type',
|
||||||
|
length: 100,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
type: string
|
||||||
|
|
||||||
|
@OneToOne(() => User, (user) => user.emailContact)
|
||||||
|
user: User
|
||||||
|
|
||||||
|
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false })
|
||||||
|
userId: number
|
||||||
|
|
||||||
|
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@Column({ name: 'email_verification_code', type: 'bigint', unsigned: true, unique: true })
|
||||||
|
emailVerificationCode: BigInt
|
||||||
|
|
||||||
|
@Column({ name: 'email_opt_in_type_id' })
|
||||||
|
emailOptInTypeId: number
|
||||||
|
|
||||||
|
@Column({ name: 'email_resend_count' })
|
||||||
|
emailResendCount: number
|
||||||
|
|
||||||
|
// @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
// emailHash: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
|
||||||
|
emailChecked: boolean
|
||||||
|
|
||||||
|
@Column({ length: 255, unique: false, nullable: true, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
phone: string
|
||||||
|
|
||||||
|
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Column({ name: 'updated_at', nullable: true, default: null, type: 'datetime' })
|
||||||
|
updatedAt: Date | null
|
||||||
|
|
||||||
|
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
|
||||||
|
deletedAt: Date | null
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export { User } from './0049-add_user_contacts_table/User'
|
export { User } from './0053-change_password_encryption/User'
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { UserContact } from './0049-add_user_contacts_table/UserContact'
|
export { UserContact } from './0053-change_password_encryption/UserContact'
|
||||||
|
|||||||
24
database/migrations/0053-change_password_encryption.ts
Normal file
24
database/migrations/0053-change_password_encryption.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/* MIGRATION TO ADD ENCRYPTION TYPE TO PASSWORDS
|
||||||
|
*
|
||||||
|
* This migration adds and renames columns in the table `users`
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn('ALTER TABLE users RENAME COLUMN created TO created_at;')
|
||||||
|
await queryFn('ALTER TABLE users RENAME COLUMN deletedAt TO deleted_at;')
|
||||||
|
// alter table emp rename column emp_name to name
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 0 AFTER password;',
|
||||||
|
)
|
||||||
|
await queryFn(`UPDATE users SET password_encryption_type = 1 WHERE id IN
|
||||||
|
(SELECT user_id FROM user_contacts WHERE email_checked = 1)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn('ALTER TABLE users RENAME COLUMN created_at TO created;')
|
||||||
|
await queryFn('ALTER TABLE users RENAME COLUMN deleted_at TO deletedAt;')
|
||||||
|
await queryFn('ALTER TABLE users DROP COLUMN password_encryption_type;')
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user