migration change on default value and encrypted storage
This commit is contained in:
parent
03a5bb3592
commit
a476bf6823
11 changed files with 82 additions and 36 deletions
|
@ -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({
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -441,6 +441,7 @@ export default abstract class BackupHelper {
|
|||
"user.firstname",
|
||||
"user.lastname",
|
||||
"user.secret",
|
||||
"user.routine",
|
||||
"user.isOwner",
|
||||
])
|
||||
.addSelect(["permissions.permission"])
|
||||
|
|
|
@ -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 "";
|
||||
}
|
||||
|
|
|
@ -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 })));
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue