feat: added logic for webhook and dynamic secret to use the kms encryption

This commit is contained in:
=
2024-07-30 23:19:32 +05:30
parent 5d78b6941d
commit 5c8d6157d7
15 changed files with 471 additions and 137 deletions
+1 -1
View File
@@ -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<TWebhooks>, tx?: Knex) => {
const find = async (filter: Partial<TWebhooks & { projectId: string }>, tx?: Knex) => {
try {
const docs = await webhookFindQuery(tx || db.replicaNode(), filter);
return docs.map(({ envId, envSlug, envName, ...el }) => ({
+43 -39
View File
@@ -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<string, unknown>) => {
export const triggerWebhookRequest = async (
{ webhookSecretKey: secretKey, webhookUrl: url }: { webhookSecretKey?: string; webhookUrl: string },
data: Record<string, unknown>
) => {
const headers: Record<string, string> = {};
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<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
// 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
+64 -19
View File
@@ -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<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
@@ -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
};
});
};