diff --git a/backend/src/db/migrations/20240717114407_add-project-data-key.ts b/backend/src/db/migrations/20240716105630_add-project-data-key.ts similarity index 100% rename from backend/src/db/migrations/20240717114407_add-project-data-key.ts rename to backend/src/db/migrations/20240716105630_add-project-data-key.ts diff --git a/backend/src/db/migrations/20240716105646_secret-v2.ts b/backend/src/db/migrations/20240716105646_secret-v2.ts index 84a151586..a9188c608 100644 --- a/backend/src/db/migrations/20240716105646_secret-v2.ts +++ b/backend/src/db/migrations/20240716105646_secret-v2.ts @@ -1,7 +1,178 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import { Knex } from "knex"; -import { SecretEncryptionAlgo, SecretKeyEncoding, SecretType, TableName } from "../schemas"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex/select"; + +import { SecretKeyEncoding, 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); @@ -145,29 +316,11 @@ export async function up(knex: Knex): Promise { } 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"); - }); + await backfillWebhooks(knex); } 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"); - }); + await backfillDynamicSecretConfigs(knex); } } @@ -206,16 +359,46 @@ export async function down(knex: Knex): Promise { 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/migrations/utils/kms.ts b/backend/src/db/migrations/utils/kms.ts new file mode 100644 index 000000000..b4698a42d --- /dev/null +++ b/backend/src/db/migrations/utils/kms.ts @@ -0,0 +1,105 @@ +import slugify from "@sindresorhus/slugify"; +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; +import { randomSecureBytes } from "@app/lib/crypto"; +import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +const getInstanceRootKey = async (knex: Knex) => { + const encryptionKey = process.env.ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY; + // if root key its base64 encoded + const isBase64 = !process.env.ENCRYPTION_KEY; + if (!encryptionKey) throw new Error("ENCRYPTION_KEY variable needed for migration"); + const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); + + const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; + const kmsRootConfig = await knex(TableName.KmsServerRootConfig).where({ id: KMS_ROOT_CONFIG_UUID }).first(); + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + if (kmsRootConfig) { + const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); + // set the flag so that other instancen nodes can start + return decryptedRootKey; + } + + const newRootKey = randomSecureBytes(32); + const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer); + await knex(TableName.KmsServerRootConfig).insert({ + encryptedRootKey, + // eslint-disable-next-line + // @ts-ignore id is kept as fixed for idempotence and to avoid race condition + id: KMS_ROOT_CONFIG_UUID + }); + return encryptedRootKey; +}; + +export const getSecretManagerDataKey = async (knex: Knex, projectId: string) => { + const KMS_VERSION = "v01"; + const KMS_VERSION_BLOB_LENGTH = 3; + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const project = await knex(TableName.Project).where({ id: projectId }).first(); + if (!project) throw new Error("Missing project id"); + + const ROOT_ENCRYPTION_KEY = await getInstanceRootKey(knex); + + let secretManagerKmsKey; + const projectSecretManagerKmsId = project?.kmsSecretManagerKeyId; + if (projectSecretManagerKmsId) { + const kmsDoc = await knex(TableName.KmsKey) + .leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`) + .where({ [`${TableName.KmsKey}.id` as "id"]: projectSecretManagerKmsId }) + .first(); + if (!kmsDoc) throw new Error("missing kms"); + secretManagerKmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY); + } else { + const [kmsDoc] = await knex(TableName.KmsKey) + .insert({ + slug: slugify(alphaNumericNanoId(8).toLowerCase()), + orgId: project.orgId, + isReserved: false + }) + .returning("*"); + + secretManagerKmsKey = randomSecureBytes(32); + const encryptedKeyMaterial = cipher.encrypt(secretManagerKmsKey, ROOT_ENCRYPTION_KEY); + await knex(TableName.InternalKms).insert({ + version: 1, + encryptedKey: encryptedKeyMaterial, + encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, + kmsKeyId: kmsDoc.id + }); + } + + const encryptedSecretManagerDataKey = project?.kmsSecretManagerEncryptedDataKey; + let dataKey: Buffer; + if (!encryptedSecretManagerDataKey) { + dataKey = randomSecureBytes(); + // the below versioning we do it automatically in kms service + const unversionedDataKey = cipher.encrypt(dataKey, secretManagerKmsKey); + const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 + await knex(TableName.Project) + .where({ id: projectId }) + .update({ + kmsSecretManagerEncryptedDataKey: Buffer.concat([unversionedDataKey, versionBlob]) + }); + } else { + const cipherTextBlob = encryptedSecretManagerDataKey.subarray(0, -KMS_VERSION_BLOB_LENGTH); + dataKey = cipher.decrypt(cipherTextBlob, secretManagerKmsKey); + } + + return { + encryptor: ({ plainText }: { plainText: Buffer }) => { + const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey); + + // Buffer#1 encrypted text + Buffer#2 version number + const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 + const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]); + return { cipherTextBlob }; + }, + decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: { cipherTextBlob: Buffer }) => { + const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); + const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey); + return decryptedBlob; + } + }; +}; diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index 83c4e308d..d90f1f7d2 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -16,17 +16,12 @@ export const DynamicSecretsSchema = z.object({ type: z.string(), defaultTTL: z.string(), maxTTL: z.string().nullable().optional(), - 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(), - encryptedConfig: zodBuffer.nullable().optional() + encryptedConfig: zodBuffer }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index 084d72e8e..3f670497f 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -12,24 +12,15 @@ 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(), - urlCipherText: z.string().nullable().optional(), - urlIV: z.string().nullable().optional(), - urlTag: z.string().nullable().optional(), type: z.string().default("general").nullable().optional(), encryptedSecretKeyWithKms: zodBuffer.nullable().optional(), - encryptedUrl: zodBuffer.nullable().optional() + encryptedUrl: zodBuffer }); 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 7463d3b9e..97a682933 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 @@ -40,11 +40,6 @@ 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"), @@ -63,11 +58,6 @@ 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, 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 44134f92d..82248bd8c 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,7 +1,4 @@ -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"; @@ -94,27 +91,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ 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 - }); - } else if (dynamicSecretCfg.encryptedConfig) { - dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); - } else { - throw new BadRequestError({ message: "Missing secret input config" }); - } - + const dynamicSecretInputConfig = secretManagerDecryptor({ + cipherTextBlob: dynamicSecretCfg.encryptedConfig + }).toString(); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; @@ -142,27 +121,10 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId }); if (dynamicSecretLeases.length) { const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - 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 - }); - } else if (dynamicSecretCfg.encryptedConfig) { - dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); - } else { - throw new BadRequestError({ message: "Missing secret input config" }); - } + const dynamicSecretInputConfig = secretManagerDecryptor({ + cipherTextBlob: dynamicSecretCfg.encryptedConfig + }).toString(); const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); 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 d85b69006..eb0c6f171 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,12 +1,10 @@ import { ForbiddenError, subject } from "@casl/ability"; import ms from "ms"; -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"; 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"; @@ -52,29 +50,6 @@ export const dynamicSecretLeaseServiceFactory = ({ 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, @@ -125,9 +100,8 @@ export const dynamicSecretLeaseServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) => - kmsDecryptor({ cipherTextBlob: value }).toString() - ); + const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); + const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const { maxTTL } = dynamicSecretCfg; @@ -194,9 +168,8 @@ export const dynamicSecretLeaseServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) => - kmsDecryptor({ cipherTextBlob: value }).toString() - ); + const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); + const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const { maxTTL } = dynamicSecretCfg; @@ -260,9 +233,8 @@ export const dynamicSecretLeaseServiceFactory = ({ type: KmsDataKey.SecretManager, projectId }); - const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) => - kmsDecryptor({ cipherTextBlob: value }).toString() - ); + const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); + const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) 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 f25a79c35..508c263ee 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,10 +1,8 @@ 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 } 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"; @@ -168,32 +166,15 @@ 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 { 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 - }); - } else if (dynamicSecretCfg.encryptedConfig) { - dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); - } else { - throw new BadRequestError({ message: "Missing secret input config" }); - } + const dynamicSecretInputConfig = secretManagerDecryptor({ + cipherTextBlob: dynamicSecretCfg.encryptedConfig + }).toString(); const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; @@ -208,11 +189,6 @@ export const dynamicSecretServiceFactory = ({ const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { encryptedConfig, - inputIV: null, - inputTag: null, - keyEncoding: null, - algorithm: null, - inputCiphertext: null, maxTTL, defaultTTL, name: newName ?? name, @@ -318,26 +294,9 @@ export const dynamicSecretServiceFactory = ({ 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 - }); - } else if (dynamicSecretCfg.encryptedConfig) { - dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); - } else { - throw new BadRequestError({ message: "Missing secret input config" }); - } + const dynamicSecretInputConfig = secretManagerDecryptor({ + cipherTextBlob: dynamicSecretCfg.encryptedConfig + }).toString(); const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts index e59497368..8e5014fdd 100644 --- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts +++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts @@ -791,10 +791,19 @@ export const snapshotDALFactory = (db: TDbClient) => { const deleteSnapshotsAboveLimit = async (folderId: string, n = 15, tx?: Knex) => { try { - const query = await (tx || db.replicaNode())(TableName.Snapshot) - .orderBy(`${TableName.Snapshot}.createdAt`, "desc") - .where(`${TableName.Snapshot}.folderId`, folderId) - .offset(n) + const query = await (tx || db) + .with("to_delete", (qb) => { + void qb + .select("id") + .from(TableName.Snapshot) + .where("folderId", folderId) + .orderBy("createdAt", "desc") + .offset(n); + }) + .from(TableName.Snapshot) + .whereIn("id", (qb) => { + void qb.select("id").from("to_delete"); + }) .delete(); return query; } catch (error) { diff --git a/backend/src/lib/crypto/encryption.ts b/backend/src/lib/crypto/encryption.ts index b7a4e10d2..258a6d285 100644 --- a/backend/src/lib/crypto/encryption.ts +++ b/backend/src/lib/crypto/encryption.ts @@ -226,8 +226,9 @@ export const infisicalSymmetricDecrypt = ({ keyEncoding: SecretKeyEncoding; }) => { const appCfg = getConfig(); - const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; - const encryptionKey = appCfg.ENCRYPTION_KEY; + // the or gate is used used in migration + const rootEncryptionKey = appCfg?.ROOT_ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY; + const encryptionKey = appCfg?.ENCRYPTION_KEY || process.env.ENCRYPTION_KEY; if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) { const data = decryptSymmetric({ key: rootEncryptionKey, iv, tag, ciphertext }); return data as T; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9b90ccbab..4e0dd4e45 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -725,8 +725,7 @@ export const registerRoutes = async ( integrationAuthDAL, snapshotDAL, snapshotSecretV2BridgeDAL, - secretApprovalRequestDAL, - dynamicSecretDAL + secretApprovalRequestDAL }); const secretImportService = secretImportServiceFactory({ licenseService, diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index a10a962cf..ab0557e93 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -129,11 +129,7 @@ export const SanitizedRoleSchema = ProjectRolesSchema.extend({ }); export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ - inputIV: true, - inputTag: true, - inputCiphertext: true, - keyEncoding: true, - algorithm: true + encryptedConfig: true }); export const SanitizedAuditLogStreamSchema = z.object({ diff --git a/backend/src/server/routes/v1/webhook-router.ts b/backend/src/server/routes/v1/webhook-router.ts index 7423f9f3e..377af135c 100644 --- a/backend/src/server/routes/v1/webhook-router.ts +++ b/backend/src/server/routes/v1/webhook-router.ts @@ -8,25 +8,24 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { WebhookType } from "@app/services/webhook/webhook-types"; -export const sanitizedWebhookSchema = WebhooksSchema.omit({ - encryptedSecretKey: true, - iv: true, - tag: true, - algorithm: true, - keyEncoding: true, - urlCipherText: true, - urlIV: true, - urlTag: true -}).merge( - z.object({ - projectId: z.string(), - environment: z.object({ - id: z.string(), - name: z.string(), - slug: z.string() - }) +export const sanitizedWebhookSchema = WebhooksSchema.pick({ + id: true, + secretPath: true, + lastStatus: true, + lastRunErrorMessage: true, + isDisabled: true, + createdAt: true, + updatedAt: true, + envId: true, + type: true +}).extend({ + projectId: z.string(), + environment: z.object({ + id: z.string(), + name: z.string(), + slug: z.string() }) -); +}); export const registerWebhookRouter = async (server: FastifyZodProvider) => { server.route({ @@ -228,7 +227,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ message: z.string(), - webhooks: sanitizedWebhookSchema.array() + webhooks: sanitizedWebhookSchema.extend({ url: z.string() }).array() }) } }, diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index d5a48ddef..351098f9d 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -575,6 +575,7 @@ export const kmsServiceFactory = ({ // by keeping the decrypted data key in inner scope // none of the entities outside can interact directly or expose the data key + // NOTICE: If changing here update migrations/utils/kms const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => { const dataKey = await $getDataKey(encryptionContext); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); @@ -753,6 +754,7 @@ export const kmsServiceFactory = ({ return { id, slug, orgId, isExternal }; }; + // akhilmhdh: a copy of this is made in migrations/utils/kms const startService = async () => { const appCfg = getConfig(); // This will switch to a seal process and HMS flow in future diff --git a/backend/src/services/project-bot/project-bot-fns.ts b/backend/src/services/project-bot/project-bot-fns.ts index 59d0493ac..d37d620b2 100644 --- a/backend/src/services/project-bot/project-bot-fns.ts +++ b/backend/src/services/project-bot/project-bot-fns.ts @@ -36,8 +36,7 @@ export const getBotKeyFnFactory = ( if (!bot || !bot.isActive || !bot.encryptedProjectKey || !bot.encryptedProjectKeyNonce) { // trying to set bot automatically const projectV1Keys = await projectBotDAL.findProjectUserWorkspaceKey(projectId); - if (!projectV1Keys) - throw new BadRequestError({ message: "Bot not found. [no-private-key]. Please ask admin user to login" }); + if (!projectV1Keys) throw new BadRequestError({ message: "Bot not found. Please ask admin user to login" }); let userPrivateKey = ""; if ( diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index a682a0bcd..49ebbc7a4 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1,21 +1,13 @@ /* eslint-disable no-await-in-loop */ import { AxiosError } from "axios"; -import { - ProjectUpgradeStatus, - ProjectVersion, - SecretKeyEncoding, - TSecretSnapshotSecretsV2, - TSecretVersionsV2 -} from "@app/db/schemas"; -import { TDynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal"; +import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas"; 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"; @@ -68,7 +60,7 @@ type TSecretQueueFactoryDep = { folderDAL: TSecretFolderDALFactory; secretDAL: TSecretDALFactory; secretImportDAL: Pick; - webhookDAL: Pick; + webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: TProjectDALFactory; projectBotDAL: TProjectBotDALFactory; @@ -87,7 +79,6 @@ type TSecretQueueFactoryDep = { secretApprovalRequestDAL: Pick; snapshotDAL: Pick; snapshotSecretV2BridgeDAL: Pick; - dynamicSecretDAL: Pick; }; export type TGetSecrets = { @@ -131,8 +122,7 @@ export const secretQueueFactory = ({ secretRotationDAL, snapshotDAL, snapshotSecretV2BridgeDAL, - secretApprovalRequestDAL, - dynamicSecretDAL + secretApprovalRequestDAL }: TSecretQueueFactoryDep) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const appCfg = getConfig(); @@ -891,38 +881,7 @@ 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 SNAPSHOT_BATCH_SIZE = 10; const snapshots = await snapshotDAL.findNSecretV1SnapshotByFolderId(folderId, SNAPSHOT_BATCH_SIZE, tx); const projectV3SecretVersionsGroupById: Record = {}; const projectV3SecretVersionTags: { secret_versions_v2Id: string; secret_tagsId: string }[] = []; @@ -1151,71 +1110,6 @@ 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 diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index 32476fe9a..7f91ca2db 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -3,9 +3,7 @@ import crypto from "node:crypto"; import { AxiosError } from "axios"; import picomatch from "picomatch"; -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"; @@ -127,28 +125,10 @@ export const fnTriggerWebhook = async ({ const webhooksTriggered = await Promise.allSettled( 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(); - } + const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString(); + const webhookSecretKey = hook.encryptedSecretKeyWithKms + ? kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString() + : undefined; return triggerWebhookRequest( { webhookUrl, webhookSecretKey }, diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index 84b4baadf..2698b1ea2 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -1,9 +1,7 @@ import { ForbiddenError } from "@casl/ability"; -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 { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -60,34 +58,28 @@ export const webhookServiceFactory = ({ const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new BadRequestError({ message: "Env not found" }); - const insertDoc: TWebhooksInsert = { - url: "", // deprecated - we are moving away from plaintext URLs - envId: env.id, - isDisabled: false, - secretPath: secretPath || "/", - type - }; - const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ projectId, type: KmsDataKey.SecretManager }); - if (webhookSecretKey) { - const encryptedSecretKeyWithKms = secretManagerEncryptor({ - plainText: Buffer.from(webhookSecretKey) - }).cipherTextBlob; - insertDoc.encryptedSecretKeyWithKms = encryptedSecretKeyWithKms; - } - if (webhookUrl) { - const encryptedUrl = secretManagerEncryptor({ - plainText: Buffer.from(webhookUrl) - }).cipherTextBlob; + const encryptedSecretKeyWithKms = webhookSecretKey + ? secretManagerEncryptor({ + plainText: Buffer.from(webhookSecretKey) + }).cipherTextBlob + : null; + const encryptedUrl = secretManagerEncryptor({ + plainText: Buffer.from(webhookUrl) + }).cipherTextBlob; - insertDoc.encryptedUrl = encryptedUrl; - } - - const webhook = await webhookDAL.create(insertDoc); + const webhook = await webhookDAL.create({ + encryptedUrl, + encryptedSecretKeyWithKms, + envId: env.id, + isDisabled: false, + secretPath: secretPath || "/", + type + }); return { ...webhook, projectId, environment: env }; }; @@ -145,28 +137,11 @@ export const webhookServiceFactory = ({ 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(); - } + const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedUrl }).toString(); + const webhookSecretKey = webhook.encryptedSecretKeyWithKms + ? kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedSecretKeyWithKms }).toString() + : undefined; + try { await triggerWebhookRequest( { webhookUrl, webhookSecretKey }, @@ -213,17 +188,7 @@ export const webhookServiceFactory = ({ projectId }); return webhooks.map((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(); - } + const decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString(); return { ...w, url: decryptedUrl diff --git a/frontend/src/views/SecretOverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx b/frontend/src/views/SecretOverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx index a56ef6d9c..b99200930 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx @@ -22,7 +22,6 @@ enum ProjectUpgradeStatus { const formSchema = z.object({ isCLIChecked: z.literal(true), isOperatorChecked: z.literal(true), - doesKnowSnapshotLimit: z.literal(true), shouldCloseOpenApprovals: z.literal(true) }); @@ -143,22 +142,6 @@ export const SecretV2MigrationSection = () => { )} /> - ( - - Folders keep 10 latest snapshots due to migration time limit. - - )} - />