diff --git a/.env.example b/.env.example index 22b3a14..7770a99 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,8 @@ JWT_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 15m REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 1d PWA_REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 5d +CODING_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 # besitzt default + MAIL_USERNAME = mail_username MAIL_PASSWORD = mail_password MAIL_HOST = mail_hoststring diff --git a/package-lock.json b/package-lock.json index dce2290..f8d7922 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@ff-admin/webapi-client": "^1.1.1", "@socket.io/admin-ui": "^0.5.1", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^5.0.0-beta.3", "express-rate-limit": "^7.5.0", @@ -1407,6 +1408,13 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/dayjs": { "version": "1.11.12", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", diff --git a/package.json b/package.json index 3d2797d..bf59964 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@ff-admin/webapi-client": "^1.1.1", "@socket.io/admin-ui": "^0.5.1", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^5.0.0-beta.3", "express-rate-limit": "^7.5.0", diff --git a/src/entity/operation/mission.ts b/src/entity/operation/mission.ts index 529b741..256ec60 100644 --- a/src/entity/operation/mission.ts +++ b/src/entity/operation/mission.ts @@ -5,6 +5,8 @@ import { mission_vehicle } from "./mission_vehicle"; import { mission_equipment } from "./mission_equipment"; import { mission_contact } from "./mission_contact"; import { getTypeByORM } from "../../migrations/ormHelper"; +import { CodingHelper } from "../../helpers/codingHelper"; +import { CODING_SECRET } from "../../env.defaults"; @Entity() export class mission { @@ -29,9 +31,12 @@ export class mission { @Column({ type: "varchar", length: 255, default: "" }) keyword: string; - @Column({ type: "varchar", length: 255, default: "" }) + @Column({ type: "text", default: "", transformer: CodingHelper.entityBaseCoding(CODING_SECRET) }) location: string; + @Column({ type: "varchar", length: 255, default: "" }) + city: string; + @Column({ type: "varchar", length: 255, default: "" }) others: string; @@ -41,7 +46,7 @@ export class mission { @Column({ type: "int", default: 0 }) recovered: number; - @Column({ type: "text", default: "[]" }) + @Column({ type: "text", default: "", transformer: CodingHelper.entityBaseCoding(CODING_SECRET, "[]") }) description: string; @Column({ type: "bigint", default: 0 }) diff --git a/src/entity/operation/mission_contact.ts b/src/entity/operation/mission_contact.ts index e94c1ea..a9f8b89 100644 --- a/src/entity/operation/mission_contact.ts +++ b/src/entity/operation/mission_contact.ts @@ -1,5 +1,7 @@ import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; import { mission } from "./mission"; +import { CodingHelper } from "../../helpers/codingHelper"; +import { CODING_SECRET } from "../../env.defaults"; @Entity() export class mission_contact { @@ -9,19 +11,19 @@ export class mission_contact { @PrimaryColumn({ type: "varchar", length: 36 }) contactId: string; - @Column({ type: "varchar", length: 255, default: "" }) + @Column({ type: "text", default: "", transformer: CodingHelper.entityBaseCoding(CODING_SECRET) }) firstname: string; - @Column({ type: "varchar", length: 255, default: "" }) + @Column({ type: "text", default: "", transformer: CodingHelper.entityBaseCoding(CODING_SECRET) }) lastname: string; - @Column({ type: "varchar", length: 255, default: "" }) + @Column({ type: "text", default: "", transformer: CodingHelper.entityBaseCoding(CODING_SECRET) }) phone: string; - @Column({ type: "varchar", length: 255, default: "" }) + @Column({ type: "text", default: "", transformer: CodingHelper.entityBaseCoding(CODING_SECRET) }) address: string; - @Column({ type: "varchar", length: 255, default: "" }) + @Column({ type: "text", default: "", transformer: CodingHelper.entityBaseCoding(CODING_SECRET) }) note: string; @ManyToOne(() => mission, { diff --git a/src/env.defaults.ts b/src/env.defaults.ts index 9a046ba..0489c08 100644 --- a/src/env.defaults.ts +++ b/src/env.defaults.ts @@ -16,6 +16,9 @@ export const JWT_EXPIRATION = process.env.JWT_EXPIRATION ?? "15m"; export const REFRESH_EXPIRATION = process.env.REFRESH_EXPIRATION ?? "1d"; export const PWA_REFRESH_EXPIRATION = process.env.PWA_REFRESH_EXPIRATION ?? "5d"; +export const CODING_SECRET = + process.env.CODING_SECRET ?? "my_coding_secret_string_41YzO6JiE6iGNUZsZXX8EQa6L2DpqtdZiPK6VYRS"; + export const MAIL_USERNAME = process.env.MAIL_USERNAME ?? ""; export const MAIL_PASSWORD = process.env.MAIL_PASSWORD ?? ""; export const MAIL_HOST = process.env.MAIL_HOST ?? ""; @@ -75,6 +78,8 @@ export function configCheck() { checkMS(REFRESH_EXPIRATION, "REFRESH_EXPIRATION"); checkMS(PWA_REFRESH_EXPIRATION, "PWA_REFRESH_EXPIRATION"); + if (CODING_SECRET == "" || typeof CODING_SECRET != "string") throw new Error("set valid value to JWT_SECRET"); + if (MAIL_USERNAME == "" || typeof MAIL_USERNAME != "string") throw new Error("set valid value to MAIL_USERNAME"); if (MAIL_PASSWORD == "" || typeof MAIL_PASSWORD != "string") throw new Error("set valid value to MAIL_PASSWORD"); if (MAIL_HOST == "" || typeof MAIL_HOST != "string") throw new Error("set valid value to MAIL_HOST"); diff --git a/src/helpers/codingHelper.ts b/src/helpers/codingHelper.ts new file mode 100644 index 0000000..e6a79f9 --- /dev/null +++ b/src/helpers/codingHelper.ts @@ -0,0 +1,86 @@ +import { createCipheriv, createDecipheriv, scryptSync, randomBytes } from "crypto"; +import { ValueTransformer } from "typeorm"; + +export abstract class CodingHelper { + private static readonly algorithm = "aes-256-gcm"; + private static readonly ivLength = 16; + private static readonly authTagLength = 16; + + static entityBaseCoding(key: string = "", fallback: string = ""): ValueTransformer { + return { + from(val: string | null | undefined): string { + if (!val) return fallback; + try { + return CodingHelper.decrypt(key, val) || fallback; + } catch (error) { + console.error("Decryption error:", error); + return fallback; + } + }, + to(val: string | null | undefined): string { + const valueToEncrypt = val || fallback; + if (valueToEncrypt === "") return ""; + + try { + return CodingHelper.encrypt(key, valueToEncrypt); + } catch (error) { + console.error("Encryption error:", error); + return ""; + } + }, + }; + } + + public static encrypt(phrase: string, content: string): 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); + + 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"); + + // 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"); + } + + public static decrypt(phrase: string, content: string): string { + if (!content) return ""; + + try { + // Dekodiere den Base64-String + const buffer = Buffer.from(content, "base64"); + + // Extrahiere IV, verschlüsselten Text und Auth-Tag + const iv = buffer.subarray(0, this.ivLength); + const authTag = buffer.subarray(buffer.length - this.authTagLength); + const encryptedText = buffer.subarray(this.ivLength, buffer.length - this.authTagLength).toString("hex"); + + const key = scryptSync(phrase, "salt", 32); + + // Erstelle Decipher und setze Auth-Tag + const decipher = createDecipheriv(this.algorithm, Uint8Array.from(key), Uint8Array.from(iv)); + decipher.setAuthTag(Uint8Array.from(authTag)); + + // Entschlüssele den Text + let decrypted = decipher.update(encryptedText, "hex", "utf8"); + decrypted += decipher.final("utf8"); + + return decrypted; + } catch (error) { + console.error("Decryption failed:", error); + return ""; + } + } +} diff --git a/src/migrations/baseSchemaTables/operation.ts b/src/migrations/baseSchemaTables/operation.ts index fa7999e..eec1e12 100644 --- a/src/migrations/baseSchemaTables/operation.ts +++ b/src/migrations/baseSchemaTables/operation.ts @@ -11,11 +11,12 @@ export const mission_table = new Table({ { name: "mission_start", ...getTypeByORM("datetime", true, 6), default: getDefaultByORM("null") }, { name: "mission_end", ...getTypeByORM("datetime", true, 6), default: getDefaultByORM("null") }, { name: "keyword", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, - { name: "location", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, + { name: "location", ...getTypeByORM("text"), default: getDefaultByORM("string") }, + { name: "city", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, { name: "others", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, { name: "rescued", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }, { name: "recovered", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) }, - { name: "description", ...getTypeByORM("text"), default: getDefaultByORM("string", "[]") }, + { name: "description", ...getTypeByORM("text"), default: getDefaultByORM("string") }, { name: "last_update", ...getTypeByORM("bigint"), default: getDefaultByORM("number", 0) }, { name: "createdAt", ...getTypeByORM("datetime", false, 6), default: getDefaultByORM("currentTimestamp") }, ], @@ -117,11 +118,11 @@ export const mission_contact_table = new Table({ columns: [ { name: "missionId", ...getTypeByORM("uuid"), isPrimary: true }, { name: "contactId", ...getTypeByORM("uuid"), isPrimary: true }, - { name: "firstname", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, - { name: "lastname", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, - { name: "phone", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, - { name: "address", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, - { name: "note", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }, + { name: "firstname", ...getTypeByORM("text"), default: getDefaultByORM("string") }, + { name: "lastname", ...getTypeByORM("text"), default: getDefaultByORM("string") }, + { name: "phone", ...getTypeByORM("text"), default: getDefaultByORM("string") }, + { name: "address", ...getTypeByORM("text"), default: getDefaultByORM("string") }, + { name: "note", ...getTypeByORM("text"), default: getDefaultByORM("string") }, ], foreignKeys: [ new TableForeignKey({