diff --git a/backend/src/routes/resources/main.ts b/backend/src/routes/resources/main.ts index 3f32a74..2b1fcda 100644 --- a/backend/src/routes/resources/main.ts +++ b/backend/src/routes/resources/main.ts @@ -324,16 +324,28 @@ function maskIban(iban: string) { } function decryptEntityBankAccount(row: Record) { - const iban = row.ibanEncrypted ? decrypt(row.ibanEncrypted as any) : null - const bic = row.bicEncrypted ? decrypt(row.bicEncrypted as any) : null - const bankName = row.bankNameEncrypted ? decrypt(row.bankNameEncrypted as any) : null + let iban = null + let bic = null + let bankName = null + let decryptError = null + + try { + iban = row.ibanEncrypted ? decrypt(row.ibanEncrypted as any) : null + bic = row.bicEncrypted ? decrypt(row.bicEncrypted as any) : null + bankName = row.bankNameEncrypted ? decrypt(row.bankNameEncrypted as any) : null + } catch (err: any) { + decryptError = err?.message || "Bankverbindung konnte nicht entschlüsselt werden." + } return { ...row, iban, bic, bankName, - displayLabel: `${maskIban(iban || "")}${bankName ? ` | ${bankName}` : ""}${row.description ? ` (${row.description})` : ""}`.trim(), + decryptError, + displayLabel: decryptError + ? `Bankverbindung nicht lesbar${row.description ? ` (${row.description})` : ""}` + : `${maskIban(iban || "")}${bankName ? ` | ${bankName}` : ""}${row.description ? ` (${row.description})` : ""}`.trim(), } } diff --git a/backend/src/utils/crypt.ts b/backend/src/utils/crypt.ts index 1ed7ddd..6ae4ee6 100644 --- a/backend/src/utils/crypt.ts +++ b/backend/src/utils/crypt.ts @@ -2,11 +2,18 @@ import crypto from "crypto"; import {secrets} from "./secrets" const ALGORITHM = "aes-256-gcm"; +function getEncryptionKey() { + const key = secrets.ENCRYPTION_KEY || "" + if (!/^[a-f0-9]{64}$/i.test(key)) { + throw new Error("ENCRYPTION_KEY muss ein 64 Zeichen langer Hex-String sein. Beispiel: openssl rand -hex 32") + } + return Buffer.from(key, "hex") +} export function encrypt(text) { - const ENCRYPTION_KEY = Buffer.from(secrets.ENCRYPTION_KEY, "hex"); + const ENCRYPTION_KEY = getEncryptionKey(); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv); @@ -21,7 +28,7 @@ export function encrypt(text) { } export function decrypt({ iv, content, tag }) { - const ENCRYPTION_KEY = Buffer.from(secrets.ENCRYPTION_KEY, "hex"); + const ENCRYPTION_KEY = getEncryptionKey(); const decipher = crypto.createDecipheriv( ALGORITHM, ENCRYPTION_KEY, diff --git a/backend/src/utils/tenantFullExport.ts b/backend/src/utils/tenantFullExport.ts index 36d5d85..3bba240 100644 --- a/backend/src/utils/tenantFullExport.ts +++ b/backend/src/utils/tenantFullExport.ts @@ -4,6 +4,7 @@ import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3" import { pool } from "../../db" import { s3 } from "./s3" import { secrets } from "./secrets" +import { decrypt, encrypt } from "./crypt" type TableRows = Record[]> type TableMetadata = { @@ -38,6 +39,12 @@ type ImportOptions = { targetTenantId?: number | null } +const ENTITY_BANKACCOUNT_PLAIN_FIELDS = { + iban: "__plainIban", + bic: "__plainBic", + bankName: "__plainBankName", +} + const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"` const tableColumns = async (client: any) => { @@ -98,6 +105,22 @@ const addRows = (tables: TableRows, table: string, rows: Record[]) tables[table] = existingRows } +const decryptEntityBankAccountsForExport = (rows: Record[] = []) => { + return rows.map((row) => { + const nextRow = { ...row } + + try { + nextRow[ENTITY_BANKACCOUNT_PLAIN_FIELDS.iban] = row.iban_encrypted ? decrypt(row.iban_encrypted) : null + nextRow[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bic] = row.bic_encrypted ? decrypt(row.bic_encrypted) : null + nextRow[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bankName] = row.bank_name_encrypted ? decrypt(row.bank_name_encrypted) : null + } catch (err: any) { + throw new Error(`Bankverbindung ${row.id || ""} konnte für den Export nicht entschlüsselt werden: ${err?.message || err}`) + } + + return nextRow + }) +} + const loadObjectAsBase64 = async (path: string) => { const { Body } = await s3.send(new GetObjectCommand({ Bucket: secrets.S3_BUCKET, @@ -165,6 +188,10 @@ export const buildTenantFullExport = async (server: FastifyInstance, tenantId: n addRows(tables, "auth_profile_teams", await loadRows(client, "auth_profile_teams", "profile_id = any($1::uuid[])", [profileIds])) } + if (tables.entitybankaccounts?.length) { + tables.entitybankaccounts = decryptEntityBankAccountsForExport(tables.entitybankaccounts) + } + const fileRows = tables.files || [] const files = [] @@ -266,6 +293,26 @@ const remapTenantScopedExport = ( } } +const encryptEntityBankAccountRowsForImport = (exportData: TenantFullExport) => { + const rows = exportData.tables.entitybankaccounts || [] + + for (const row of rows) { + const plainIban = row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.iban] + const plainBic = row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bic] + const plainBankName = row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bankName] + + if (typeof plainIban === "string" && typeof plainBic === "string" && typeof plainBankName === "string") { + row.iban_encrypted = encrypt(plainIban) + row.bic_encrypted = encrypt(plainBic) + row.bank_name_encrypted = encrypt(plainBankName) + } + + delete row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.iban] + delete row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bic] + delete row[ENTITY_BANKACCOUNT_PLAIN_FIELDS.bankName] + } +} + const prepareColumnValue = (value: any, isJsonColumn: boolean) => { if (!isJsonColumn || value === null || typeof value === "undefined") return value if (typeof value === "string") return value @@ -384,6 +431,7 @@ export const importTenantFullExport = async ( } const exportData = remapTenantScopedExport(rawExportData, options.targetTenantId) + encryptEntityBankAccountRowsForImport(exportData) const client = await pool.connect() const importOrder = [ "tenants",