From a807f0cf6c31685a000125cf74a83eca9ff117cb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 29 Oct 2024 22:08:17 +0400 Subject: [PATCH] feat: added option for choosing encryption method --- backend/src/db/schemas/kms-root-config.ts | 2 +- backend/src/lib/config/env.ts | 8 ++ backend/src/services/hsm/hsm-service.ts | 2 +- backend/src/services/kms/kms-service.ts | 146 ++++++++++++++-------- backend/src/services/kms/kms-types.ts | 5 + 5 files changed, 109 insertions(+), 54 deletions(-) diff --git a/backend/src/db/schemas/kms-root-config.ts b/backend/src/db/schemas/kms-root-config.ts index 978d108bd..d15e1dff8 100644 --- a/backend/src/db/schemas/kms-root-config.ts +++ b/backend/src/db/schemas/kms-root-config.ts @@ -12,7 +12,7 @@ import { TImmutableDBKeys } from "./models"; export const KmsRootConfigSchema = z.object({ id: z.string().uuid(), encryptedRootKey: zodBuffer, - isEncryptedByHsm: z.boolean().default(false), + encryptionStrategy: z.string(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index b289968bf..0734cc24b 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -1,6 +1,8 @@ import { Logger } from "pino"; import { z } from "zod"; +import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; + import { removeTrailingSlash } from "../fn"; import { zpStr } from "../zod"; @@ -165,6 +167,9 @@ const envSchema = z WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()), ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"), + // KMS ENCRYPTION + ROOT_KEY_ENCRYPTION_STRATEGY: z.nativeEnum(RootKeyEncryptionStrategy).default(RootKeyEncryptionStrategy.Basic), + // HSM HSM_LIB_PATH: zpStr( z @@ -198,6 +203,9 @@ const envSchema = z }) .transform((data) => ({ ...data, + + // ROOT_KEY_ENCRYPTION_STRATEGY: "HSM", + DB_READ_REPLICAS: data.DB_READ_REPLICAS ? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS)) : undefined, diff --git a/backend/src/services/hsm/hsm-service.ts b/backend/src/services/hsm/hsm-service.ts index c9c4fb4f6..4aa5154e7 100644 --- a/backend/src/services/hsm/hsm-service.ts +++ b/backend/src/services/hsm/hsm-service.ts @@ -152,7 +152,7 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe } }; - const isActive = async () => { + const isActive = () => { if (!module || !appCfg.isHsmConfigured || !sessionManager) { return false; } diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 2a18d9ee1..def6413bb 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -2,7 +2,7 @@ import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; import { z } from "zod"; -import { KmsKeysSchema } from "@app/db/schemas"; +import { KmsKeysSchema, TKmsRootConfig } from "@app/db/schemas"; import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms"; import { ExternalKmsAwsSchema, @@ -28,6 +28,7 @@ import { TKmsRootConfigDALFactory } from "./kms-root-config-dal"; import { KmsDataKey, KmsType, + RootKeyEncryptionStrategy, TDecryptWithKeyDTO, TDecryptWithKmsDTO, TEncryptionWithKeyDTO, @@ -613,6 +614,61 @@ export const kmsServiceFactory = ({ } }; + const $createBasicEncryptionKey = () => { + const appCfg = getConfig(); + + const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; + const isBase64 = !appCfg.ENCRYPTION_KEY; + if (!encryptionKey) + throw new Error( + "Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?" + ); + + const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); + + return encryptionKeyBuffer; + }; + + const $decryptRootKey = async (kmsRootConfig: TKmsRootConfig) => { + // case 1: root key is encrypted with HSM + if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Hsm) { + if (!hsmService.isActive()) { + throw new Error("Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); + } + + return hsmService.decrypt(kmsRootConfig.encryptedRootKey); + } + + // case 2: root key is encrypted with basic encryption + if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Basic) { + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const encryptionKeyBuffer = $createBasicEncryptionKey(); + + return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); + } + + throw new Error(`Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}`); + }; + + const $encryptRootKey = async (plainKeyBuffer: Buffer, strategy: RootKeyEncryptionStrategy) => { + if (strategy === RootKeyEncryptionStrategy.Hsm) { + if (!hsmService.isActive()) { + throw new Error("Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); + } + return hsmService.encrypt(plainKeyBuffer); + } + + if (strategy === RootKeyEncryptionStrategy.Basic) { + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const encryptionKeyBuffer = $createBasicEncryptionKey(); + + return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer); + } + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Invalid root key encryption strategy: ${strategy}`); + }; + // by keeping the decrypted data key in inner scope // none of the entities outside can interact directly or expose the data key // NOTICE: If changing here update migrations/utils/kms @@ -798,13 +854,7 @@ export const kmsServiceFactory = ({ // akhilmhdh: a copy of this is made in migrations/utils/kms const startService = async () => { const appCfg = getConfig(); - // This will switch to a seal process and HMS flow in future - const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; - // if root key its base64 encoded - const isBase64 = !appCfg.ENCRYPTION_KEY; - if (!encryptionKey) throw new Error("Root encryption key not found for KMS service."); - const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); - const hsmEnabled = await hsmService.isActive(); + const hsmEnabled = hsmService.isActive(); const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null); if (!lock) { @@ -817,44 +867,40 @@ export const kmsServiceFactory = ({ // check if KMS root key was already generated and saved in DB const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + + // case 1: a root key already exists in the DB if (kmsRootConfig) { if (lock) await lock.release(); logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting."); - let decryptedRootKey: Buffer | null = null; - try { - decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); - logger.info("KMS: Decrypted ROOT Key with platform key."); - } catch (err) { - // First we attempt to decrypt with regular root key. If it fails, we check if HSM is enabled, and if it is we attempt to decrypt with HSM. - if (hsmEnabled && kmsRootConfig.isEncryptedByHsm) { - decryptedRootKey = hsmService.decrypt(kmsRootConfig.encryptedRootKey); - logger.info("KMS: Decrypted ROOT Key with HSM."); - } else { - // If HSM is not enabled we assume it's a general error, and throw. - throw err; - } - } + const decryptedRootKey = await $decryptRootKey(kmsRootConfig).catch((err) => { + logger.error(err, `KMS: Failed to decrypt ROOT Key [strategy=${kmsRootConfig.encryptionStrategy}]`); + throw err; + }); - if (!decryptedRootKey) { - logger.error( - { hsmEnabled, isEncryptedByHsm: kmsRootConfig.isEncryptedByHsm }, - "KMS: Failed to decrypt ROOT Key" + const selectedEncryptionStrategy = appCfg.ROOT_KEY_ENCRYPTION_STRATEGY; + + // case: the users selected encryption key strategy does not match the one in the DB + // in this case we need to re-encrypt the key with the selected strategy, and update the strategy and key in the DB + if (selectedEncryptionStrategy !== kmsRootConfig.encryptionStrategy) { + logger.info( + { + newStrategy: selectedEncryptionStrategy, + configuredStrategy: kmsRootConfig.encryptionStrategy + }, + "KMS: Change in root encryption key strategy detected. Re-encrypting ROOT Key with selected strategy" ); - throw new Error("Failed to decrypt ROOT Key"); - } - - // If the key is not encrypted with HSM, we re-encrypt it with HSM and update the key in the DB. - if (!kmsRootConfig.isEncryptedByHsm && hsmEnabled) { - const encryptedRootKey = hsmService.encrypt(decryptedRootKey); + const encryptedRootKey = await $encryptRootKey(decryptedRootKey, selectedEncryptionStrategy); if (!encryptedRootKey) { - logger.error("KMS: Failed to encrypt ROOT Key with HSM"); - throw new Error("Failed to encrypt ROOT Key with HSM"); + logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy"); + throw new Error("Failed to re-encrypt ROOT Key with selected strategy"); } - await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { encryptedRootKey, isEncryptedByHsm: true }); + await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { + encryptedRootKey, + encryptionStrategy: selectedEncryptionStrategy + }); } // set the flag so that other instance nodes can start @@ -865,26 +911,22 @@ export const kmsServiceFactory = ({ } logger.info("KMS: Generating ROOT Key"); + const selectedEncryptionStrategy = appCfg.ROOT_KEY_ENCRYPTION_STRATEGY; + const newRootKey = randomSecureBytes(32); - - let encryptedRootKey: Buffer | null = null; - let isEncryptedByHsm = false; - - if (hsmEnabled) { - encryptedRootKey = hsmService.encrypt(newRootKey); - isEncryptedByHsm = true; - } else { - encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer); - } - - if (!encryptedRootKey) { + const encryptedRootKey = await $encryptRootKey(newRootKey, selectedEncryptionStrategy).catch((err) => { logger.error({ hsmEnabled }, "KMS: Failed to encrypt ROOT Key"); - } + throw err; + }); - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID, isEncryptedByHsm }); + await kmsRootConfigDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: KMS_ROOT_CONFIG_UUID, + encryptedRootKey, + encryptionStrategy: selectedEncryptionStrategy + }); - // set the flag so that other instancen nodes can start + // set the flag so that other instance nodes can start await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true"); logger.info("KMS: Saved and loaded ROOT Key into memory"); if (lock) await lock.release(); diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index 5d5b77a09..e1aa7b5e0 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -56,3 +56,8 @@ export type TUpdateProjectSecretManagerKmsKeyDTO = { projectId: string; kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string }; }; + +export enum RootKeyEncryptionStrategy { + Basic = "BASIC", + Hsm = "HSM" +}