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
@@ -269,7 +269,7 @@ export const integrationAuthServiceFactory = ({
const awsAssumeIamRoleArnEncrypted = secretManagerEncryptor({
plainText: Buffer.from(awsAssumeIamRoleArn)
}).cipherTextBlob;
updateDoc.encryptedAwsIamAssumRole = awsAssumeIamRoleArnEncrypted;
updateDoc.encryptedAwsAssumeIamRoleArn = awsAssumeIamRoleArnEncrypted;
}
}
} else {
+110 -4
View File
@@ -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<TSecretImportDALFactory, "find" | "findByFolderIds">;
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "find" | "transaction" | "update" | "bulkUpdate" | "upsert">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
projectDAL: TProjectDALFactory;
projectBotDAL: TProjectBotDALFactory;
@@ -79,6 +87,7 @@ type TSecretQueueFactoryDep = {
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "find" | "upsert">;
};
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<string, TSecretVersionsV2> = {};
@@ -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 {
+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
};
});
};