KI-AGENT: Verschlüssele Bankverbindungen beim Import neu
This commit is contained in:
@@ -324,16 +324,28 @@ function maskIban(iban: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function decryptEntityBankAccount(row: Record<string, any>) {
|
function decryptEntityBankAccount(row: Record<string, any>) {
|
||||||
const iban = row.ibanEncrypted ? decrypt(row.ibanEncrypted as any) : null
|
let iban = null
|
||||||
const bic = row.bicEncrypted ? decrypt(row.bicEncrypted as any) : null
|
let bic = null
|
||||||
const bankName = row.bankNameEncrypted ? decrypt(row.bankNameEncrypted as any) : 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 {
|
return {
|
||||||
...row,
|
...row,
|
||||||
iban,
|
iban,
|
||||||
bic,
|
bic,
|
||||||
bankName,
|
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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ import crypto from "crypto";
|
|||||||
import {secrets} from "./secrets"
|
import {secrets} from "./secrets"
|
||||||
const ALGORITHM = "aes-256-gcm";
|
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) {
|
export function encrypt(text) {
|
||||||
const ENCRYPTION_KEY = Buffer.from(secrets.ENCRYPTION_KEY, "hex");
|
const ENCRYPTION_KEY = getEncryptionKey();
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
|
const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
|
||||||
|
|
||||||
@@ -21,7 +28,7 @@ export function encrypt(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function decrypt({ iv, content, tag }) {
|
export function decrypt({ iv, content, tag }) {
|
||||||
const ENCRYPTION_KEY = Buffer.from(secrets.ENCRYPTION_KEY, "hex");
|
const ENCRYPTION_KEY = getEncryptionKey();
|
||||||
const decipher = crypto.createDecipheriv(
|
const decipher = crypto.createDecipheriv(
|
||||||
ALGORITHM,
|
ALGORITHM,
|
||||||
ENCRYPTION_KEY,
|
ENCRYPTION_KEY,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"
|
|||||||
import { pool } from "../../db"
|
import { pool } from "../../db"
|
||||||
import { s3 } from "./s3"
|
import { s3 } from "./s3"
|
||||||
import { secrets } from "./secrets"
|
import { secrets } from "./secrets"
|
||||||
|
import { decrypt, encrypt } from "./crypt"
|
||||||
|
|
||||||
type TableRows = Record<string, Record<string, any>[]>
|
type TableRows = Record<string, Record<string, any>[]>
|
||||||
type TableMetadata = {
|
type TableMetadata = {
|
||||||
@@ -38,6 +39,12 @@ type ImportOptions = {
|
|||||||
targetTenantId?: number | null
|
targetTenantId?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ENTITY_BANKACCOUNT_PLAIN_FIELDS = {
|
||||||
|
iban: "__plainIban",
|
||||||
|
bic: "__plainBic",
|
||||||
|
bankName: "__plainBankName",
|
||||||
|
}
|
||||||
|
|
||||||
const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"`
|
const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"`
|
||||||
|
|
||||||
const tableColumns = async (client: any) => {
|
const tableColumns = async (client: any) => {
|
||||||
@@ -98,6 +105,22 @@ const addRows = (tables: TableRows, table: string, rows: Record<string, any>[])
|
|||||||
tables[table] = existingRows
|
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 loadObjectAsBase64 = async (path: string) => {
|
||||||
const { Body } = await s3.send(new GetObjectCommand({
|
const { Body } = await s3.send(new GetObjectCommand({
|
||||||
Bucket: secrets.S3_BUCKET,
|
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]))
|
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 fileRows = tables.files || []
|
||||||
const 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) => {
|
const prepareColumnValue = (value: any, isJsonColumn: boolean) => {
|
||||||
if (!isJsonColumn || value === null || typeof value === "undefined") return value
|
if (!isJsonColumn || value === null || typeof value === "undefined") return value
|
||||||
if (typeof value === "string") return value
|
if (typeof value === "string") return value
|
||||||
@@ -384,6 +431,7 @@ export const importTenantFullExport = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const exportData = remapTenantScopedExport(rawExportData, options.targetTenantId)
|
const exportData = remapTenantScopedExport(rawExportData, options.targetTenantId)
|
||||||
|
encryptEntityBankAccountRowsForImport(exportData)
|
||||||
const client = await pool.connect()
|
const client = await pool.connect()
|
||||||
const importOrder = [
|
const importOrder = [
|
||||||
"tenants",
|
"tenants",
|
||||||
|
|||||||
Reference in New Issue
Block a user