KI-AGENT: Verschlüssele Bankverbindungen beim Import neu

This commit is contained in:
2026-05-19 08:19:25 +02:00
parent 817d0e814b
commit 716de8a503
3 changed files with 73 additions and 6 deletions

View File

@@ -324,16 +324,28 @@ function maskIban(iban: string) {
}
function decryptEntityBankAccount(row: Record<string, any>) {
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(),
}
}

View File

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

View File

@@ -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<string, Record<string, any>[]>
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<string, any>[])
tables[table] = existingRows
}
const decryptEntityBankAccountsForExport = (rows: Record<string, any>[] = []) => {
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",