diff --git a/src/backend/database/repositories/field-encryption-boundary.ts b/src/backend/database/repositories/field-encryption-boundary.ts deleted file mode 100644 index 04cdf02d..00000000 --- a/src/backend/database/repositories/field-encryption-boundary.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { FieldCrypto } from "../../utils/field-crypto.js"; -import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js"; - -const FIELD_ENCRYPTION_POLICY = { - users: { - sensitive: new Set([ - "passwordHash", - "clientSecret", - "totpSecret", - "totpBackupCodes", - "oidcIdentifier", - ]), - plaintext: new Set(["id", "username", "isAdmin", "isOidc"]), - }, - ssh_data: { - sensitive: new Set([ - "password", - "key", - "keyPassword", - "sudoPassword", - "autostartPassword", - "autostartKey", - "autostartKeyPassword", - "socks5Password", - "rdpPassword", - "vncPassword", - "telnetPassword", - ]), - plaintext: new Set([ - "id", - "userId", - "connectionType", - "name", - "ip", - "port", - "username", - "folder", - "tags", - "authType", - "credentialId", - ]), - }, - ssh_credentials: { - sensitive: new Set([ - "password", - "key", - "privateKey", - "publicKey", - "keyPassword", - ]), - plaintext: new Set([ - "id", - "userId", - "name", - "description", - "folder", - "tags", - "authType", - "username", - "keyType", - "detectedKeyType", - "usageCount", - "lastUsed", - ]), - }, - opkssh_tokens: { - sensitive: new Set(["sshCert", "privateKey"]), - plaintext: new Set(["id", "userId", "hostId", "createdAt", "expiresAt"]), - }, - termix_identity_ca: { - sensitive: new Set(["privateKey"]), - plaintext: new Set(["id", "publicKey", "createdAt", "updatedAt"]), - }, - vault_tokens: { - sensitive: new Set(["sshCert", "privateKey"]), - plaintext: new Set(["id", "userId", "profileId", "expiresAt"]), - }, -} as const; - -type PolicyTable = keyof typeof FIELD_ENCRYPTION_POLICY; -export type FieldClassification = "sensitive" | "plaintext" | "unknown"; - -export class FieldEncryptionBoundary { - static classifyField( - tableName: string, - fieldName: string, - ): FieldClassification { - const policy = this.getPolicy(tableName); - if (!policy) return "unknown"; - if (policy.sensitive.has(fieldName)) return "sensitive"; - if (policy.plaintext.has(fieldName)) return "plaintext"; - return "unknown"; - } - - static getSensitiveFields(tableName: string): string[] { - const policy = this.getPolicy(tableName); - return policy ? [...policy.sensitive].sort() : []; - } - - static encryptRecord>( - tableName: string, - record: T, - userDataKey: Buffer, - recordId = record.id, - ): T { - const id = this.requireRecordId(recordId); - const encryptedRecord: Record = { ...record }; - - for (const fieldName of this.getSensitiveFields(tableName)) { - const value = encryptedRecord[fieldName]; - if (typeof value === "string" && value) { - encryptedRecord[fieldName] = FieldCrypto.encryptField( - value, - userDataKey, - id, - fieldName, - ); - } - } - - return encryptedRecord as T; - } - - static decryptRecord>( - tableName: string, - record: T, - userDataKey: Buffer, - recordId = record.id, - ): T { - const id = this.requireRecordId(recordId); - const decryptedRecord: Record = { ...record }; - - for (const fieldName of this.getSensitiveFields(tableName)) { - const value = decryptedRecord[fieldName]; - if (typeof value === "string" && value) { - decryptedRecord[fieldName] = LazyFieldEncryption.safeGetFieldValue( - value, - userDataKey, - id, - fieldName, - ); - } - } - - return decryptedRecord as T; - } - - private static getPolicy(tableName: string) { - return FIELD_ENCRYPTION_POLICY[tableName as PolicyTable]; - } - - private static requireRecordId(recordId: unknown): string { - if (recordId === null || recordId === undefined || recordId === "") { - throw new Error("Field encryption requires a stable record id."); - } - return String(recordId); - } -} diff --git a/src/backend/tests/database/repositories/field-encryption-boundary.test.ts b/src/backend/tests/database/repositories/field-encryption-boundary.test.ts deleted file mode 100644 index a7dd89d7..00000000 --- a/src/backend/tests/database/repositories/field-encryption-boundary.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import crypto from "crypto"; -import { describe, expect, it } from "vitest"; -import { FieldEncryptionBoundary } from "../../../database/repositories/field-encryption-boundary.js"; - -describe("FieldEncryptionBoundary", () => { - const userDataKey = crypto.randomBytes(32); - - it("encrypts sensitive host fields while leaving queryable metadata plaintext", () => { - const host = { - id: 42, - userId: "user-1", - name: "prod-db", - ip: "10.0.0.5", - username: "root", - password: "secret", - rdpPassword: "rdp-secret", - }; - - const encrypted = FieldEncryptionBoundary.encryptRecord( - "ssh_data", - host, - userDataKey, - ); - - expect(encrypted.password).not.toBe("secret"); - expect(encrypted.rdpPassword).not.toBe("rdp-secret"); - expect(encrypted.ip).toBe("10.0.0.5"); - expect(encrypted.name).toBe("prod-db"); - - const decrypted = FieldEncryptionBoundary.decryptRecord( - "ssh_data", - encrypted, - userDataKey, - ); - expect(decrypted).toMatchObject(host); - }); - - it("encrypts credential secret fields and keeps metadata plaintext", () => { - const credential = { - id: 7, - userId: "user-1", - name: "primary credential", - authType: "key", - key: "private-key-material", - keyPassword: "key-password", - }; - - const encrypted = FieldEncryptionBoundary.encryptRecord( - "ssh_credentials", - credential, - userDataKey, - ); - - expect(encrypted.key).not.toBe("private-key-material"); - expect(encrypted.keyPassword).not.toBe("key-password"); - expect(encrypted.name).toBe("primary credential"); - - expect( - FieldEncryptionBoundary.decryptRecord( - "ssh_credentials", - encrypted, - userDataKey, - ), - ).toMatchObject(credential); - }); - - it("keeps empty and non-string sensitive values unchanged", () => { - const encrypted = FieldEncryptionBoundary.encryptRecord( - "ssh_data", - { - id: 1, - password: "", - key: null, - }, - userDataKey, - ); - - expect(encrypted.password).toBe(""); - expect(encrypted.key).toBeNull(); - }); - - it("requires a stable record id instead of inventing a temporary encryption context", () => { - expect(() => - FieldEncryptionBoundary.encryptRecord( - "ssh_data", - { password: "secret" }, - userDataKey, - ), - ).toThrow(/stable record id/); - }); - - it("classifies sensitive, plaintext, and unknown fields", () => { - expect(FieldEncryptionBoundary.classifyField("ssh_data", "password")).toBe( - "sensitive", - ); - expect(FieldEncryptionBoundary.classifyField("ssh_data", "ip")).toBe( - "plaintext", - ); - expect(FieldEncryptionBoundary.classifyField("ssh_data", "newField")).toBe( - "unknown", - ); - }); -});