From 73ced2291e0cff0e0c5263520854ebf7283d244a Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 8 Nov 2022 13:41:33 +0100 Subject: [PATCH 01/10] database migration added --- backend/src/config/index.ts | 2 +- .../0053-change_password_encryption/User.ts | 127 ++++++++++++++++++ .../UserContact.ts | 66 +++++++++ database/entity/User.ts | 2 +- database/entity/UserContact.ts | 2 +- .../0053-change_password_encryption | 38 ++++++ 6 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 database/entity/0053-change_password_encryption/User.ts create mode 100644 database/entity/0053-change_password_encryption/UserContact.ts create mode 100644 database/migrations/0053-change_password_encryption 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/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts new file mode 100644 index 000000000..bf2d02268 --- /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: 1, + }) + 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..05bfdfffe --- /dev/null +++ b/database/entity/0053-change_password_encryption/UserContact.ts @@ -0,0 +1,66 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToOne, + JoinColumn, + ManyToOne, +} 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 + + @ManyToOne(() => User, (user) => user.userContacts) + @JoinColumn({ name: 'user_id' }) + contactUser: User +} 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 b/database/migrations/0053-change_password_encryption new file mode 100644 index 000000000..1b87e2511 --- /dev/null +++ b/database/migrations/0053-change_password_encryption @@ -0,0 +1,38 @@ +/* MIGRATION TO ADD GRADIDO_ID + * + * This migration adds and renames columns to and 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 1 AFTER password;', + ) + + // TODO these steps comes after verification and test + /* + await queryFn('ALTER TABLE users DROP COLUMN public_key;') + await queryFn('ALTER TABLE users DROP COLUMN privkey;') + await queryFn('ALTER TABLE users DROP COLUMN email_hash;') + await queryFn('ALTER TABLE users DROP COLUMN passphrase;') + */ +} + +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;') + + // TODO these steps comes after verification and test + /* + await queryFn('ALTER TABLE users ADD COLUMN public_key binary(32) DEFAULT NULL;') + await queryFn('ALTER TABLE users ADD COLUMN privkey binary(80) DEFAULT NULL;') + await queryFn('ALTER TABLE users ADD COLUMN email_hash binary(32) DEFAULT NULL;') + await queryFn('ALTER TABLE users ADD COLUMN passphrase text DEFAULT NULL;') + */ +} \ No newline at end of file From ac6b8e666ff893d19786b51f20bff36b6c771038 Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 9 Nov 2022 10:46:01 +0100 Subject: [PATCH 02/10] encryption interface --- .../graphql/enum/PasswordEncryptionType.ts | 12 +++++ backend/src/password/EmailEncryptr.ts | 19 +++++++ backend/src/password/EncryptorUtils.ts | 52 +++++++++++++++++++ backend/src/password/GradidoIDEncryptr.ts | 19 +++++++ backend/src/password/PasswordEncryptr.ts | 6 +++ ...ion => 0053-change_password_encryption.ts} | 4 +- 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 backend/src/graphql/enum/PasswordEncryptionType.ts create mode 100644 backend/src/password/EmailEncryptr.ts create mode 100644 backend/src/password/EncryptorUtils.ts create mode 100644 backend/src/password/GradidoIDEncryptr.ts create mode 100644 backend/src/password/PasswordEncryptr.ts rename database/migrations/{0053-change_password_encryption => 0053-change_password_encryption.ts} (97%) diff --git a/backend/src/graphql/enum/PasswordEncryptionType.ts b/backend/src/graphql/enum/PasswordEncryptionType.ts new file mode 100644 index 000000000..4f23aa693 --- /dev/null +++ b/backend/src/graphql/enum/PasswordEncryptionType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +export enum PasswordEncryptionType { + EMAIL = 0, + ONE_TIME = 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/password/EmailEncryptr.ts b/backend/src/password/EmailEncryptr.ts new file mode 100644 index 000000000..59098e207 --- /dev/null +++ b/backend/src/password/EmailEncryptr.ts @@ -0,0 +1,19 @@ +import { User } from '@entity/User' +import { PasswordEncryptr } from './PasswordEncryptr' +import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' + +export class EmailEncryptr implements PasswordEncryptr { + async encryptPassword(dbUser: User, password: string): Promise { + const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.emailContact.email, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + + return passwordHash + } + + async verifyPassword(dbUser: User, password: string): Promise { + if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { + return false + } + return true + } +} diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts new file mode 100644 index 000000000..6609ff075 --- /dev/null +++ b/backend/src/password/EncryptorUtils.ts @@ -0,0 +1,52 @@ +import CONFIG from '@/config' +import { backendLogger as logger } from '@/server/logger' + +// 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) + + logger.debug( + `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`, + ) + return [encryptionKeyHash, encryptionKey] +} diff --git a/backend/src/password/GradidoIDEncryptr.ts b/backend/src/password/GradidoIDEncryptr.ts new file mode 100644 index 000000000..630bee056 --- /dev/null +++ b/backend/src/password/GradidoIDEncryptr.ts @@ -0,0 +1,19 @@ +import { User } from '@entity/User' +import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' +import { PasswordEncryptr } from './PasswordEncryptr' + +export class GradidoIDEncryptr implements PasswordEncryptr { + async encryptPassword(dbUser: User, password: string): Promise { + const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.gradidoID, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + + return passwordHash + } + + async verifyPassword(dbUser: User, password: string): Promise { + if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { + return false + } + return true + } +} diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts new file mode 100644 index 000000000..24d949be9 --- /dev/null +++ b/backend/src/password/PasswordEncryptr.ts @@ -0,0 +1,6 @@ +import { User } from '@entity/User' + +export interface PasswordEncryptr { + encryptPassword(dbUser: User, password: string): Promise + verifyPassword(dbUser: User, password: string): Promise +} diff --git a/database/migrations/0053-change_password_encryption b/database/migrations/0053-change_password_encryption.ts similarity index 97% rename from database/migrations/0053-change_password_encryption rename to database/migrations/0053-change_password_encryption.ts index 1b87e2511..5d880689f 100644 --- a/database/migrations/0053-change_password_encryption +++ b/database/migrations/0053-change_password_encryption.ts @@ -1,4 +1,4 @@ -/* MIGRATION TO ADD GRADIDO_ID +/* MIGRATION TO ADD ENCRYPTION TO PASSWORDS * * This migration adds and renames columns to and in the table `users` */ @@ -35,4 +35,4 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom await queryFn('ALTER TABLE users ADD COLUMN email_hash binary(32) DEFAULT NULL;') await queryFn('ALTER TABLE users ADD COLUMN passphrase text DEFAULT NULL;') */ -} \ No newline at end of file +} From 107cc016ba0064462ccde429699a1fb711f61508 Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 15 Nov 2022 20:30:09 +0100 Subject: [PATCH 03/10] set up completed and working --- .../graphql/enum/PasswordEncryptionType.ts | 4 +-- .../src/graphql/resolver/UserResolver.test.ts | 1 + backend/src/password/EmailEncryptr.ts | 19 ------------ backend/src/password/EncryptorUtils.ts | 14 +++++++++ backend/src/password/GradidoIDEncryptr.ts | 19 ------------ backend/src/password/PasswordEncryptr.ts | 30 +++++++++++++++++-- backend/src/util/communityUser.ts | 2 ++ .../0053-change_password_encryption/User.ts | 28 ++++++++--------- .../UserContact.ts | 6 ---- 9 files changed, 60 insertions(+), 63 deletions(-) delete mode 100644 backend/src/password/EmailEncryptr.ts delete mode 100644 backend/src/password/GradidoIDEncryptr.ts diff --git a/backend/src/graphql/enum/PasswordEncryptionType.ts b/backend/src/graphql/enum/PasswordEncryptionType.ts index 4f23aa693..b3a00d748 100644 --- a/backend/src/graphql/enum/PasswordEncryptionType.ts +++ b/backend/src/graphql/enum/PasswordEncryptionType.ts @@ -1,8 +1,8 @@ import { registerEnumType } from 'type-graphql' export enum PasswordEncryptionType { - EMAIL = 0, - ONE_TIME = 1, + NO_PASSWORD = 0, + EMAIL = 1, GRADIDO_ID = 2, } diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index cf4ad8d4b..791ed4c8e 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -146,6 +146,7 @@ describe('UserResolver', () => { publisherId: 1234, referrerId: null, contributionLinkId: null, + passwordEncryptionType: 1, }, ]) const valUUID = validateUUID(user[0].gradidoID) diff --git a/backend/src/password/EmailEncryptr.ts b/backend/src/password/EmailEncryptr.ts deleted file mode 100644 index 59098e207..000000000 --- a/backend/src/password/EmailEncryptr.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { User } from '@entity/User' -import { PasswordEncryptr } from './PasswordEncryptr' -import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' - -export class EmailEncryptr implements PasswordEncryptr { - async encryptPassword(dbUser: User, password: string): Promise { - const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.emailContact.email, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - - return passwordHash - } - - async verifyPassword(dbUser: User, password: string): Promise { - if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { - return false - } - return true - } -} diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 6609ff075..5f6f4b416 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -1,5 +1,7 @@ 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') @@ -50,3 +52,15 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): ) return [encryptionKeyHash, encryptionKey] } + +export const getBasicCryptographicKey = (dbUser: User): string | null => { + if (dbUser.passwordEncryptionType === PasswordEncryptionType.NO_PASSWORD) { + return null + } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.EMAIL) { + return dbUser.emailContact.email + } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.GRADIDO_ID) { + return dbUser.gradidoID + } + + throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) +} diff --git a/backend/src/password/GradidoIDEncryptr.ts b/backend/src/password/GradidoIDEncryptr.ts deleted file mode 100644 index 630bee056..000000000 --- a/backend/src/password/GradidoIDEncryptr.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { User } from '@entity/User' -import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' -import { PasswordEncryptr } from './PasswordEncryptr' - -export class GradidoIDEncryptr implements PasswordEncryptr { - async encryptPassword(dbUser: User, password: string): Promise { - const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.gradidoID, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - - return passwordHash - } - - async verifyPassword(dbUser: User, password: string): Promise { - if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { - return false - } - return true - } -} diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts index 24d949be9..24dc7f352 100644 --- a/backend/src/password/PasswordEncryptr.ts +++ b/backend/src/password/PasswordEncryptr.ts @@ -1,6 +1,30 @@ import { User } from '@entity/User' +import { logger } from '@test/testSetup' +import { getBasicCryptographicKey, SecretKeyCryptographyCreateKey } from './EncryptorUtils' -export interface PasswordEncryptr { - encryptPassword(dbUser: User, password: string): Promise - verifyPassword(dbUser: User, password: string): Promise +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 + } + + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } } diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index e885b7043..87d0432dc 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -26,6 +26,8 @@ const communityDbUser: dbUser = { isAdmin: null, publisherId: 0, passphrase: '', + // default password encryption type + passwordEncryptionType: 2, 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 index bf2d02268..fac4e1031 100644 --- a/database/entity/0053-change_password_encryption/User.ts +++ b/database/entity/0053-change_password_encryption/User.ts @@ -34,20 +34,20 @@ export class User extends BaseEntity { }) 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 - */ + @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' }) diff --git a/database/entity/0053-change_password_encryption/UserContact.ts b/database/entity/0053-change_password_encryption/UserContact.ts index 05bfdfffe..97b12d4cd 100644 --- a/database/entity/0053-change_password_encryption/UserContact.ts +++ b/database/entity/0053-change_password_encryption/UserContact.ts @@ -5,8 +5,6 @@ import { Column, DeleteDateColumn, OneToOne, - JoinColumn, - ManyToOne, } from 'typeorm' import { User } from './User' @@ -59,8 +57,4 @@ export class UserContact extends BaseEntity { @DeleteDateColumn({ name: 'deleted_at', nullable: true }) deletedAt: Date | null - - @ManyToOne(() => User, (user) => user.userContacts) - @JoinColumn({ name: 'user_id' }) - contactUser: User } From 47d8469eb3292edbeeebcc4bb85273058b48afaf Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 17 Nov 2022 13:38:16 +0100 Subject: [PATCH 04/10] new password implementation set up and test --- .../src/graphql/resolver/UserResolver.test.ts | 65 ++++++++++++++++- backend/src/graphql/resolver/UserResolver.ts | 71 +++++-------------- backend/src/password/PasswordEncryptr.ts | 46 ++++++------ backend/src/util/communityUser.ts | 2 +- 4 files changed, 103 insertions(+), 81 deletions(-) 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.') }, From 39d006351d8e6c04bdaa6b6025c9e7898c724326 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 17 Nov 2022 14:10:17 +0100 Subject: [PATCH 05/10] unused import added wrongly --- backend/src/graphql/resolver/UserResolver.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index d3bfb1c66..03aabfbb4 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -38,7 +38,6 @@ 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' From fc23fc9e87768e0fc5f5f57235261c18b4518f54 Mon Sep 17 00:00:00 2001 From: joseji Date: Sun, 20 Nov 2022 18:14:43 +0100 Subject: [PATCH 06/10] fixes done --- .../src/graphql/resolver/UserResolver.test.ts | 6 ++-- backend/src/graphql/resolver/UserResolver.ts | 4 ++- backend/src/password/EncryptorUtils.ts | 27 ++++++++++-------- backend/src/password/PasswordEncryptor.ts | 17 +++++++++++ backend/src/password/PasswordEncryptr.ts | 28 ------------------- .../0053-change_password_encryption/User.ts | 2 +- .../0053-change_password_encryption.ts | 22 ++------------- 7 files changed, 42 insertions(+), 64 deletions(-) create mode 100644 backend/src/password/PasswordEncryptor.ts delete mode 100644 backend/src/password/PasswordEncryptr.ts diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 03aabfbb4..a3e2dbe21 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -36,7 +36,7 @@ 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 { encryptPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' @@ -149,7 +149,7 @@ describe('UserResolver', () => { publisherId: 1234, referrerId: null, contributionLinkId: null, - passwordEncryptionType: 1, + passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, }, ]) const valUUID = validateUUID(user[0].gradidoID) @@ -1168,7 +1168,7 @@ describe('UserResolver', () => { describe('user just registered', () => { let bibi: User - it('password type should be gradido id', async () => { + it('has password type gradido id', async () => { const users = await User.find() bibi = users[1] diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index ef5edd747..a9006bbb6 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -40,7 +40,7 @@ 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 { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -432,6 +432,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) { @@ -780,6 +781,7 @@ export class UserResolver { logger.debug('PrivateKey encrypted...') // Save new password hash and newly encrypted private key + 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 index 5f6f4b416..2ca47109d 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -47,20 +47,23 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): 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] } -export const getBasicCryptographicKey = (dbUser: User): string | null => { - if (dbUser.passwordEncryptionType === PasswordEncryptionType.NO_PASSWORD) { - return null - } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.EMAIL) { - return dbUser.emailContact.email - } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.GRADIDO_ID) { - return dbUser.gradidoID +export const getUserCryptographicSalt = (dbUser: User): string => { + switch (dbUser.passwordEncryptionType) { + case PasswordEncryptionType.NO_PASSWORD: { + 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: + throw new 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..2c6ebfb0f --- /dev/null +++ b/backend/src/password/PasswordEncryptor.ts @@ -0,0 +1,17 @@ +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 basicKey = getUserCryptographicSalt(dbUser) + 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 => { + if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { + return false + } + return true +} diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts deleted file mode 100644 index b8ef2de31..000000000 --- a/backend/src/password/PasswordEncryptr.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { User } from '@entity/User' -// import { logger } from '@test/testSetup' getting error "jest is not defined" -import { getBasicCryptographicKey, SecretKeyCryptographyCreateKey } from './EncryptorUtils' - -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/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts index fac4e1031..2a3332925 100644 --- a/database/entity/0053-change_password_encryption/User.ts +++ b/database/entity/0053-change_password_encryption/User.ts @@ -88,7 +88,7 @@ export class User extends BaseEntity { type: 'int', unsigned: true, nullable: false, - default: 1, + default: 0, }) passwordEncryptionType: number diff --git a/database/migrations/0053-change_password_encryption.ts b/database/migrations/0053-change_password_encryption.ts index 5d880689f..0c8632186 100644 --- a/database/migrations/0053-change_password_encryption.ts +++ b/database/migrations/0053-change_password_encryption.ts @@ -1,6 +1,6 @@ -/* MIGRATION TO ADD ENCRYPTION TO PASSWORDS +/* MIGRATION TO ADD ENCRYPTION TYPE TO PASSWORDS * - * This migration adds and renames columns to and in the table `users` + * This migration adds and renames columns in the table `users` */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ @@ -11,28 +11,12 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis 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 1 AFTER password;', + 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 0 AFTER password;', ) - - // TODO these steps comes after verification and test - /* - await queryFn('ALTER TABLE users DROP COLUMN public_key;') - await queryFn('ALTER TABLE users DROP COLUMN privkey;') - await queryFn('ALTER TABLE users DROP COLUMN email_hash;') - await queryFn('ALTER TABLE users DROP COLUMN passphrase;') - */ } 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;') - - // TODO these steps comes after verification and test - /* - await queryFn('ALTER TABLE users ADD COLUMN public_key binary(32) DEFAULT NULL;') - await queryFn('ALTER TABLE users ADD COLUMN privkey binary(80) DEFAULT NULL;') - await queryFn('ALTER TABLE users ADD COLUMN email_hash binary(32) DEFAULT NULL;') - await queryFn('ALTER TABLE users ADD COLUMN passphrase text DEFAULT NULL;') - */ } From 3aeb9dd0f17e8abdaaec37258bdf6b6dd2b22950 Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 22 Nov 2022 11:12:07 +0100 Subject: [PATCH 07/10] fixes added --- .../src/graphql/resolver/UserResolver.test.ts | 24 +++++++++++++++++++ backend/src/password/EncryptorUtils.ts | 2 ++ backend/src/password/PasswordEncryptor.ts | 4 ++-- .../0053-change_password_encryption.ts | 2 ++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 4e05aadd6..377dfa131 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -39,6 +39,7 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { encryptPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' +import { tokenToString } from 'typescript' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -1220,6 +1221,29 @@ describe('UserResolver', () => { }), ) }) + + 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, + }, + }, + }), + ) + }) }) }) }) diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 2ca47109d..971b6a32e 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -53,6 +53,7 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): 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: { @@ -64,6 +65,7 @@ export const getUserCryptographicSalt = (dbUser: User): string => { 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 index 2c6ebfb0f..3dc0736df 100644 --- a/backend/src/password/PasswordEncryptor.ts +++ b/backend/src/password/PasswordEncryptor.ts @@ -3,8 +3,8 @@ import { User } from '@entity/User' import { getUserCryptographicSalt, SecretKeyCryptographyCreateKey } from './EncryptorUtils' export const encryptPassword = (dbUser: User, password: string): bigint => { - const basicKey = getUserCryptographicSalt(dbUser) - const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash + const salt = getUserCryptographicSalt(dbUser) + const keyBuffer = SecretKeyCryptographyCreateKey(salt, password) // return short and long hash const passwordHash = keyBuffer[0].readBigUInt64LE() return passwordHash } diff --git a/database/migrations/0053-change_password_encryption.ts b/database/migrations/0053-change_password_encryption.ts index 0c8632186..635109430 100644 --- a/database/migrations/0053-change_password_encryption.ts +++ b/database/migrations/0053-change_password_encryption.ts @@ -13,6 +13,8 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis 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>) { From e55f516c512019b15081ad476278884b35631587 Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 22 Nov 2022 11:19:42 +0100 Subject: [PATCH 08/10] removed unused import --- backend/src/graphql/resolver/UserResolver.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 377dfa131..200ba8163 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -39,7 +39,6 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { encryptPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' -import { tokenToString } from 'typescript' // import { klicktippSignIn } from '@/apis/KlicktippController' From 60e3f628325b38cfd5798bf5ebbec00b6a95c99b Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 23 Nov 2022 11:32:27 +0100 Subject: [PATCH 09/10] enum use on community user initialization --- backend/src/util/communityUser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 9913418fb..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' @@ -27,7 +28,7 @@ const communityDbUser: dbUser = { publisherId: 0, passphrase: '', // default password encryption type - passwordEncryptionType: 0, + passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, hasId: function (): boolean { throw new Error('Function not implemented.') }, From 2a74b924721257b239a0148ec7bd8b28e424a8d5 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 24 Nov 2022 11:21:47 +0100 Subject: [PATCH 10/10] user saving on db --- .../src/graphql/resolver/UserResolver.test.ts | 16 +++++++++++----- backend/src/graphql/resolver/UserResolver.ts | 1 + backend/src/password/PasswordEncryptor.ts | 5 +---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 200ba8163..d8472fba9 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1193,9 +1193,11 @@ describe('UserResolver', () => { let bibi: User beforeAll(async () => { - const users = await User.find() - bibi = users[1] - + const usercontact = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + bibi = usercontact.user bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL bibi.password = SecretKeyCryptographyCreateKey( 'bibi@bloxberg.de', @@ -1208,11 +1210,15 @@ describe('UserResolver', () => { it('changes to gradidoID on login', async () => { await mutate({ mutation: login, variables: variables }) - const users = await User.find() - bibi = users[0] + 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(), diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index b376c632f..752c585fd 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -311,6 +311,7 @@ export class UserResolver { 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) diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts index 3dc0736df..1735106c1 100644 --- a/backend/src/password/PasswordEncryptor.ts +++ b/backend/src/password/PasswordEncryptor.ts @@ -10,8 +10,5 @@ export const encryptPassword = (dbUser: User, password: string): bigint => { } export const verifyPassword = (dbUser: User, password: string): boolean => { - if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { - return false - } - return true + return dbUser.password.toString() === encryptPassword(dbUser, password).toString() }