diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e7139033b..26227b90d 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) 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 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/backend/src/graphql/enum/PasswordEncryptionType.ts b/backend/src/graphql/enum/PasswordEncryptionType.ts new file mode 100644 index 000000000..b3a00d748 --- /dev/null +++ b/backend/src/graphql/enum/PasswordEncryptionType.ts @@ -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 +}) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 6323abfde..d8472fba9 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -36,6 +36,9 @@ 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/PasswordEncryptor' +import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' +import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -146,6 +149,7 @@ describe('UserResolver', () => { publisherId: 1234, referrerId: null, contributionLinkId: null, + passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, }, ]) const valUUID = validateUUID(user[0].gradidoID) @@ -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,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', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 707b7ac49..752c585fd 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -40,17 +40,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/PasswordEncryptor' +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 => { @@ -107,48 +105,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...') @@ -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 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) + await dbUser.save() + } // 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...') @@ -481,6 +442,7 @@ export class UserResolver { dbUser.lastName = lastName dbUser.language = language dbUser.publisherId = publisherId + dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD dbUser.passphrase = passphrase.join(' ') logger.debug('new dbUser=' + dbUser) if (redeemCode) { @@ -634,7 +596,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!', @@ -692,10 +654,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 ...') @@ -801,7 +764,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!', @@ -813,7 +776,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`) } @@ -829,7 +792,8 @@ export class UserResolver { logger.debug('PrivateKey encrypted...') // 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 } diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts new file mode 100644 index 000000000..971b6a32e --- /dev/null +++ b/backend/src/password/EncryptorUtils.ts @@ -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}`) + } +} diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts new file mode 100644 index 000000000..1735106c1 --- /dev/null +++ b/backend/src/password/PasswordEncryptor.ts @@ -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() +} diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index e885b7043..298348f0f 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { PasswordEncryptionType } from '@/graphql/enum/PasswordEncryptionType' import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' import { User as dbUser } from '@entity/User' import { UserContact } from '@entity/UserContact' @@ -26,6 +27,8 @@ const communityDbUser: dbUser = { isAdmin: null, publisherId: 0, passphrase: '', + // default password encryption type + passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, hasId: function (): boolean { throw new Error('Function not implemented.') }, diff --git a/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts new file mode 100644 index 000000000..2a3332925 --- /dev/null +++ b/database/entity/0053-change_password_encryption/User.ts @@ -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[] +} diff --git a/database/entity/0053-change_password_encryption/UserContact.ts b/database/entity/0053-change_password_encryption/UserContact.ts new file mode 100644 index 000000000..97b12d4cd --- /dev/null +++ b/database/entity/0053-change_password_encryption/UserContact.ts @@ -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 +} diff --git a/database/entity/User.ts b/database/entity/User.ts index d073f428a..b3c00a9b4 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0049-add_user_contacts_table/User' +export { User } from './0053-change_password_encryption/User' diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts index a368bb7ca..dd74e65c4 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0049-add_user_contacts_table/UserContact' +export { UserContact } from './0053-change_password_encryption/UserContact' diff --git a/database/migrations/0053-change_password_encryption.ts b/database/migrations/0053-change_password_encryption.ts new file mode 100644 index 000000000..635109430 --- /dev/null +++ b/database/migrations/0053-change_password_encryption.ts @@ -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>) { + 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>) { + 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;') +}