migration change on default value and encrypted storage

This commit is contained in:
Julian Krauser 2025-05-04 19:01:06 +02:00
parent 03a5bb3592
commit a476bf6823
11 changed files with 82 additions and 36 deletions

View file

@ -19,6 +19,7 @@ export async function login(req: Request, res: Response): Promise<any> {
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({

View file

@ -31,7 +31,9 @@ export async function getMeById(req: Request, res: Response): Promise<any> {
export async function getMyTotp(req: Request, res: Response): Promise<any> {
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<any> {
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",

View file

@ -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";

View file

@ -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,

View file

@ -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, "<self>"),
})
secret: string;
@Column({

View file

@ -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")

View file

@ -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);
}
}

View file

@ -441,6 +441,7 @@ export default abstract class BackupHelper {
"user.firstname",
"user.lastname",
"user.secret",
"user.routine",
"user.isOwner",
])
.addSelect(["permissions.permission"])

View file

@ -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 == "<self>") 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 == "<self>") 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 "";
}

View file

@ -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<void> {
@ -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 })));

View file

@ -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<user>}
*/
static async getUserSecretAndRoutine(userId: string): Promise<user> {
//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);
});
}
}