From 2cbae96c9aa38cfc60dfe553ec45fa7fa9673ff7 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 17 Jul 2024 23:19:32 +0800 Subject: [PATCH] feat: added project data key --- .../20240717114407_add-project-data-key.ts | 29 ++++ backend/src/db/schemas/projects.ts | 5 +- backend/src/keystore/keystore.ts | 6 +- backend/src/services/kms/kms-service.ts | 132 +++++++++++++++--- 4 files changed, 147 insertions(+), 25 deletions(-) create mode 100644 backend/src/db/migrations/20240717114407_add-project-data-key.ts diff --git a/backend/src/db/migrations/20240717114407_add-project-data-key.ts b/backend/src/db/migrations/20240717114407_add-project-data-key.ts new file mode 100644 index 000000000..e8ef083a3 --- /dev/null +++ b/backend/src/db/migrations/20240717114407_add-project-data-key.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasKmsSecretManagerEncryptedDataKey = await knex.schema.hasColumn( + TableName.Project, + "kmsSecretManagerEncryptedDataKey" + ); + + await knex.schema.alterTable(TableName.Project, (tb) => { + if (!hasKmsSecretManagerEncryptedDataKey) { + tb.binary("kmsSecretManagerEncryptedDataKey"); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasKmsSecretManagerEncryptedDataKey = await knex.schema.hasColumn( + TableName.Project, + "kmsSecretManagerEncryptedDataKey" + ); + + await knex.schema.alterTable(TableName.Project, (t) => { + if (hasKmsSecretManagerEncryptedDataKey) { + t.dropColumn("kmsSecretManagerEncryptedDataKey"); + } + }); +} diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 19597c9df..deba51b9a 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const ProjectsSchema = z.object({ @@ -20,7 +22,8 @@ export const ProjectsSchema = z.object({ pitVersionLimit: z.number().default(10), kmsCertificateKeyId: z.string().uuid().nullable().optional(), auditLogsRetentionDays: z.number().nullable().optional(), - kmsSecretManagerKeyId: z.string().uuid().nullable().optional() + kmsSecretManagerKeyId: z.string().uuid().nullable().optional(), + kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index ce752a1e5..90f79bb36 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -6,7 +6,11 @@ export type TKeyStoreFactory = ReturnType; // all the key prefixes used must be set here to avoid conflict export enum KeyStorePrefixes { - SecretReplication = "secret-replication-import-lock" + SecretReplication = "secret-replication-import-lock", + KmsProjectDataKeyCreation = "kms-project-data-key-creation-lock", + KmsProjectKeyCreation = "kms-project-key-creation-lock", + WaitUntilReadyKmsProjectDataKeyCreation = "wait-until-ready-kms-project-data-key-creation-", + WaitUntilReadyKmsProjectKeyCreation = "wait-until-ready-kms-project-key-creation-" } type TWaitTillReady = { diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index dbc86b860..bc22f04ab 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -1,7 +1,7 @@ import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; -import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { randomSecureBytes } from "@app/lib/crypto"; import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; @@ -167,35 +167,120 @@ export const kmsServiceFactory = ({ }; const getProjectSecretManagerKmsKeyId = async (projectId: string) => { - const keyId = await projectDAL.transaction(async (tx) => { - const project = await projectDAL.findById(projectId, tx); - if (!project) { - throw new BadRequestError({ message: "Project not found" }); + let project = await projectDAL.findById(projectId); + if (!project) { + throw new BadRequestError({ message: "Project not found" }); + } + + if (!project.kmsSecretManagerKeyId) { + // create default kms key for certificate service + const lock = await keyStore + .acquireLock([KeyStorePrefixes.KmsProjectKeyCreation, projectId], 3000, { retryCount: 3 }) + .catch(() => null); + + try { + if (!lock) { + await keyStore.waitTillReady({ + key: `${KeyStorePrefixes.WaitUntilReadyKmsProjectKeyCreation}${projectId}`, + keyCheckCb: (val) => val === "true", + waitingCb: () => logger.info("KMS. Waiting for project key to be created") + }); + + project = await projectDAL.findById(projectId); + } else { + const kmsKeyId = await projectDAL.transaction(async (tx) => { + const key = await generateKmsKey({ + isReserved: true, + orgId: project.orgId, + tx + }); + + await projectDAL.updateById( + projectId, + { + kmsSecretManagerKeyId: key.id + }, + tx + ); + + return key.id; + }); + + await keyStore.setItemWithExpiry( + `${KeyStorePrefixes.WaitUntilReadyKmsProjectKeyCreation}${projectId}`, + 10, + "true" + ); + + return kmsKeyId; + } + } finally { + await lock?.release(); } + } - if (!project.kmsSecretManagerKeyId) { - // create default kms key for certificate service - const key = await generateKmsKey({ - isReserved: true, - orgId: project.orgId, - tx - }); + if (!project.kmsSecretManagerKeyId) { + throw new Error("Missing project KMS key ID"); + } - await projectDAL.updateById( - projectId, - { - kmsSecretManagerKeyId: key.id - }, - tx - ); + return project.kmsSecretManagerKeyId; + }; - return key.id; + const getProjectSecretManagerKmsDataKey = async (projectId: string) => { + const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId); + let project = await projectDAL.findById(projectId); + + if (!project.kmsSecretManagerEncryptedDataKey) { + const lock = await keyStore + .acquireLock([KeyStorePrefixes.KmsProjectDataKeyCreation, projectId], 3000, { retryCount: 3 }) + .catch(() => null); + + try { + if (!lock) { + await keyStore.waitTillReady({ + key: `${KeyStorePrefixes.WaitUntilReadyKmsProjectDataKeyCreation}${projectId}`, + keyCheckCb: (val) => val === "true", + waitingCb: () => logger.info("KMS. Waiting for project data key to be created") + }); + + project = await projectDAL.findById(projectId); + } else { + const dataKey = randomSecureBytes(); + const kmsEncryptor = await encryptWithKmsKey({ + kmsId: kmsKeyId + }); + + const { cipherTextBlob } = kmsEncryptor({ + plainText: dataKey + }); + + await projectDAL.updateById(projectId, { + kmsSecretManagerEncryptedDataKey: cipherTextBlob + }); + + await keyStore.setItemWithExpiry( + `${KeyStorePrefixes.WaitUntilReadyKmsProjectDataKeyCreation}${projectId}`, + 10, + "true" + ); + return dataKey; + } + } finally { + await lock?.release(); } + } - return project.kmsSecretManagerKeyId; + if (!project.kmsSecretManagerEncryptedDataKey) { + throw new Error("Missing project data key"); + } + + const kmsDecryptor = await decryptWithKmsKey({ + kmsId: kmsKeyId }); - return keyId; + return kmsDecryptor({ + cipherTextBlob: project.kmsSecretManagerEncryptedDataKey + }); }; const startService = async () => { @@ -251,6 +336,7 @@ export const kmsServiceFactory = ({ decryptWithKmsKey, decryptWithInputKey, getOrgKmsKeyId, - getProjectSecretManagerKmsKeyId + getProjectSecretManagerKmsKeyId, + getProjectSecretManagerKmsDataKey }; };