From 26a5d74b140fdbc2acc63f999f8636fca7b79440 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 16 Jul 2024 15:52:29 +0800 Subject: [PATCH] misc: modified encryption/decryption of external kms config --- .../20240716063111_add-org-kms-data-key.ts | 21 ++++++++ backend/src/db/schemas/organizations.ts | 5 +- .../external-kms/external-kms-service.ts | 38 +++++++++------ backend/src/services/kms/kms-service.ts | 48 ++++++++++++++++++- 4 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 backend/src/db/migrations/20240716063111_add-org-kms-data-key.ts diff --git a/backend/src/db/migrations/20240716063111_add-org-kms-data-key.ts b/backend/src/db/migrations/20240716063111_add-org-kms-data-key.ts new file mode 100644 index 000000000..13e6a1ef6 --- /dev/null +++ b/backend/src/db/migrations/20240716063111_add-org-kms-data-key.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasKmsDataKeyCol = await knex.schema.hasColumn(TableName.Organization, "kmsEncryptedDataKey"); + await knex.schema.alterTable(TableName.Organization, (tb) => { + if (!hasKmsDataKeyCol) { + tb.binary("kmsEncryptedDataKey"); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasKmsDataKeyCol = await knex.schema.hasColumn(TableName.Organization, "kmsEncryptedDataKey"); + await knex.schema.alterTable(TableName.Organization, (t) => { + if (hasKmsDataKeyCol) { + t.dropColumn("kmsEncryptedDataKey"); + } + }); +} diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index 7b7a004fc..aa63423c9 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const OrganizationsSchema = z.object({ @@ -16,7 +18,8 @@ export const OrganizationsSchema = z.object({ updatedAt: z.date(), authEnforced: z.boolean().default(false).nullable().optional(), scimEnabled: z.boolean().default(false).nullable().optional(), - kmsDefaultKeyId: z.string().uuid().nullable().optional() + kmsDefaultKeyId: z.string().uuid().nullable().optional(), + kmsEncryptedDataKey: zodBuffer.nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index a2d314844..36e995b9e 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -22,7 +22,10 @@ import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model"; type TExternalKmsServiceFactoryDep = { externalKmsDAL: TExternalKmsDALFactory; - kmsService: Pick; + kmsService: Pick< + TKmsServiceFactory, + "getOrgKmsKeyId" | "decryptWithInputKey" | "encryptWithInputKey" | "getOrgKmsDataKey" + >; kmsDAL: Pick; permissionService: Pick; }; @@ -69,10 +72,11 @@ export const externalKmsServiceFactory = ({ throw new BadRequestError({ message: "external kms provided is invalid" }); } - const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId); - const kmsEncryptor = await kmsService.encryptWithKmsKey({ - kmsId: orgKmsKeyId + const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId); + const kmsEncryptor = await kmsService.encryptWithInputKey({ + key: orgKmsDataKey }); + const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({ plainText: Buffer.from(sanitizedProviderInput, "utf8") }); @@ -125,12 +129,13 @@ export const externalKmsServiceFactory = ({ const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); - const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); let sanitizedProviderInput = ""; if (provider) { - const kmsDecryptor = await kmsService.decryptWithKmsKey({ - kmsId: orgDefaultKmsId + const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId); + const kmsDecryptor = await kmsService.decryptWithInputKey({ + key: orgKmsDataKey }); + const decryptedProviderInputBlob = kmsDecryptor({ cipherTextBlob: externalKmsDoc.encryptedProviderInputs }); @@ -154,8 +159,9 @@ export const externalKmsServiceFactory = ({ let encryptedProviderInputs: Buffer | undefined; if (sanitizedProviderInput) { - const kmsEncryptor = await kmsService.encryptWithKmsKey({ - kmsId: orgDefaultKmsId + const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId); + const kmsEncryptor = await kmsService.encryptWithInputKey({ + key: orgKmsDataKey }); const { cipherTextBlob } = kmsEncryptor({ plainText: Buffer.from(sanitizedProviderInput, "utf8") @@ -239,10 +245,11 @@ export const externalKmsServiceFactory = ({ const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); - const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); - const kmsDecryptor = await kmsService.decryptWithKmsKey({ - kmsId: orgDefaultKmsId + const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId); + const kmsDecryptor = await kmsService.decryptWithInputKey({ + key: orgKmsDataKey }); + const decryptedProviderInputBlob = kmsDecryptor({ cipherTextBlob: externalKmsDoc.encryptedProviderInputs }); @@ -278,10 +285,11 @@ export const externalKmsServiceFactory = ({ const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); - const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); - const kmsDecryptor = await kmsService.decryptWithKmsKey({ - kmsId: orgDefaultKmsId + const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId); + const kmsDecryptor = await kmsService.decryptWithInputKey({ + key: orgKmsDataKey }); + const decryptedProviderInputBlob = kmsDecryptor({ cipherTextBlob: externalKmsDoc.encryptedProviderInputs }); diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index bc22f04ab..d8597af77 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -1,3 +1,5 @@ +import crypto from "node:crypto"; + import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; @@ -166,6 +168,50 @@ export const kmsServiceFactory = ({ return keyId; }; + const getOrgKmsDataKey = async (orgId: string) => { + const kmsKeyId = await getOrgKmsKeyId(orgId); + const orgKmsDataKey = await orgDAL.transaction(async (tx) => { + const org = await orgDAL.findById(orgId, tx); + + if (!org) { + throw new BadRequestError({ message: "Org not found" }); + } + + let encryptedDataKey = org.kmsEncryptedDataKey; + if (!encryptedDataKey) { + const dataKey = crypto.randomBytes(32); + const kmsEncryptor = await encryptWithKmsKey({ + kmsId: kmsKeyId + }); + + const { cipherTextBlob } = kmsEncryptor({ + plainText: dataKey + }); + + encryptedDataKey = cipherTextBlob; + await orgDAL.updateById( + org.id, + { + kmsEncryptedDataKey: encryptedDataKey + }, + tx + ); + + return dataKey; + } + + const kmsDecryptor = await decryptWithKmsKey({ + kmsId: kmsKeyId + }); + + return kmsDecryptor({ + cipherTextBlob: encryptedDataKey + }); + }); + + return orgKmsDataKey; + }; + const getProjectSecretManagerKmsKeyId = async (projectId: string) => { let project = await projectDAL.findById(projectId); if (!project) { @@ -337,6 +383,6 @@ export const kmsServiceFactory = ({ decryptWithInputKey, getOrgKmsKeyId, getProjectSecretManagerKmsKeyId, - getProjectSecretManagerKmsDataKey + getOrgKmsDataKey }; };