mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 03:28:15 +00:00
feat: changed webhook and dynamic secret change to migration mode, resolved snapshot deletion issue in update
This commit is contained in:
@@ -1,7 +1,178 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import { Knex } from "knex";
|
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 { 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<string, Awaited<ReturnType<typeof getSecretManagerDataKey>>["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<string, Awaited<ReturnType<typeof getSecretManagerDataKey>>["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<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2);
|
const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2);
|
||||||
@@ -145,29 +316,11 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (await knex.schema.hasTable(TableName.Webhook)) {
|
if (await knex.schema.hasTable(TableName.Webhook)) {
|
||||||
const hasEncryptedWebhookSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
|
await backfillWebhooks(knex);
|
||||||
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)) {
|
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||||
const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV");
|
await backfillDynamicSecretConfigs(knex);
|
||||||
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");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,16 +359,46 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
if (await knex.schema.hasTable(TableName.Webhook)) {
|
if (await knex.schema.hasTable(TableName.Webhook)) {
|
||||||
const hasEncryptedWebhookSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
|
const hasEncryptedWebhookSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
|
||||||
const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl");
|
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) => {
|
await knex.schema.alterTable(TableName.Webhook, (t) => {
|
||||||
if (hasEncryptedWebhookSecretKey) t.dropColumn("encryptedSecretKeyWithKms");
|
if (hasEncryptedWebhookSecretKey) t.dropColumn("encryptedSecretKeyWithKms");
|
||||||
if (hasEncryptedWebhookUrl) t.dropColumn("encryptedUrl");
|
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)) {
|
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||||
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
|
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) => {
|
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||||
if (hasEncryptedConfig) t.dropColumn("encryptedConfig");
|
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");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -16,17 +16,12 @@ export const DynamicSecretsSchema = z.object({
|
|||||||
type: z.string(),
|
type: z.string(),
|
||||||
defaultTTL: z.string(),
|
defaultTTL: z.string(),
|
||||||
maxTTL: z.string().nullable().optional(),
|
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(),
|
folderId: z.string().uuid(),
|
||||||
status: z.string().nullable().optional(),
|
status: z.string().nullable().optional(),
|
||||||
statusDetails: z.string().nullable().optional(),
|
statusDetails: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
encryptedConfig: zodBuffer.nullable().optional()
|
encryptedConfig: zodBuffer
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||||
|
|||||||
@@ -12,24 +12,15 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const WebhooksSchema = z.object({
|
export const WebhooksSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
secretPath: z.string().default("/"),
|
secretPath: z.string().default("/"),
|
||||||
url: z.string(),
|
|
||||||
lastStatus: z.string().nullable().optional(),
|
lastStatus: z.string().nullable().optional(),
|
||||||
lastRunErrorMessage: z.string().nullable().optional(),
|
lastRunErrorMessage: z.string().nullable().optional(),
|
||||||
isDisabled: z.boolean().default(false),
|
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(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
envId: z.string().uuid(),
|
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(),
|
type: z.string().default("general").nullable().optional(),
|
||||||
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
|
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
|
||||||
encryptedUrl: zodBuffer.nullable().optional()
|
encryptedUrl: zodBuffer
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TWebhooks = z.infer<typeof WebhooksSchema>;
|
export type TWebhooks = z.infer<typeof WebhooksSchema>;
|
||||||
|
|||||||
@@ -40,11 +40,6 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
|
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
|
||||||
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
|
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
|
||||||
db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"),
|
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("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
|
||||||
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
|
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
|
||||||
db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
|
db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
|
||||||
@@ -63,11 +58,6 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
|||||||
type: doc.dynType,
|
type: doc.dynType,
|
||||||
defaultTTL: doc.dynDefaultTTL,
|
defaultTTL: doc.dynDefaultTTL,
|
||||||
maxTTL: doc.dynMaxTTL,
|
maxTTL: doc.dynMaxTTL,
|
||||||
inputIV: doc.dynInputIV,
|
|
||||||
inputTag: doc.dynInputTag,
|
|
||||||
inputCiphertext: doc.dynInputCiphertext,
|
|
||||||
algorithm: doc.dynAlgorithm,
|
|
||||||
keyEncoding: doc.dynKeyEncoding,
|
|
||||||
folderId: doc.dynFolderId,
|
folderId: doc.dynFolderId,
|
||||||
status: doc.dynStatus,
|
status: doc.dynStatus,
|
||||||
statusDetails: doc.dynStatusDetails,
|
statusDetails: doc.dynStatusDetails,
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
|
||||||
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
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 { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
@@ -94,27 +91,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
let dynamicSecretInputConfig = "";
|
const dynamicSecretInputConfig = secretManagerDecryptor({
|
||||||
if (
|
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||||
dynamicSecretCfg.keyEncoding &&
|
}).toString();
|
||||||
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 selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
||||||
|
|
||||||
@@ -142,27 +121,10 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
|
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
|
||||||
if (dynamicSecretLeases.length) {
|
if (dynamicSecretLeases.length) {
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
let dynamicSecretInputConfig = "";
|
|
||||||
|
|
||||||
if (
|
const dynamicSecretInputConfig = secretManagerDecryptor({
|
||||||
dynamicSecretCfg.keyEncoding &&
|
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||||
dynamicSecretCfg.inputCiphertext &&
|
}).toString();
|
||||||
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 decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
||||||
|
|
||||||
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TDynamicSecrets } from "@app/db/schemas";
|
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
@@ -52,29 +50,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
licenseService,
|
licenseService,
|
||||||
kmsService
|
kmsService
|
||||||
}: TDynamicSecretLeaseServiceFactoryDep) => {
|
}: 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 ({
|
const create = async ({
|
||||||
environmentSlug,
|
environmentSlug,
|
||||||
path,
|
path,
|
||||||
@@ -125,9 +100,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) =>
|
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
|
||||||
kmsDecryptor({ cipherTextBlob: value }).toString()
|
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
|
||||||
);
|
|
||||||
|
|
||||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||||
const { maxTTL } = dynamicSecretCfg;
|
const { maxTTL } = dynamicSecretCfg;
|
||||||
@@ -194,9 +168,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) =>
|
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
|
||||||
kmsDecryptor({ cipherTextBlob: value }).toString()
|
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
|
||||||
);
|
|
||||||
|
|
||||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||||
const { maxTTL } = dynamicSecretCfg;
|
const { maxTTL } = dynamicSecretCfg;
|
||||||
@@ -260,9 +233,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) =>
|
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
|
||||||
kmsDecryptor({ cipherTextBlob: value }).toString()
|
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
|
||||||
);
|
|
||||||
|
|
||||||
const revokeResponse = await selectedProvider
|
const revokeResponse = await selectedProvider
|
||||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
|
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
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" });
|
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let dynamicSecretInputConfig = "";
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
|
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
|
||||||
await kmsService.createCipherPairWithDataKey({
|
await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
if (
|
const dynamicSecretInputConfig = secretManagerDecryptor({
|
||||||
dynamicSecretCfg.keyEncoding &&
|
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||||
dynamicSecretCfg.inputCiphertext &&
|
}).toString();
|
||||||
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 decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
||||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||||
@@ -208,11 +189,6 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
|
|
||||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
||||||
encryptedConfig,
|
encryptedConfig,
|
||||||
inputIV: null,
|
|
||||||
inputTag: null,
|
|
||||||
keyEncoding: null,
|
|
||||||
algorithm: null,
|
|
||||||
inputCiphertext: null,
|
|
||||||
maxTTL,
|
maxTTL,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
name: newName ?? name,
|
name: newName ?? name,
|
||||||
@@ -318,26 +294,9 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
let dynamicSecretInputConfig = "";
|
const dynamicSecretInputConfig = secretManagerDecryptor({
|
||||||
if (
|
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||||
dynamicSecretCfg.keyEncoding &&
|
}).toString();
|
||||||
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 decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
|
|||||||
@@ -791,10 +791,19 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const deleteSnapshotsAboveLimit = async (folderId: string, n = 15, tx?: Knex) => {
|
const deleteSnapshotsAboveLimit = async (folderId: string, n = 15, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const query = await (tx || db.replicaNode())(TableName.Snapshot)
|
const query = await (tx || db)
|
||||||
.orderBy(`${TableName.Snapshot}.createdAt`, "desc")
|
.with("to_delete", (qb) => {
|
||||||
.where(`${TableName.Snapshot}.folderId`, folderId)
|
void qb
|
||||||
.offset(n)
|
.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();
|
.delete();
|
||||||
return query;
|
return query;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -226,8 +226,9 @@ export const infisicalSymmetricDecrypt = <T = string>({
|
|||||||
keyEncoding: SecretKeyEncoding;
|
keyEncoding: SecretKeyEncoding;
|
||||||
}) => {
|
}) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
|
// the or gate is used used in migration
|
||||||
const encryptionKey = appCfg.ENCRYPTION_KEY;
|
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) {
|
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
|
||||||
const data = decryptSymmetric({ key: rootEncryptionKey, iv, tag, ciphertext });
|
const data = decryptSymmetric({ key: rootEncryptionKey, iv, tag, ciphertext });
|
||||||
return data as T;
|
return data as T;
|
||||||
|
|||||||
@@ -725,8 +725,7 @@ export const registerRoutes = async (
|
|||||||
integrationAuthDAL,
|
integrationAuthDAL,
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
snapshotSecretV2BridgeDAL,
|
snapshotSecretV2BridgeDAL,
|
||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL
|
||||||
dynamicSecretDAL
|
|
||||||
});
|
});
|
||||||
const secretImportService = secretImportServiceFactory({
|
const secretImportService = secretImportServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
|
|||||||
@@ -129,11 +129,7 @@ export const SanitizedRoleSchema = ProjectRolesSchema.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||||
inputIV: true,
|
encryptedConfig: true
|
||||||
inputTag: true,
|
|
||||||
inputCiphertext: true,
|
|
||||||
keyEncoding: true,
|
|
||||||
algorithm: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SanitizedAuditLogStreamSchema = z.object({
|
export const SanitizedAuditLogStreamSchema = z.object({
|
||||||
|
|||||||
@@ -8,25 +8,24 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
|||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { WebhookType } from "@app/services/webhook/webhook-types";
|
import { WebhookType } from "@app/services/webhook/webhook-types";
|
||||||
|
|
||||||
export const sanitizedWebhookSchema = WebhooksSchema.omit({
|
export const sanitizedWebhookSchema = WebhooksSchema.pick({
|
||||||
encryptedSecretKey: true,
|
id: true,
|
||||||
iv: true,
|
secretPath: true,
|
||||||
tag: true,
|
lastStatus: true,
|
||||||
algorithm: true,
|
lastRunErrorMessage: true,
|
||||||
keyEncoding: true,
|
isDisabled: true,
|
||||||
urlCipherText: true,
|
createdAt: true,
|
||||||
urlIV: true,
|
updatedAt: true,
|
||||||
urlTag: true
|
envId: true,
|
||||||
}).merge(
|
type: true
|
||||||
z.object({
|
}).extend({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
environment: z.object({
|
environment: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z.string()
|
slug: z.string()
|
||||||
})
|
|
||||||
})
|
})
|
||||||
);
|
});
|
||||||
|
|
||||||
export const registerWebhookRouter = async (server: FastifyZodProvider) => {
|
export const registerWebhookRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -228,7 +227,7 @@ export const registerWebhookRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
webhooks: sanitizedWebhookSchema.array()
|
webhooks: sanitizedWebhookSchema.extend({ url: z.string() }).array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -575,6 +575,7 @@ export const kmsServiceFactory = ({
|
|||||||
|
|
||||||
// by keeping the decrypted data key in inner scope
|
// by keeping the decrypted data key in inner scope
|
||||||
// none of the entities outside can interact directly or expose the data key
|
// 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 createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => {
|
||||||
const dataKey = await $getDataKey(encryptionContext);
|
const dataKey = await $getDataKey(encryptionContext);
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||||
@@ -753,6 +754,7 @@ export const kmsServiceFactory = ({
|
|||||||
return { id, slug, orgId, isExternal };
|
return { id, slug, orgId, isExternal };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// akhilmhdh: a copy of this is made in migrations/utils/kms
|
||||||
const startService = async () => {
|
const startService = async () => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
// This will switch to a seal process and HMS flow in future
|
// This will switch to a seal process and HMS flow in future
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ export const getBotKeyFnFactory = (
|
|||||||
if (!bot || !bot.isActive || !bot.encryptedProjectKey || !bot.encryptedProjectKeyNonce) {
|
if (!bot || !bot.isActive || !bot.encryptedProjectKey || !bot.encryptedProjectKeyNonce) {
|
||||||
// trying to set bot automatically
|
// trying to set bot automatically
|
||||||
const projectV1Keys = await projectBotDAL.findProjectUserWorkspaceKey(projectId);
|
const projectV1Keys = await projectBotDAL.findProjectUserWorkspaceKey(projectId);
|
||||||
if (!projectV1Keys)
|
if (!projectV1Keys) throw new BadRequestError({ message: "Bot not found. Please ask admin user to login" });
|
||||||
throw new BadRequestError({ message: "Bot not found. [no-private-key]. Please ask admin user to login" });
|
|
||||||
|
|
||||||
let userPrivateKey = "";
|
let userPrivateKey = "";
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import {
|
import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas";
|
||||||
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 { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
||||||
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||||
import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|
||||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy, isSamePath, unique } from "@app/lib/fn";
|
import { groupBy, isSamePath, unique } from "@app/lib/fn";
|
||||||
@@ -68,7 +60,7 @@ type TSecretQueueFactoryDep = {
|
|||||||
folderDAL: TSecretFolderDALFactory;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
secretDAL: TSecretDALFactory;
|
secretDAL: TSecretDALFactory;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "find" | "transaction" | "update" | "bulkUpdate" | "upsert">;
|
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectBotDAL: TProjectBotDALFactory;
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
@@ -87,7 +79,6 @@ type TSecretQueueFactoryDep = {
|
|||||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
|
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
|
||||||
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
|
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
|
||||||
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">;
|
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">;
|
||||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "find" | "upsert">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecrets = {
|
export type TGetSecrets = {
|
||||||
@@ -131,8 +122,7 @@ export const secretQueueFactory = ({
|
|||||||
secretRotationDAL,
|
secretRotationDAL,
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
snapshotSecretV2BridgeDAL,
|
snapshotSecretV2BridgeDAL,
|
||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL
|
||||||
dynamicSecretDAL
|
|
||||||
}: TSecretQueueFactoryDep) => {
|
}: TSecretQueueFactoryDep) => {
|
||||||
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -891,38 +881,7 @@ export const secretQueueFactory = ({
|
|||||||
await secretV2BridgeDAL.upsertSecretReferences(secretReferences, tx);
|
await secretV2BridgeDAL.upsertSecretReferences(secretReferences, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dynamicSecrets = await dynamicSecretDAL.find({ folderId }, { tx });
|
const SNAPSHOT_BATCH_SIZE = 10;
|
||||||
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 snapshots = await snapshotDAL.findNSecretV1SnapshotByFolderId(folderId, SNAPSHOT_BATCH_SIZE, tx);
|
||||||
const projectV3SecretVersionsGroupById: Record<string, TSecretVersionsV2> = {};
|
const projectV3SecretVersionsGroupById: Record<string, TSecretVersionsV2> = {};
|
||||||
const projectV3SecretVersionTags: { secret_versions_v2Id: string; secret_tagsId: string }[] = [];
|
const projectV3SecretVersionTags: { secret_versions_v2Id: string; secret_tagsId: string }[] = [];
|
||||||
@@ -1151,71 +1110,6 @@ export const secretQueueFactory = ({
|
|||||||
tx
|
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
|
* 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
|
* Thus doesn't make sense for rest to be there
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import crypto from "node:crypto";
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
@@ -127,28 +125,10 @@ export const fnTriggerWebhook = async ({
|
|||||||
|
|
||||||
const webhooksTriggered = await Promise.allSettled(
|
const webhooksTriggered = await Promise.allSettled(
|
||||||
toBeTriggeredHooks.map((hook) => {
|
toBeTriggeredHooks.map((hook) => {
|
||||||
let webhookUrl = hook.url;
|
const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString();
|
||||||
let webhookSecretKey;
|
const webhookSecretKey = hook.encryptedSecretKeyWithKms
|
||||||
if (hook.urlTag && hook.urlCipherText && hook.urlIV) {
|
? kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString()
|
||||||
webhookUrl = infisicalSymmetricDecrypt({
|
: undefined;
|
||||||
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(
|
return triggerWebhookRequest(
|
||||||
{ webhookUrl, webhookSecretKey },
|
{ webhookUrl, webhookSecretKey },
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TWebhooksInsert } from "@app/db/schemas";
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
@@ -60,34 +58,28 @@ export const webhookServiceFactory = ({
|
|||||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||||
if (!env) throw new BadRequestError({ message: "Env not found" });
|
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({
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
projectId,
|
projectId,
|
||||||
type: KmsDataKey.SecretManager
|
type: KmsDataKey.SecretManager
|
||||||
});
|
});
|
||||||
if (webhookSecretKey) {
|
|
||||||
const encryptedSecretKeyWithKms = secretManagerEncryptor({
|
|
||||||
plainText: Buffer.from(webhookSecretKey)
|
|
||||||
}).cipherTextBlob;
|
|
||||||
insertDoc.encryptedSecretKeyWithKms = encryptedSecretKeyWithKms;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webhookUrl) {
|
const encryptedSecretKeyWithKms = webhookSecretKey
|
||||||
const encryptedUrl = secretManagerEncryptor({
|
? secretManagerEncryptor({
|
||||||
plainText: Buffer.from(webhookUrl)
|
plainText: Buffer.from(webhookSecretKey)
|
||||||
}).cipherTextBlob;
|
}).cipherTextBlob
|
||||||
|
: null;
|
||||||
|
const encryptedUrl = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(webhookUrl)
|
||||||
|
}).cipherTextBlob;
|
||||||
|
|
||||||
insertDoc.encryptedUrl = encryptedUrl;
|
const webhook = await webhookDAL.create({
|
||||||
}
|
encryptedUrl,
|
||||||
|
encryptedSecretKeyWithKms,
|
||||||
const webhook = await webhookDAL.create(insertDoc);
|
envId: env.id,
|
||||||
|
isDisabled: false,
|
||||||
|
secretPath: secretPath || "/",
|
||||||
|
type
|
||||||
|
});
|
||||||
return { ...webhook, projectId, environment: env };
|
return { ...webhook, projectId, environment: env };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,28 +137,11 @@ export const webhookServiceFactory = ({
|
|||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
type: KmsDataKey.SecretManager
|
type: KmsDataKey.SecretManager
|
||||||
});
|
});
|
||||||
let webhookUrl = webhook.url;
|
const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedUrl }).toString();
|
||||||
let webhookSecretKey;
|
const webhookSecretKey = webhook.encryptedSecretKeyWithKms
|
||||||
if (webhook.urlTag && webhook.urlCipherText && webhook.urlIV) {
|
? kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedSecretKeyWithKms }).toString()
|
||||||
webhookUrl = infisicalSymmetricDecrypt({
|
: undefined;
|
||||||
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 {
|
try {
|
||||||
await triggerWebhookRequest(
|
await triggerWebhookRequest(
|
||||||
{ webhookUrl, webhookSecretKey },
|
{ webhookUrl, webhookSecretKey },
|
||||||
@@ -213,17 +188,7 @@ export const webhookServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
return webhooks.map((w) => {
|
return webhooks.map((w) => {
|
||||||
let decryptedUrl = w.url;
|
const decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString();
|
||||||
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 {
|
return {
|
||||||
...w,
|
...w,
|
||||||
url: decryptedUrl
|
url: decryptedUrl
|
||||||
|
|||||||
-17
@@ -22,7 +22,6 @@ enum ProjectUpgradeStatus {
|
|||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
isCLIChecked: z.literal(true),
|
isCLIChecked: z.literal(true),
|
||||||
isOperatorChecked: z.literal(true),
|
isOperatorChecked: z.literal(true),
|
||||||
doesKnowSnapshotLimit: z.literal(true),
|
|
||||||
shouldCloseOpenApprovals: z.literal(true)
|
shouldCloseOpenApprovals: z.literal(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,22 +142,6 @@ export const SecretV2MigrationSection = () => {
|
|||||||
</Checkbox>
|
</Checkbox>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="doesKnowSnapshotLimit"
|
|
||||||
defaultValue={false}
|
|
||||||
render={({ field: { onBlur, value, onChange }, fieldState: { error } }) => (
|
|
||||||
<Checkbox
|
|
||||||
id="is-snapshot-checked"
|
|
||||||
isChecked={value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
onBlur={onBlur}
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
>
|
|
||||||
Folders keep 10 latest snapshots due to migration time limit.
|
|
||||||
</Checkbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="shouldCloseOpenApprovals"
|
name="shouldCloseOpenApprovals"
|
||||||
|
|||||||
Reference in New Issue
Block a user