diff --git a/src/controller/authController.ts b/src/controller/authController.ts index 9ecfa64..80492ea 100644 --- a/src/controller/authController.ts +++ b/src/controller/authController.ts @@ -19,6 +19,7 @@ export async function login(req: Request, res: Response): Promise { let username = req.body.username; let totp = req.body.totp; + // TODO: change to first routine and later login password/totp let { id, secret } = await UserService.getByUsername(username); let valid = speakeasy.totp.verify({ diff --git a/src/controller/userController.ts b/src/controller/userController.ts index 8827dc9..c8001a7 100644 --- a/src/controller/userController.ts +++ b/src/controller/userController.ts @@ -31,7 +31,9 @@ export async function getMeById(req: Request, res: Response): Promise { export async function getMyTotp(req: Request, res: Response): Promise { const userId = req.userId; - let { secret } = await UserService.getById(userId); + let { secret, routine } = await UserService.getUserSecretAndRoutine(userId); + + console.log(secret); const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`; @@ -57,7 +59,7 @@ export async function verifyMyTotp(req: Request, res: Response): Promise { const userId = req.userId; let totp = req.body.totp; - let { secret } = await UserService.getById(userId); + let { secret, routine } = await UserService.getUserSecretAndRoutine(userId); let valid = speakeasy.totp.verify({ secret: secret, encoding: "base32", diff --git a/src/controller/webapiController.ts b/src/controller/webapiController.ts index e8e4206..c1153fe 100644 --- a/src/controller/webapiController.ts +++ b/src/controller/webapiController.ts @@ -1,13 +1,5 @@ import { Request, Response } from "express"; import { JWTHelper } from "../helpers/jwtHelper"; -import { JWTToken } from "../type/jwtTypes"; -import InternalException from "../exceptions/internalException"; -import RefreshCommandHandler from "../command/refreshCommandHandler"; -import { CreateRefreshCommand, DeleteRefreshCommand } from "../command/refreshCommand"; -import UserService from "../service/management/userService"; -import speakeasy from "speakeasy"; -import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; -import RefreshService from "../service/refreshService"; import WebapiService from "../service/management/webapiService"; import ForbiddenRequestException from "../exceptions/forbiddenRequestException"; import WebapiCommandHandler from "../command/management/webapi/webapiCommandHandler"; diff --git a/src/data-source.ts b/src/data-source.ts index dad9827..fb60d49 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import "reflect-metadata"; import { DataSource } from "typeorm"; -import { DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_TYPE, DB_USERNAME } from "./env.defaults"; +import { configCheck, DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_TYPE, DB_USERNAME } from "./env.defaults"; import { user } from "./entity/management/user"; import { refresh } from "./entity/refresh"; @@ -57,6 +57,8 @@ import { MemberCreatedAt1746006549262 } from "./migrations/1746006549262-memberC import { UserLoginRoutine1746252454922 } from "./migrations/1746252454922-UserLoginRoutine"; import { SettingsFromEnv_SET1745059495808 } from "./migrations/1745059495808-settingsFromEnv_set"; +configCheck(); + const dataSource = new DataSource({ type: DB_TYPE as any, host: DB_HOST, diff --git a/src/entity/management/user.ts b/src/entity/management/user.ts index a833864..d659acb 100644 --- a/src/entity/management/user.ts +++ b/src/entity/management/user.ts @@ -2,6 +2,8 @@ import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from import { role } from "./role"; import { userPermission } from "./user_permission"; import { LoginRoutineEnum } from "../../enums/loginRoutineEnum"; +import { CodingHelper } from "../../helpers/codingHelper"; +import { APPLICATION_SECRET } from "../../env.defaults"; @Entity() export class user { @@ -20,7 +22,11 @@ export class user { @Column({ type: "varchar", length: 255 }) lastname: string; - @Column({ type: "text", select: false }) + @Column({ + type: "text", + select: false, + transformer: CodingHelper.entityBaseCoding(APPLICATION_SECRET, ""), + }) secret: string; @Column({ diff --git a/src/env.defaults.ts b/src/env.defaults.ts index 7bdcb4b..c6df8ae 100644 --- a/src/env.defaults.ts +++ b/src/env.defaults.ts @@ -11,7 +11,7 @@ export const DB_PASSWORD = process.env.DB_PASSWORD ?? ""; export const SERVER_PORT = Number(process.env.SERVER_PORT ?? 5000); -export const APPLICATION_SECRET = process.env.APPLICATION_SECRET; +export const APPLICATION_SECRET = process.env.APPLICATION_SECRET ?? ""; export const USE_SECURITY_STRICT_LIMIT = process.env.USE_SECURITY_STRICT_LIMIT ?? "true"; export const SECURITY_STRICT_LIMIT_WINDOW = (process.env.SECURITY_STRICT_LIMIT_WINDOW ?? "15m") as ms.StringValue; @@ -45,6 +45,8 @@ export function configCheck() { if (DB_USERNAME == "" || typeof DB_USERNAME != "string") throw new Error("set valid value to DB_USERNAME"); if (DB_PASSWORD == "" || typeof DB_PASSWORD != "string") throw new Error("set valid value to DB_PASSWORD"); + if (APPLICATION_SECRET == "") throw new Error("set valid APPLICATION_SECRET"); + if (isNaN(SERVER_PORT)) throw new Error("set valid numeric value to SERVER_PORT"); if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false") diff --git a/src/exceptions/databaseActionException.ts b/src/exceptions/databaseActionException.ts index b0b145c..eba9bb3 100644 --- a/src/exceptions/databaseActionException.ts +++ b/src/exceptions/databaseActionException.ts @@ -2,7 +2,7 @@ import CustomRequestException from "./customRequestException"; export default class DatabaseActionException extends CustomRequestException { constructor(action: string, table: string, err: any) { - let errstring = `${action} on ${table} with ${err?.code ?? "XX"} at ${err?.sqlMessage ?? "XX"}`; + let errstring = `${action} on ${table} with ${err?.code ?? "XX"} at ${err?.sqlMessage ?? err?.message ?? "XX"}`; super(500, errstring, err); } } diff --git a/src/helpers/backupHelper.ts b/src/helpers/backupHelper.ts index f107881..2f737ad 100644 --- a/src/helpers/backupHelper.ts +++ b/src/helpers/backupHelper.ts @@ -441,6 +441,7 @@ export default abstract class BackupHelper { "user.firstname", "user.lastname", "user.secret", + "user.routine", "user.isOwner", ]) .addSelect(["permissions.permission"]) diff --git a/src/helpers/codingHelper.ts b/src/helpers/codingHelper.ts index e6a79f9..354abc8 100644 --- a/src/helpers/codingHelper.ts +++ b/src/helpers/codingHelper.ts @@ -9,12 +9,13 @@ export abstract class CodingHelper { static entityBaseCoding(key: string = "", fallback: string = ""): ValueTransformer { return { from(val: string | null | undefined): string { - if (!val) return fallback; + if (!val || val == "") return fallback; try { - return CodingHelper.decrypt(key, val) || fallback; + return CodingHelper.decrypt(key, val, true); } catch (error) { console.error("Decryption error:", error); - return fallback; + if (fallback == "") return val; + else return fallback; } }, to(val: string | null | undefined): string { @@ -22,40 +23,47 @@ export abstract class CodingHelper { if (valueToEncrypt === "") return ""; try { - return CodingHelper.encrypt(key, valueToEncrypt); + return CodingHelper.encrypt(key, valueToEncrypt, true); } catch (error) { console.error("Encryption error:", error); + if (fallback == "") return val; return ""; } }, }; } - public static encrypt(phrase: string, content: string): string { + public static encrypt(phrase: string, content: string, passError = false): string { if (!content) return ""; - // Generiere zufälligen IV für jede Verschlüsselung (sicherer als statischer IV) - const iv = randomBytes(this.ivLength); - const key = scryptSync(phrase, "salt", 32); + try { + // Generiere zufälligen IV für jede Verschlüsselung (sicherer als statischer IV) + const iv = randomBytes(this.ivLength); + const key = scryptSync(phrase, "salt", 32); - const cipher = createCipheriv(this.algorithm, Uint8Array.from(key), Uint8Array.from(iv)); + const cipher = createCipheriv(this.algorithm, Uint8Array.from(key), Uint8Array.from(iv)); - // Verschlüssele den Inhalt - let encrypted = cipher.update(content, "utf8", "hex"); - encrypted += cipher.final("hex"); + // Verschlüssele den Inhalt + let encrypted = cipher.update(content, "utf8", "hex"); + encrypted += cipher.final("hex"); - // Speichere das Auth-Tag für GCM (wichtig für die Entschlüsselung) - const authTag = cipher.getAuthTag(); + // Speichere das Auth-Tag für GCM (wichtig für die Entschlüsselung) + const authTag = cipher.getAuthTag(); - // Gib das Format: iv:verschlüsselter_text:authTag zurück - return Buffer.concat([ - Uint8Array.from(iv), - Uint8Array.from(Buffer.from(encrypted, "hex")), - Uint8Array.from(authTag), - ]).toString("base64"); + // Gib das Format: iv:verschlüsselter_text:authTag zurück + return Buffer.concat([ + Uint8Array.from(iv), + Uint8Array.from(Buffer.from(encrypted, "hex")), + Uint8Array.from(authTag), + ]).toString("base64"); + } catch (error) { + if (passError) throw error; + console.error("Encryption failed:", error); + return ""; + } } - public static decrypt(phrase: string, content: string): string { + public static decrypt(phrase: string, content: string, passError = false): string { if (!content) return ""; try { @@ -79,6 +87,7 @@ export abstract class CodingHelper { return decrypted; } catch (error) { + if (passError) throw error; console.error("Decryption failed:", error); return ""; } diff --git a/src/migrations/1746252454922-UserLoginRoutine.ts b/src/migrations/1746252454922-UserLoginRoutine.ts index 0da51e0..3072e37 100644 --- a/src/migrations/1746252454922-UserLoginRoutine.ts +++ b/src/migrations/1746252454922-UserLoginRoutine.ts @@ -1,5 +1,8 @@ import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; import { getDefaultByORM, getTypeByORM } from "./ormHelper"; +import { LoginRoutineEnum } from "../enums/loginRoutineEnum"; +import { CodingHelper } from "../helpers/codingHelper"; +import { APPLICATION_SECRET } from "../env.defaults"; export class UserLoginRoutine1746252454922 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { @@ -9,7 +12,11 @@ export class UserLoginRoutine1746252454922 implements MigrationInterface { await queryRunner.addColumns("user", [ new TableColumn({ name: "secret", ...getTypeByORM("text") }), - new TableColumn({ name: "routine", ...getTypeByORM("varchar") }), + new TableColumn({ + name: "routine", + ...getTypeByORM("varchar"), + default: getDefaultByORM("string", LoginRoutineEnum.totp), + }), ]); await queryRunner.manager.getRepository("user").save(users.map((u) => ({ id: u.id, secret: u.secret }))); diff --git a/src/service/management/userService.ts b/src/service/management/userService.ts index 8fe4dd4..8119d5d 100644 --- a/src/service/management/userService.ts +++ b/src/service/management/userService.ts @@ -129,4 +129,28 @@ export default abstract class UserService { throw new DatabaseActionException("SELECT", "userRoles", err); }); } + + /** + * @description get secret and routine by iser + * @param userId string + * @returns {Promise} + */ + static async getUserSecretAndRoutine(userId: string): Promise { + //TODO: not working yet + return await dataSource + .getRepository(user) + .createQueryBuilder("user") + .select("user.id") + .addSelect("user.secret") + .addSelect("user.routine") + .where("user.id = :id", { id: userId }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + console.log(err); + throw new DatabaseActionException("SELECT", "user credentials", err); + }); + } }