From 5c8d6157d74216559d4241ab938e0c754aaefe7b Mon Sep 17 00:00:00 2001 From: = Date: Tue, 30 Jul 2024 00:31:42 +0530 Subject: [PATCH] feat: added logic for webhook and dynamic secret to use the kms encryption --- .../db/migrations/20240716105646_secret-v2.ts | 43 ++++++- backend/src/db/schemas/dynamic-secrets.ts | 15 ++- backend/src/db/schemas/integration-auths.ts | 1 - backend/src/db/schemas/webhooks.ts | 6 +- .../dynamic-secret-lease-dal.ts | 4 +- .../dynamic-secret-lease-queue.ts | 73 +++++++++-- .../dynamic-secret-lease-service.ts | 76 ++++++++---- .../dynamic-secret/dynamic-secret-service.ts | 90 ++++++++++---- backend/src/queue/queue-service.ts | 1 + backend/src/server/routes/index.ts | 16 ++- .../integration-auth-service.ts | 2 +- backend/src/services/secret/secret-queue.ts | 114 +++++++++++++++++- backend/src/services/webhook/webhook-dal.ts | 2 +- backend/src/services/webhook/webhook-fns.ts | 82 +++++++------ .../src/services/webhook/webhook-service.ts | 83 ++++++++++--- 15 files changed, 471 insertions(+), 137 deletions(-) diff --git a/backend/src/db/migrations/20240716105646_secret-v2.ts b/backend/src/db/migrations/20240716105646_secret-v2.ts index 020c46b78..84a151586 100644 --- a/backend/src/db/migrations/20240716105646_secret-v2.ts +++ b/backend/src/db/migrations/20240716105646_secret-v2.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { SecretType, TableName } from "../schemas"; +import { SecretEncryptionAlgo, SecretKeyEncoding, SecretType, TableName } from "../schemas"; import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { @@ -143,6 +143,32 @@ export async function up(knex: Knex): Promise { t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE"); }); } + + 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"); + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (!hasEncryptedWebhookSecretKey) t.binary("encryptedSecretKeyWithKms"); + if (!hasEncryptedWebhookUrl) t.binary("encryptedUrl"); + }); + } + + if (await knex.schema.hasTable(TableName.DynamicSecret)) { + 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"); + const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig"); + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (hasInputIV) t.string("inputIV").alter(); + if (hasInputCipherText) t.text("inputCiphertext").alter(); + if (hasInputTag) t.string("inputTag").alter(); + if (hasAlgorithm) t.string("algorithm").defaultTo(SecretEncryptionAlgo.AES_256_GCM).alter(); + if (hasKeyEncoding) t.string("keyEncoding").defaultTo(SecretKeyEncoding.UTF8).alter(); + if (!hasEncryptedConfig) t.binary("encryptedConfig"); + }); + } } export async function down(knex: Knex): Promise { @@ -177,4 +203,19 @@ 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"); + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (hasEncryptedWebhookSecretKey) t.dropColumn("encryptedSecretKeyWithKms"); + if (hasEncryptedWebhookUrl) t.dropColumn("encryptedUrl"); + }); + } + + if (await knex.schema.hasTable(TableName.DynamicSecret)) { + const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig"); + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (hasEncryptedConfig) t.dropColumn("encryptedConfig"); + }); + } } diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index b27da396c..83c4e308d 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const DynamicSecretsSchema = z.object({ @@ -14,16 +16,17 @@ 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"), + inputIV: z.string().nullable().optional(), + inputCiphertext: z.string().nullable().optional(), + inputTag: z.string().nullable().optional(), + algorithm: z.string().default("aes-256-gcm").nullable().optional(), + keyEncoding: z.string().default("utf8").nullable().optional(), folderId: z.string().uuid(), status: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedConfig: zodBuffer.nullable().optional() }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/integration-auths.ts b/backend/src/db/schemas/integration-auths.ts index 1e38c2139..85368eff7 100644 --- a/backend/src/db/schemas/integration-auths.ts +++ b/backend/src/db/schemas/integration-auths.ts @@ -35,7 +35,6 @@ export const IntegrationAuthsSchema = z.object({ awsAssumeIamRoleArnCipherText: z.string().nullable().optional(), awsAssumeIamRoleArnIV: z.string().nullable().optional(), awsAssumeIamRoleArnTag: z.string().nullable().optional(), - encryptedAwsIamAssumRole: zodBuffer.nullable().optional(), encryptedAccess: zodBuffer.nullable().optional(), encryptedAccessId: zodBuffer.nullable().optional(), encryptedRefresh: zodBuffer.nullable().optional(), diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index a7aac2933..084d72e8e 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const WebhooksSchema = z.object({ @@ -25,7 +27,9 @@ export const WebhooksSchema = z.object({ urlCipherText: z.string().nullable().optional(), urlIV: z.string().nullable().optional(), urlTag: z.string().nullable().optional(), - type: z.string().default("general").nullable().optional() + type: z.string().default("general").nullable().optional(), + encryptedSecretKeyWithKms: zodBuffer.nullable().optional(), + encryptedUrl: zodBuffer.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 339b2d626..7463d3b9e 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 @@ -48,6 +48,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { 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") ); @@ -71,7 +72,8 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { status: doc.dynStatus, statusDetails: doc.dynStatusDetails, createdAt: doc.dynCreatedAt, - updatedAt: doc.dynUpdatedAt + updatedAt: doc.dynUpdatedAt, + encryptedConfig: doc.dynEncryptedConfig } }; } 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 9bdb1c24e..44134f92d 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,8 +1,12 @@ 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 { BadRequestError } from "@app/lib/errors"; 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"; @@ -14,6 +18,8 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = { dynamicSecretLeaseDAL: Pick; dynamicSecretDAL: Pick; dynamicSecretProviders: Record; + kmsService: Pick; + folderDAL: Pick; }; export type TDynamicSecretLeaseQueueServiceFactory = ReturnType; @@ -22,7 +28,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ queueService, dynamicSecretDAL, dynamicSecretProviders, - dynamicSecretLeaseDAL + dynamicSecretLeaseDAL, + kmsService, + folderDAL }: TDynamicSecretLeaseQueueServiceFactoryDep) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { await queueService.queue( @@ -77,15 +85,38 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; - const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ + 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 + }); + + let dynamicSecretInputConfig = ""; + if ( + dynamicSecretCfg.keyEncoding && + dynamicSecretCfg.inputCiphertext && + dynamicSecretCfg.inputTag && + dynamicSecretCfg.inputIV + ) { + dynamicSecretInputConfig = infisicalSymmetricDecrypt({ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, ciphertext: dynamicSecretCfg.inputCiphertext, tag: dynamicSecretCfg.inputTag, iv: dynamicSecretCfg.inputIV - }) - ) as object; + }); + } else if (dynamicSecretCfg.encryptedConfig) { + dynamicSecretInputConfig = secretManagerDecryptor({ + cipherTextBlob: dynamicSecretCfg.encryptedConfig + }).toString(); + } else { + throw new BadRequestError({ message: "Missing secret input config" }); + } + + const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; + const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id); @@ -100,17 +131,39 @@ 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 decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ + let dynamicSecretInputConfig = ""; + + if ( + dynamicSecretCfg.keyEncoding && + dynamicSecretCfg.inputCiphertext && + dynamicSecretCfg.inputTag && + dynamicSecretCfg.inputIV + ) { + dynamicSecretInputConfig = infisicalSymmetricDecrypt({ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, ciphertext: dynamicSecretCfg.inputCiphertext, tag: dynamicSecretCfg.inputTag, iv: dynamicSecretCfg.inputIV - }) - ) as object; + }); + } else if (dynamicSecretCfg.encryptedConfig) { + dynamicSecretInputConfig = secretManagerDecryptor({ + cipherTextBlob: dynamicSecretCfg.encryptedConfig + }).toString(); + } else { + throw new BadRequestError({ message: "Missing secret input config" }); + } + const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) 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 1e5487d22..d85b69006 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,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import ms from "ms"; -import { SecretKeyEncoding } from "@app/db/schemas"; +import { SecretKeyEncoding, TDynamicSecrets } 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"; @@ -9,6 +9,8 @@ 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,6 +36,7 @@ type TDynamicSecretLeaseServiceFactoryDep = { folderDAL: Pick; permissionService: Pick; projectDAL: Pick; + kmsService: Pick; }; export type TDynamicSecretLeaseServiceFactory = ReturnType; @@ -46,8 +49,32 @@ export const dynamicSecretLeaseServiceFactory = ({ permissionService, dynamicSecretQueueService, projectDAL, - licenseService + licenseService, + kmsService }: TDynamicSecretLeaseServiceFactoryDep) => { + const $getDynamicSecretInputConfig = (dynamicSecretCfg: TDynamicSecrets, decryptValue: (arg: Buffer) => string) => { + if ( + dynamicSecretCfg.keyEncoding && + dynamicSecretCfg.inputCiphertext && + dynamicSecretCfg.inputTag && + dynamicSecretCfg.inputIV + ) { + return JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; + } + if (dynamicSecretCfg.encryptedConfig) { + return JSON.parse(decryptValue(dynamicSecretCfg.encryptedConfig)) as object; + } + + throw new BadRequestError({ message: "Missing secret input config" }); + }; + const create = async ({ environmentSlug, path, @@ -94,14 +121,13 @@ export const dynamicSecretLeaseServiceFactory = ({ throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) - ) as object; + const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) => + kmsDecryptor({ cipherTextBlob: value }).toString() + ); const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const { maxTTL } = dynamicSecretCfg; @@ -164,14 +190,13 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) - ) as object; + const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) => + kmsDecryptor({ cipherTextBlob: value }).toString() + ); const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const { maxTTL } = dynamicSecretCfg; @@ -231,14 +256,13 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) - ) as object; + const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) => + kmsDecryptor({ cipherTextBlob: value }).toString() + ); 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 1aef3cc86..f25a79c35 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -4,8 +4,10 @@ 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 { infisicalSymmetricDecrypt } 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,6 +36,7 @@ type TDynamicSecretServiceFactoryDep = { folderDAL: Pick; projectDAL: Pick; permissionService: Pick; + kmsService: Pick; }; export type TDynamicSecretServiceFactory = ReturnType; @@ -46,7 +49,8 @@ export const dynamicSecretServiceFactory = ({ dynamicSecretProviders, permissionService, dynamicSecretQueueService, - projectDAL + projectDAL, + kmsService }: TDynamicSecretServiceFactoryDep) => { const create = async ({ path, @@ -96,16 +100,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 encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); + const encryptedConfig = secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob; const dynamicSecretCfg = await dynamicSecretDAL.create({ type: provider.type, version: 1, - inputIV: encryptedInput.iv, - inputTag: encryptedInput.tag, - inputCiphertext: encryptedInput.ciphertext, - algorithm: encryptedInput.algorithm, - keyEncoding: encryptedInput.encoding, + encryptedConfig, maxTTL, defaultTTL, folderId: folder.id, @@ -164,28 +168,51 @@ export const dynamicSecretServiceFactory = ({ throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" }); } + let dynamicSecretInputConfig = ""; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ + const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + if ( + dynamicSecretCfg.keyEncoding && + dynamicSecretCfg.inputCiphertext && + dynamicSecretCfg.inputTag && + dynamicSecretCfg.inputIV + ) { + dynamicSecretInputConfig = infisicalSymmetricDecrypt({ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, ciphertext: dynamicSecretCfg.inputCiphertext, tag: dynamicSecretCfg.inputTag, iv: dynamicSecretCfg.inputIV - }) - ) as object; + }); + } else if (dynamicSecretCfg.encryptedConfig) { + dynamicSecretInputConfig = secretManagerDecryptor({ + cipherTextBlob: dynamicSecretCfg.encryptedConfig + }).toString(); + } else { + throw new BadRequestError({ message: "Missing secret input config" }); + } + + const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) 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 encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput)); + const encryptedConfig = secretManagerEncryptor({ + plainText: Buffer.from(JSON.stringify(updatedInput)) + }).cipherTextBlob; + const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { - inputIV: encryptedInput.iv, - inputTag: encryptedInput.tag, - inputCiphertext: encryptedInput.ciphertext, - algorithm: encryptedInput.algorithm, - keyEncoding: encryptedInput.encoding, + encryptedConfig, + inputIV: null, + inputTag: null, + keyEncoding: null, + algorithm: null, + inputCiphertext: null, maxTTL, defaultTTL, name: newName ?? name, @@ -286,14 +313,33 @@ export const dynamicSecretServiceFactory = ({ const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" }); - const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + + let dynamicSecretInputConfig = ""; + if ( + dynamicSecretCfg.keyEncoding && + dynamicSecretCfg.inputCiphertext && + dynamicSecretCfg.inputTag && + dynamicSecretCfg.inputIV + ) { + dynamicSecretInputConfig = infisicalSymmetricDecrypt({ keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, ciphertext: dynamicSecretCfg.inputCiphertext, tag: dynamicSecretCfg.inputTag, iv: dynamicSecretCfg.inputIV - }) - ) as object; + }); + } else if (dynamicSecretCfg.encryptedConfig) { + dynamicSecretInputConfig = secretManagerDecryptor({ + cipherTextBlob: dynamicSecretCfg.encryptedConfig + }).toString(); + } else { + throw new BadRequestError({ message: "Missing secret input config" }); + } + + const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) 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/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 1f58dfec3..ec222f0fa 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -216,6 +216,7 @@ export const queueServiceFactory = (redisUrl: string) => { const job = await q.getJob(jobId); if (!job) return true; if (!job.repeatJobKey) return true; + await job.remove(); return q.removeRepeatableByKey(job.repeatJobKey); }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 023e13000..9b90ccbab 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -677,7 +677,8 @@ export const registerRoutes = async ( permissionService, webhookDAL, projectEnvDAL, - projectDAL + projectDAL, + kmsService }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); @@ -724,7 +725,8 @@ export const registerRoutes = async ( integrationAuthDAL, snapshotDAL, snapshotSecretV2BridgeDAL, - secretApprovalRequestDAL + secretApprovalRequestDAL, + dynamicSecretDAL }); const secretImportService = secretImportServiceFactory({ licenseService, @@ -987,7 +989,9 @@ export const registerRoutes = async ( queueService, dynamicSecretLeaseDAL, dynamicSecretProviders, - dynamicSecretDAL + dynamicSecretDAL, + kmsService, + folderDAL }); const dynamicSecretService = dynamicSecretServiceFactory({ projectDAL, @@ -997,7 +1001,8 @@ export const registerRoutes = async ( dynamicSecretProviders, folderDAL, permissionService, - licenseService + licenseService, + kmsService }); const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ projectDAL, @@ -1007,7 +1012,8 @@ export const registerRoutes = async ( dynamicSecretLeaseDAL, dynamicSecretProviders, folderDAL, - licenseService + licenseService, + kmsService }); const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 5ffed26d2..e16f6bb77 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -269,7 +269,7 @@ export const integrationAuthServiceFactory = ({ const awsAssumeIamRoleArnEncrypted = secretManagerEncryptor({ plainText: Buffer.from(awsAssumeIamRoleArn) }).cipherTextBlob; - updateDoc.encryptedAwsIamAssumRole = awsAssumeIamRoleArnEncrypted; + updateDoc.encryptedAwsAssumeIamRoleArn = awsAssumeIamRoleArnEncrypted; } } } else { diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 0fbce0964..a682a0bcd 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1,13 +1,21 @@ /* eslint-disable no-await-in-loop */ import { AxiosError } from "axios"; -import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas"; +import { + ProjectUpgradeStatus, + ProjectVersion, + SecretKeyEncoding, + TSecretSnapshotSecretsV2, + TSecretVersionsV2 +} from "@app/db/schemas"; +import { TDynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal"; import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { BadRequestError } from "@app/lib/errors"; import { groupBy, isSamePath, unique } from "@app/lib/fn"; @@ -60,7 +68,7 @@ type TSecretQueueFactoryDep = { folderDAL: TSecretFolderDALFactory; secretDAL: TSecretDALFactory; secretImportDAL: Pick; - webhookDAL: Pick; + webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: TProjectDALFactory; projectBotDAL: TProjectBotDALFactory; @@ -79,6 +87,7 @@ type TSecretQueueFactoryDep = { secretApprovalRequestDAL: Pick; snapshotDAL: Pick; snapshotSecretV2BridgeDAL: Pick; + dynamicSecretDAL: Pick; }; export type TGetSecrets = { @@ -122,7 +131,8 @@ export const secretQueueFactory = ({ secretRotationDAL, snapshotDAL, snapshotSecretV2BridgeDAL, - secretApprovalRequestDAL + secretApprovalRequestDAL, + dynamicSecretDAL }: TSecretQueueFactoryDep) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const appCfg = getConfig(); @@ -881,6 +891,37 @@ export const secretQueueFactory = ({ await secretV2BridgeDAL.upsertSecretReferences(secretReferences, tx); } + const dynamicSecrets = await dynamicSecretDAL.find({ folderId }, { tx }); + if (dynamicSecrets.length) { + await dynamicSecretDAL.upsert( + dynamicSecrets.map((el) => { + let { encryptedConfig } = el; + if (!encryptedConfig) { + if (el.keyEncoding && el.inputCiphertext && el.inputTag && el.inputIV) { + const decryptedConfig = infisicalSymmetricDecrypt({ + keyEncoding: el.keyEncoding as SecretKeyEncoding, + ciphertext: el.inputCiphertext, + tag: el.inputTag, + iv: el.inputIV + }); + encryptedConfig = secretManagerEncryptor({ plainText: Buffer.from(decryptedConfig) }).cipherTextBlob; + } + } + return { + ...el, + encryptedConfig, + keyEncoding: null, + inputCiphertext: null, + inputTag: null, + inputIV: null, + algorithm: null + }; + }), + "id", + tx + ); + } + const SNAPSHOT_BATCH_SIZE = 15; const snapshots = await snapshotDAL.findNSecretV1SnapshotByFolderId(folderId, SNAPSHOT_BATCH_SIZE, tx); const projectV3SecretVersionsGroupById: Record = {}; @@ -1110,6 +1151,71 @@ export const secretQueueFactory = ({ tx ); + /* + * webhooks + * */ + const projectV1Webhooks = await webhookDAL.find({ projectId }, tx); + if (projectV1Webhooks.length) { + await webhookDAL.upsert( + projectV1Webhooks.map((el) => { + let { encryptedSecretKeyWithKms, encryptedUrl } = el; + if (!encryptedSecretKeyWithKms) { + if (el.encryptedSecretKey && el.iv && el.tag) { + const webhookSecretKey = infisicalSymmetricDecrypt({ + keyEncoding: el.keyEncoding as SecretKeyEncoding, + ciphertext: el.encryptedSecretKey, + iv: el.iv, + tag: el.tag + }); + encryptedSecretKeyWithKms = secretManagerEncryptor({ + plainText: Buffer.from(webhookSecretKey) + }).cipherTextBlob; + } + } + if (!encryptedUrl) { + if (el.urlTag && el.urlCipherText && el.urlIV) { + const webhookUrl = infisicalSymmetricDecrypt({ + keyEncoding: el.keyEncoding as SecretKeyEncoding, + ciphertext: el.urlCipherText, + iv: el.urlIV, + tag: el.urlTag + }); + encryptedUrl = secretManagerEncryptor({ + plainText: Buffer.from(webhookUrl) + }).cipherTextBlob; + } else { + encryptedUrl = secretManagerEncryptor({ + plainText: Buffer.from(el.url) + }).cipherTextBlob; + } + } + + return { + id: el.id, + url: el.url, + envId: el.envId, + type: el.type, + isDisabled: el.isDisabled, + lastStatus: el.lastStatus, + secretPath: el.secretPath, + lastRunErrorMessage: el.lastRunErrorMessage, + encryptedSecretKeyWithKms, + encryptedUrl, + urlCipherText: null, + urlIV: null, + urlTag: null, + encryptedSecretKey: null, + iv: null, + tag: null, + keyEncoding: null, + algorithm: null + }; + }), + "id", + tx + ); + } + /* * approvals: we will delete all approvals this is because some secret versions may not be added yet * Thus doesn't make sense for rest to be there @@ -1193,7 +1299,7 @@ export const secretQueueFactory = ({ }); queueService.start(QueueName.SecretWebhook, async (job) => { - await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL }); + await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL, kmsService }); }); return { diff --git a/backend/src/services/webhook/webhook-dal.ts b/backend/src/services/webhook/webhook-dal.ts index 14d30a35e..ba2745737 100644 --- a/backend/src/services/webhook/webhook-dal.ts +++ b/backend/src/services/webhook/webhook-dal.ts @@ -20,7 +20,7 @@ export const webhookDALFactory = (db: TDbClient) => { .select(tx.ref("projectId").withSchema(TableName.Environment)) .select(selectAllTableCols(TableName.Webhook)); - const find = async (filter: Partial, tx?: Knex) => { + const find = async (filter: Partial, tx?: Knex) => { try { const docs = await webhookFindQuery(tx || db.replicaNode(), filter); return docs.map(({ envId, envSlug, envName, ...el }) => ({ diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index 4690edba9..32476fe9a 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -3,12 +3,14 @@ import crypto from "node:crypto"; import { AxiosError } from "axios"; import picomatch from "picomatch"; -import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas"; +import { SecretKeyEncoding } 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,40 +18,12 @@ import { WebhookType } from "./webhook-types"; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; -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) => { +export const triggerWebhookRequest = async ( + { webhookSecretKey: secretKey, webhookUrl: url }: { webhookSecretKey?: string; webhookUrl: string }, + 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"); @@ -124,6 +98,7 @@ export type TFnTriggerWebhookDTO = { webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; + kmsService: Pick; }; // this is reusable function @@ -134,7 +109,8 @@ export const fnTriggerWebhook = async ({ projectId, webhookDAL, projectEnvDAL, - projectDAL + projectDAL, + kmsService }: TFnTriggerWebhookDTO) => { const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const toBeTriggeredHooks = webhooks.filter( @@ -144,10 +120,38 @@ 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) => - triggerWebhookRequest( - hook, + toBeTriggeredHooks.map((hook) => { + let webhookUrl = hook.url; + let webhookSecretKey; + if (hook.urlTag && hook.urlCipherText && hook.urlIV) { + webhookUrl = infisicalSymmetricDecrypt({ + keyEncoding: hook.keyEncoding as SecretKeyEncoding, + ciphertext: hook.urlCipherText, + iv: hook.urlIV, + tag: hook.urlTag + }); + } else if (hook.encryptedUrl) { + webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString(); + } + if (hook.encryptedSecretKey && hook.iv && hook.tag) { + webhookSecretKey = infisicalSymmetricDecrypt({ + keyEncoding: hook.keyEncoding as SecretKeyEncoding, + ciphertext: hook.encryptedSecretKey, + iv: hook.iv, + tag: hook.tag + }); + } else if (hook.encryptedSecretKeyWithKms) { + webhookSecretKey = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString(); + } + + return triggerWebhookRequest( + { webhookUrl, webhookSecretKey }, getWebhookPayload("secrets.modified", { workspaceName: project.name, workspaceId: projectId, @@ -155,8 +159,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 41dacd34b..84b4baadf 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -1,15 +1,17 @@ import { ForbiddenError } from "@casl/ability"; -import { TWebhooksInsert } from "@app/db/schemas"; +import { SecretKeyEncoding, 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 { infisicalSymmetricDecrypt } 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 { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; +import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; import { TCreateWebhookDTO, TDeleteWebhookDTO, @@ -23,6 +25,7 @@ type TWebhookServiceFactoryDep = { projectEnvDAL: TProjectEnvDALFactory; projectDAL: Pick; permissionService: Pick; + kmsService: Pick; }; export type TWebhookServiceFactory = ReturnType; @@ -31,7 +34,8 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionService, - projectDAL + projectDAL, + kmsService }: TWebhookServiceFactoryDep) => { const createWebhook = async ({ actor, @@ -64,22 +68,23 @@ export const webhookServiceFactory = ({ type }; + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + projectId, + type: KmsDataKey.SecretManager + }); 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; + const encryptedSecretKeyWithKms = secretManagerEncryptor({ + plainText: Buffer.from(webhookSecretKey) + }).cipherTextBlob; + insertDoc.encryptedSecretKeyWithKms = encryptedSecretKeyWithKms; } 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 encryptedUrl = secretManagerEncryptor({ + plainText: Buffer.from(webhookUrl) + }).cipherTextBlob; + + insertDoc.encryptedUrl = encryptedUrl; } const webhook = await webhookDAL.create(insertDoc); @@ -136,9 +141,35 @@ 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 + }); + let webhookUrl = webhook.url; + let webhookSecretKey; + if (webhook.urlTag && webhook.urlCipherText && webhook.urlIV) { + webhookUrl = infisicalSymmetricDecrypt({ + keyEncoding: webhook.keyEncoding as SecretKeyEncoding, + ciphertext: webhook.urlCipherText, + iv: webhook.urlIV, + tag: webhook.urlTag + }); + } else if (webhook.encryptedUrl) { + webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedUrl }).toString(); + } + if (webhook.encryptedSecretKey && webhook.iv && webhook.tag) { + webhookSecretKey = infisicalSymmetricDecrypt({ + keyEncoding: webhook.keyEncoding as SecretKeyEncoding, + ciphertext: webhook.encryptedSecretKey, + iv: webhook.iv, + tag: webhook.tag + }); + } else if (webhook.encryptedSecretKeyWithKms) { + webhookSecretKey = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedSecretKeyWithKms }).toString(); + } try { await triggerWebhookRequest( - webhook, + { webhookUrl, webhookSecretKey }, getWebhookPayload("test", { workspaceName: project.name, workspaceId: webhook.projectId, @@ -177,11 +208,25 @@ 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 { url } = decryptWebhookDetails(w); + let decryptedUrl = w.url; + if (w.urlTag && w.urlCipherText && w.urlIV) { + decryptedUrl = infisicalSymmetricDecrypt({ + keyEncoding: w.keyEncoding as SecretKeyEncoding, + ciphertext: w.urlCipherText, + iv: w.urlIV, + tag: w.urlTag + }); + } else if (w.encryptedUrl) { + decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString(); + } return { ...w, - url + url: decryptedUrl }; }); };