From e95d7e55c106d6b278987d9a29e2473ab3132fa7 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 1 Aug 2024 13:39:41 +0530 Subject: [PATCH] feat: removed all the migration done for webhook and dynamic secret towards kms encryption --- .../db/migrations/20240730181850_secret-v2.ts | 225 +----------------- backend/src/db/schemas/dynamic-secrets.ts | 10 +- backend/src/db/schemas/webhooks.ts | 15 +- .../dynamic-secret-lease-dal.ts | 21 +- .../dynamic-secret-lease-queue.ts | 53 ++--- .../dynamic-secret-lease-service.ts | 50 ++-- .../dynamic-secret/dynamic-secret-service.ts | 67 +++--- backend/src/server/routes/index.ts | 13 +- backend/src/server/routes/sanitizedSchemas.ts | 6 +- backend/src/services/secret/secret-queue.ts | 2 +- backend/src/services/webhook/webhook-fns.ts | 64 +++-- .../src/services/webhook/webhook-service.ts | 70 +++--- 12 files changed, 187 insertions(+), 409 deletions(-) diff --git a/backend/src/db/migrations/20240730181850_secret-v2.ts b/backend/src/db/migrations/20240730181850_secret-v2.ts index a9188c608..d44c67cf1 100644 --- a/backend/src/db/migrations/20240730181850_secret-v2.ts +++ b/backend/src/db/migrations/20240730181850_secret-v2.ts @@ -1,178 +1,8 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { Knex } from "knex"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; -import { selectAllTableCols } from "@app/lib/knex/select"; - -import { SecretKeyEncoding, SecretType, TableName } from "../schemas"; +import { SecretType, TableName } from "../schemas"; import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; -import { getSecretManagerDataKey } from "./utils/kms"; - -const backfillWebhooks = async (knex: Knex) => { - const hasEncryptedSecretKeyWithKms = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms"); - const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl"); - const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText"); - const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV"); - const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag"); - const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey"); - const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv"); - const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag"); - const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding"); - const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm"); - const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url"); - - await knex.schema.alterTable(TableName.Webhook, (t) => { - if (!hasEncryptedSecretKeyWithKms) t.binary("encryptedSecretKeyWithKms"); - if (!hasEncryptedWebhookUrl) t.binary("encryptedUrl"); - if (hasUrl) t.string("url").nullable().alter(); - }); - - const kmsEncryptorGroupByProjectId: Record>["encryptor"]> = - {}; - if (hasUrlCipherText && hasUrlIV && hasUrlTag && hasEncryptedSecretKey && hasIV && hasTag) { - // eslint-disable-next-line - const webhooksToFill = await knex(TableName.Webhook) - .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`) - .whereNull("encryptedUrl") - // eslint-disable-next-line - // @ts-ignore knex migration fails - .select(selectAllTableCols(TableName.Webhook)) - .select("projectId"); - - const updatedWebhooks = []; - for (const webhook of webhooksToFill) { - if (!kmsEncryptorGroupByProjectId[webhook.projectId]) { - // eslint-disable-next-line - const { encryptor } = await getSecretManagerDataKey(knex, webhook.projectId); - kmsEncryptorGroupByProjectId[webhook.projectId] = encryptor; - } - - const kmsEncryptor = kmsEncryptorGroupByProjectId[webhook.projectId]; - - // @ts-ignore post migration fails - let webhookUrl = webhook.url; - let webhookSecretKey; - - // @ts-ignore post migration fails - if (webhook.urlTag && webhook.urlCipherText && webhook.urlIV) { - webhookUrl = infisicalSymmetricDecrypt({ - // @ts-ignore post migration fails - keyEncoding: webhook.keyEncoding as SecretKeyEncoding, - // @ts-ignore post migration fails - ciphertext: webhook.urlCipherText, - // @ts-ignore post migration fails - iv: webhook.urlIV, - // @ts-ignore post migration fails - tag: webhook.urlTag - }); - } - // @ts-ignore post migration fails - if (webhook.encryptedSecretKey && webhook.iv && webhook.tag) { - webhookSecretKey = infisicalSymmetricDecrypt({ - // @ts-ignore post migration fails - keyEncoding: webhook.keyEncoding as SecretKeyEncoding, - // @ts-ignore post migration fails - ciphertext: webhook.encryptedSecretKey, - // @ts-ignore post migration fails - iv: webhook.iv, - // @ts-ignore post migration fails - tag: webhook.tag - }); - } - const { projectId, ...el } = webhook; - updatedWebhooks.push({ - ...el, - encryptedSecretKeyWithKms: webhookSecretKey - ? kmsEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob - : null, - encryptedUrl: kmsEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob - }); - } - if (updatedWebhooks.length) { - // eslint-disable-next-line - await knex(TableName.Webhook).insert(updatedWebhooks).onConflict("id").merge(); - } - } - await knex.schema.alterTable(TableName.Webhook, (t) => { - t.binary("encryptedUrl").notNullable().alter(); - - if (hasUrlIV) t.dropColumn("urlIV"); - if (hasUrlCipherText) t.dropColumn("urlCipherText"); - if (hasUrlTag) t.dropColumn("urlTag"); - if (hasIV) t.dropColumn("iv"); - if (hasTag) t.dropColumn("tag"); - if (hasEncryptedSecretKey) t.dropColumn("encryptedSecretKey"); - if (hasKeyEncoding) t.dropColumn("keyEncoding"); - if (hasAlgorithm) t.dropColumn("algorithm"); - if (hasUrl) t.dropColumn("url"); - }); -}; - -const backfillDynamicSecretConfigs = async (knex: Knex) => { - const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig"); - - const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext"); - const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV"); - const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag"); - const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding"); - const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm"); - - await knex.schema.alterTable(TableName.DynamicSecret, (t) => { - if (!hasEncryptedConfig) t.binary("encryptedConfig"); - }); - const kmsEncryptorGroupByProjectId: Record>["encryptor"]> = - {}; - if (hasInputCipherText && hasInputIV && hasInputTag) { - // eslint-disable-next-line - const dynamicSecretConfigs = await knex(TableName.DynamicSecret) - .join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`) - .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) - .whereNull("encryptedConfig") - // @ts-ignore post migration fails - .select(selectAllTableCols(TableName.DynamicSecret)) - .select("projectId"); - - const updatedConfigs = []; - for (const dynamicSecretConfig of dynamicSecretConfigs) { - if (!kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId]) { - // eslint-disable-next-line - const { encryptor } = await getSecretManagerDataKey(knex, dynamicSecretConfig.projectId); - kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId] = encryptor; - } - - const kmsEncryptor = kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId]; - const inputConfig = infisicalSymmetricDecrypt({ - // @ts-ignore post migration fails - keyEncoding: dynamicSecretConfig.keyEncoding as SecretKeyEncoding, - // @ts-ignore post migration fails - ciphertext: dynamicSecretConfig.inputCiphertext as string, - // @ts-ignore post migration fails - iv: dynamicSecretConfig.inputIV as string, - // @ts-ignore post migration fails - tag: dynamicSecretConfig.inputTag as string - }); - - const { projectId, ...el } = dynamicSecretConfig; - updatedConfigs.push({ - ...el, - encryptedConfig: kmsEncryptor({ plainText: Buffer.from(inputConfig) }).cipherTextBlob - }); - } - if (updatedConfigs.length) { - // eslint-disable-next-line - await knex(TableName.DynamicSecret).insert(updatedConfigs).onConflict("id").merge(); - } - } - await knex.schema.alterTable(TableName.DynamicSecret, (t) => { - t.binary("encryptedConfig").notNullable().alter(); - - if (hasInputTag) t.dropColumn("inputTag"); - if (hasInputIV) t.dropColumn("inputIV"); - if (hasInputCipherText) t.dropColumn("inputCiphertext"); - if (hasKeyEncoding) t.dropColumn("keyEncoding"); - if (hasAlgorithm) t.dropColumn("algorithm"); - }); -}; export async function up(knex: Knex): Promise { const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2); @@ -314,14 +144,6 @@ export async function up(knex: Knex): Promise { t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE"); }); } - - if (await knex.schema.hasTable(TableName.Webhook)) { - await backfillWebhooks(knex); - } - - if (await knex.schema.hasTable(TableName.DynamicSecret)) { - await backfillDynamicSecretConfigs(knex); - } } export async function down(knex: Knex): Promise { @@ -356,49 +178,4 @@ export async function down(knex: Knex): Promise { if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn"); }); } - if (await knex.schema.hasTable(TableName.Webhook)) { - const hasEncryptedWebhookSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms"); - const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl"); - const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText"); - const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV"); - const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag"); - const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey"); - const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv"); - const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag"); - const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding"); - const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm"); - const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url"); - - await knex.schema.alterTable(TableName.Webhook, (t) => { - if (hasEncryptedWebhookSecretKey) t.dropColumn("encryptedSecretKeyWithKms"); - if (hasEncryptedWebhookUrl) t.dropColumn("encryptedUrl"); - if (!hasUrl) t.string("url"); - if (!hasEncryptedSecretKey) t.string("encryptedSecretKey"); - if (!hasIV) t.string("iv"); - if (!hasTag) t.string("tag"); - if (!hasAlgorithm) t.string("algorithm"); - if (!hasKeyEncoding) t.string("keyEncoding"); - if (!hasUrlCipherText) t.string("urlCipherText"); - if (!hasUrlIV) t.string("urlIV"); - if (!hasUrlTag) t.string("urlTag"); - }); - } - - if (await knex.schema.hasTable(TableName.DynamicSecret)) { - const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig"); - - const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV"); - const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext"); - const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag"); - const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm"); - const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding"); - await knex.schema.alterTable(TableName.DynamicSecret, (t) => { - if (hasEncryptedConfig) t.dropColumn("encryptedConfig"); - if (!hasInputIV) t.string("inputIV"); - if (!hasInputCipherText) t.text("inputCiphertext"); - if (!hasInputTag) t.string("inputTag"); - if (!hasAlgorithm) t.string("algorithm"); - if (!hasKeyEncoding) t.string("keyEncoding"); - }); - } } diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index d90f1f7d2..b27da396c 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const DynamicSecretsSchema = z.object({ @@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({ type: z.string(), defaultTTL: z.string(), maxTTL: z.string().nullable().optional(), + inputIV: z.string(), + inputCiphertext: z.string(), + inputTag: z.string(), + algorithm: z.string().default("aes-256-gcm"), + keyEncoding: z.string().default("utf8"), folderId: z.string().uuid(), status: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date(), - encryptedConfig: zodBuffer + updatedAt: z.date() }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index 3f670497f..a7aac2933 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -5,22 +5,27 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const WebhooksSchema = z.object({ id: z.string().uuid(), secretPath: z.string().default("/"), + url: z.string(), lastStatus: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(), isDisabled: z.boolean().default(false), + encryptedSecretKey: z.string().nullable().optional(), + iv: z.string().nullable().optional(), + tag: z.string().nullable().optional(), + algorithm: z.string().nullable().optional(), + keyEncoding: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), envId: z.string().uuid(), - type: z.string().default("general").nullable().optional(), - encryptedSecretKeyWithKms: zodBuffer.nullable().optional(), - encryptedUrl: zodBuffer + urlCipherText: z.string().nullable().optional(), + urlIV: z.string().nullable().optional(), + urlTag: z.string().nullable().optional(), + type: z.string().default("general").nullable().optional() }); export type TWebhooks = z.infer; diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts index 97a682933..810628030 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts @@ -12,10 +12,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => { try { - const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) - .count("*") - .where({ dynamicSecretId }) - .first(); + const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first(); return parseInt(doc || "0", 10); } catch (error) { throw new DatabaseError({ error, name: "DynamicSecretCountLeases" }); @@ -24,7 +21,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) + const doc = await (tx || db)(TableName.DynamicSecretLease) .where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id }) .first() .join( @@ -40,10 +37,14 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"), db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"), db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"), + db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"), + db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"), + db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"), + db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"), + db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"), db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"), db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"), db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"), - db.ref("encryptedConfig").withSchema(TableName.DynamicSecret).as("dynEncryptedConfig"), db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"), db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt") ); @@ -58,12 +59,16 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { type: doc.dynType, defaultTTL: doc.dynDefaultTTL, maxTTL: doc.dynMaxTTL, + inputIV: doc.dynInputIV, + inputTag: doc.dynInputTag, + inputCiphertext: doc.dynInputCiphertext, + algorithm: doc.dynAlgorithm, + keyEncoding: doc.dynKeyEncoding, folderId: doc.dynFolderId, status: doc.dynStatus, statusDetails: doc.dynStatusDetails, createdAt: doc.dynCreatedAt, - updatedAt: doc.dynUpdatedAt, - encryptedConfig: doc.dynEncryptedConfig + updatedAt: doc.dynUpdatedAt } }; } catch (error) { diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index 82248bd8c..9bdb1c24e 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -1,9 +1,8 @@ +import { SecretKeyEncoding } from "@app/db/schemas"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; -import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; -import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; @@ -15,8 +14,6 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = { dynamicSecretLeaseDAL: Pick; dynamicSecretDAL: Pick; dynamicSecretProviders: Record; - kmsService: Pick; - folderDAL: Pick; }; export type TDynamicSecretLeaseQueueServiceFactory = ReturnType; @@ -25,9 +22,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ queueService, dynamicSecretDAL, dynamicSecretProviders, - dynamicSecretLeaseDAL, - kmsService, - folderDAL + dynamicSecretLeaseDAL }: TDynamicSecretLeaseQueueServiceFactoryDep) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { await queueService.queue( @@ -82,20 +77,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; - const folder = await folderDAL.findById(dynamicSecretCfg.folderId); - if (!folder) throw new DisableRotationErrors({ message: "Folder not found" }); - const { projectId } = folder; - - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - - const dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id); @@ -110,22 +100,17 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting) throw new DisableRotationErrors({ message: "Document not deleted" }); - const folder = await folderDAL.findById(dynamicSecretCfg.folderId); - if (!folder) throw new DisableRotationErrors({ message: "Folder not found" }); - const { projectId } = folder; - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId }); if (dynamicSecretLeases.length) { const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - - const dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); - const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); await Promise.all( diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index eb0c6f171..1e5487d22 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -1,14 +1,14 @@ import { ForbiddenError, subject } from "@casl/ability"; import ms from "ms"; +import { SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; -import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -34,7 +34,6 @@ type TDynamicSecretLeaseServiceFactoryDep = { folderDAL: Pick; permissionService: Pick; projectDAL: Pick; - kmsService: Pick; }; export type TDynamicSecretLeaseServiceFactory = ReturnType; @@ -47,8 +46,7 @@ export const dynamicSecretLeaseServiceFactory = ({ permissionService, dynamicSecretQueueService, projectDAL, - licenseService, - kmsService + licenseService }: TDynamicSecretLeaseServiceFactoryDep) => { const create = async ({ environmentSlug, @@ -96,12 +94,14 @@ export const dynamicSecretLeaseServiceFactory = ({ throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); - const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const { maxTTL } = dynamicSecretCfg; @@ -164,12 +164,14 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); - const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const { maxTTL } = dynamicSecretCfg; @@ -229,12 +231,14 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); - const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; const revokeResponse = await selectedProvider .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId) diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 508c263ee..1aef3cc86 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,11 +1,11 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; -import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -34,7 +34,6 @@ type TDynamicSecretServiceFactoryDep = { folderDAL: Pick; projectDAL: Pick; permissionService: Pick; - kmsService: Pick; }; export type TDynamicSecretServiceFactory = ReturnType; @@ -47,8 +46,7 @@ export const dynamicSecretServiceFactory = ({ dynamicSecretProviders, permissionService, dynamicSecretQueueService, - projectDAL, - kmsService + projectDAL }: TDynamicSecretServiceFactoryDep) => { const create = async ({ path, @@ -98,16 +96,16 @@ export const dynamicSecretServiceFactory = ({ const isConnected = await selectedProvider.validateConnection(provider.inputs); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const encryptedConfig = secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob; + const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); const dynamicSecretCfg = await dynamicSecretDAL.create({ type: provider.type, version: 1, - encryptedConfig, + inputIV: encryptedInput.iv, + inputTag: encryptedInput.tag, + inputCiphertext: encryptedInput.ciphertext, + algorithm: encryptedInput.algorithm, + keyEncoding: encryptedInput.encoding, maxTTL, defaultTTL, folderId: folder.id, @@ -167,28 +165,27 @@ export const dynamicSecretServiceFactory = ({ } const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = - await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); - - const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const updatedInput = await selectedProvider.validateProviderInputs(newInput); const isConnected = await selectedProvider.validateConnection(newInput); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const encryptedConfig = secretManagerEncryptor({ - plainText: Buffer.from(JSON.stringify(updatedInput)) - }).cipherTextBlob; - + const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput)); const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { - encryptedConfig, + inputIV: encryptedInput.iv, + inputTag: encryptedInput.tag, + inputCiphertext: encryptedInput.ciphertext, + algorithm: encryptedInput.algorithm, + keyEncoding: encryptedInput.encoding, maxTTL, defaultTTL, name: newName ?? name, @@ -289,16 +286,14 @@ export const dynamicSecretServiceFactory = ({ const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" }); - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - - const dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); - - const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; return { ...dynamicSecretCfg, inputs: providerInputs }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4e0dd4e45..023e13000 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -677,8 +677,7 @@ export const registerRoutes = async ( permissionService, webhookDAL, projectEnvDAL, - projectDAL, - kmsService + projectDAL }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); @@ -988,9 +987,7 @@ export const registerRoutes = async ( queueService, dynamicSecretLeaseDAL, dynamicSecretProviders, - dynamicSecretDAL, - kmsService, - folderDAL + dynamicSecretDAL }); const dynamicSecretService = dynamicSecretServiceFactory({ projectDAL, @@ -1000,8 +997,7 @@ export const registerRoutes = async ( dynamicSecretProviders, folderDAL, permissionService, - licenseService, - kmsService + licenseService }); const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ projectDAL, @@ -1011,8 +1007,7 @@ export const registerRoutes = async ( dynamicSecretLeaseDAL, dynamicSecretProviders, folderDAL, - licenseService, - kmsService + licenseService }); const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index ab0557e93..a10a962cf 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -129,7 +129,11 @@ export const SanitizedRoleSchema = ProjectRolesSchema.extend({ }); export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ - encryptedConfig: true + inputIV: true, + inputTag: true, + inputCiphertext: true, + keyEncoding: true, + algorithm: true }); export const SanitizedAuditLogStreamSchema = z.object({ diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 6626dff7f..28053f4b3 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1133,7 +1133,7 @@ export const secretQueueFactory = ({ }); queueService.start(QueueName.SecretWebhook, async (job) => { - await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL, kmsService }); + await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL }); }); return { diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index 7f91ca2db..4690edba9 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -3,12 +3,12 @@ import crypto from "node:crypto"; import { AxiosError } from "axios"; import picomatch from "picomatch"; +import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas"; import { request } from "@app/lib/config/request"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; -import { TKmsServiceFactory } from "../kms/kms-service"; -import { KmsDataKey } from "../kms/kms-types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; @@ -16,12 +16,40 @@ import { WebhookType } from "./webhook-types"; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; -export const triggerWebhookRequest = async ( - { webhookSecretKey: secretKey, webhookUrl: url }: { webhookSecretKey?: string; webhookUrl: string }, - data: Record -) => { +export const decryptWebhookDetails = (webhook: TWebhooks) => { + const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook; + + let decryptedSecretKey = ""; + let decryptedUrl = url; + + if (encryptedSecretKey) { + decryptedSecretKey = infisicalSymmetricDecrypt({ + keyEncoding: keyEncoding as SecretKeyEncoding, + ciphertext: encryptedSecretKey, + iv: iv as string, + tag: tag as string + }); + } + + if (urlCipherText) { + decryptedUrl = infisicalSymmetricDecrypt({ + keyEncoding: keyEncoding as SecretKeyEncoding, + ciphertext: urlCipherText, + iv: urlIV as string, + tag: urlTag as string + }); + } + + return { + secretKey: decryptedSecretKey, + url: decryptedUrl + }; +}; + +export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record) => { const headers: Record = {}; const payload = { ...data, timestamp: Date.now() }; + const { secretKey, url } = decryptWebhookDetails(webhook); if (secretKey) { const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex"); @@ -96,7 +124,6 @@ export type TFnTriggerWebhookDTO = { webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; - kmsService: Pick; }; // this is reusable function @@ -107,8 +134,7 @@ export const fnTriggerWebhook = async ({ projectId, webhookDAL, projectEnvDAL, - projectDAL, - kmsService + projectDAL }: TFnTriggerWebhookDTO) => { const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const toBeTriggeredHooks = webhooks.filter( @@ -118,20 +144,10 @@ export const fnTriggerWebhook = async ({ if (!toBeTriggeredHooks.length) return; logger.info("Secret webhook job started", { environment, secretPath, projectId }); const project = await projectDAL.findById(projectId); - const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ - projectId, - type: KmsDataKey.SecretManager - }); - const webhooksTriggered = await Promise.allSettled( - toBeTriggeredHooks.map((hook) => { - const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString(); - const webhookSecretKey = hook.encryptedSecretKeyWithKms - ? kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString() - : undefined; - - return triggerWebhookRequest( - { webhookUrl, webhookSecretKey }, + toBeTriggeredHooks.map((hook) => + triggerWebhookRequest( + hook, getWebhookPayload("secrets.modified", { workspaceName: project.name, workspaceId: projectId, @@ -139,8 +155,8 @@ export const fnTriggerWebhook = async ({ secretPath, type: hook.type }) - ); - }) + ) + ) ); // filter hooks by status diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index 2698b1ea2..41dacd34b 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -1,15 +1,15 @@ import { ForbiddenError } from "@casl/ability"; +import { TWebhooksInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; -import { TKmsServiceFactory } from "../kms/kms-service"; -import { KmsDataKey } from "../kms/kms-types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; -import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; +import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; import { TCreateWebhookDTO, TDeleteWebhookDTO, @@ -23,7 +23,6 @@ type TWebhookServiceFactoryDep = { projectEnvDAL: TProjectEnvDALFactory; projectDAL: Pick; permissionService: Pick; - kmsService: Pick; }; export type TWebhookServiceFactory = ReturnType; @@ -32,8 +31,7 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionService, - projectDAL, - kmsService + projectDAL }: TWebhookServiceFactoryDep) => { const createWebhook = async ({ actor, @@ -58,28 +56,33 @@ export const webhookServiceFactory = ({ const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new BadRequestError({ message: "Env not found" }); - const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ - projectId, - type: KmsDataKey.SecretManager - }); - - const encryptedSecretKeyWithKms = webhookSecretKey - ? secretManagerEncryptor({ - plainText: Buffer.from(webhookSecretKey) - }).cipherTextBlob - : null; - const encryptedUrl = secretManagerEncryptor({ - plainText: Buffer.from(webhookUrl) - }).cipherTextBlob; - - const webhook = await webhookDAL.create({ - encryptedUrl, - encryptedSecretKeyWithKms, + const insertDoc: TWebhooksInsert = { + url: "", // deprecated - we are moving away from plaintext URLs envId: env.id, isDisabled: false, secretPath: secretPath || "/", type - }); + }; + + if (webhookSecretKey) { + const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey); + insertDoc.encryptedSecretKey = ciphertext; + insertDoc.iv = iv; + insertDoc.tag = tag; + insertDoc.algorithm = algorithm; + insertDoc.keyEncoding = encoding; + } + + if (webhookUrl) { + const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl); + insertDoc.urlCipherText = ciphertext; + insertDoc.urlIV = iv; + insertDoc.urlTag = tag; + insertDoc.algorithm = algorithm; + insertDoc.keyEncoding = encoding; + } + + const webhook = await webhookDAL.create(insertDoc); return { ...webhook, projectId, environment: env }; }; @@ -133,18 +136,9 @@ export const webhookServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); let webhookError: string | undefined; - const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ - projectId: project.id, - type: KmsDataKey.SecretManager - }); - const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedUrl }).toString(); - const webhookSecretKey = webhook.encryptedSecretKeyWithKms - ? kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedSecretKeyWithKms }).toString() - : undefined; - try { await triggerWebhookRequest( - { webhookUrl, webhookSecretKey }, + webhook, getWebhookPayload("test", { workspaceName: project.name, workspaceId: webhook.projectId, @@ -183,15 +177,11 @@ export const webhookServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath); - const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); return webhooks.map((w) => { - const decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString(); + const { url } = decryptWebhookDetails(w); return { ...w, - url: decryptedUrl + url }; }); };