feat: updated codebase for new field changes made on project level enc migration

This commit is contained in:
=
2025-02-10 15:46:37 +05:30
parent 3e5a58eec4
commit 5f6870fda8
19 changed files with 254 additions and 198 deletions
@@ -33,7 +33,18 @@ export async function up(knex: Knex): Promise<void> {
const webhooks = await knex(TableName.Webhook) const webhooks = await knex(TableName.Webhook)
.where({}) .where({})
.leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`) .leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`)
.select("url", "encryptedSecretKey", "iv", "tag", "keyEncoding", "urlCipherText", "urlIV", "urlTag", "id", "envId") .select(
"url",
"encryptedSecretKey",
"iv",
"tag",
"keyEncoding",
"urlCipherText",
"urlIV",
"urlTag",
knex.ref("id").withSchema(TableName.Webhook),
"envId"
)
.select(knex.ref("projectId").withSchema(TableName.Environment)); .select(knex.ref("projectId").withSchema(TableName.Environment));
const updatedWebhooks = await Promise.all( const updatedWebhooks = await Promise.all(
@@ -55,7 +66,9 @@ export async function up(knex: Knex): Promise<void> {
tag: el.tag, tag: el.tag,
ciphertext: el.encryptedSecretKey ciphertext: el.encryptedSecretKey
}); });
encryptedSecretKey = projectKmsService.encryptor({ plainText: Buffer.from(decyptedSecretKey, "utf8") }); encryptedSecretKey = projectKmsService.encryptor({
plainText: Buffer.from(decyptedSecretKey, "utf8")
}).cipherTextBlob;
} }
const decryptedUrl = const decryptedUrl =
@@ -68,7 +81,9 @@ export async function up(knex: Knex): Promise<void> {
}) })
: null; : null;
const encryptedUrl = projectKmsService.encryptor({ plainText: Buffer.from(decryptedUrl || el.url) }); const encryptedUrl = projectKmsService.encryptor({
plainText: Buffer.from(decryptedUrl || el.url)
}).cipherTextBlob;
return { id: el.id, encryptedUrl, encryptedSecretKey, envId: el.envId }; return { id: el.id, encryptedUrl, encryptedSecretKey, envId: el.envId };
}) })
); );
@@ -3,6 +3,7 @@ import { Knex } from "knex";
import { inMemoryKeyStore } from "@app/keystore/memory"; import { inMemoryKeyStore } from "@app/keystore/memory";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { selectAllTableCols } from "@app/lib/knex"; import { selectAllTableCols } from "@app/lib/knex";
import { initLogger } from "@app/lib/logger";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { SecretKeyEncoding, TableName } from "../schemas"; import { SecretKeyEncoding, TableName } from "../schemas";
@@ -21,6 +22,7 @@ export async function up(knex: Knex): Promise<void> {
}); });
} }
await initLogger();
const envConfig = getMigrationEnvConfig(); const envConfig = getMigrationEnvConfig();
const keyStore = inMemoryKeyStore(); const keyStore = inMemoryKeyStore();
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
@@ -33,31 +35,39 @@ export async function up(knex: Knex): Promise<void> {
.select(knex.ref("projectId").withSchema(TableName.Environment)); .select(knex.ref("projectId").withSchema(TableName.Environment));
const updatedRotationData = await Promise.all( const updatedRotationData = await Promise.all(
secretRotations.map(async (el) => { secretRotations.map(async ({ projectId, ...el }) => {
let projectKmsService = projectEncryptionRingBuffer.getItem(el.projectId); let projectKmsService = projectEncryptionRingBuffer.getItem(projectId);
if (!projectKmsService) { if (!projectKmsService) {
projectKmsService = await kmsService.createCipherPairWithDataKey({ projectKmsService = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId: el.projectId projectId
}); });
projectEncryptionRingBuffer.push(el.projectId, projectKmsService); projectEncryptionRingBuffer.push(projectId, projectKmsService);
} }
const decryptedRotationData = const decryptedRotationData =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This will be removed in next cycle so ignore the ts missing error
el.encryptedDataTag && el.encryptedDataIV && el.encryptedData && el.keyEncoding el.encryptedDataTag && el.encryptedDataIV && el.encryptedData && el.keyEncoding
? infisicalSymmetricDecrypt({ ? infisicalSymmetricDecrypt({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This will be removed in next cycle so ignore the ts missing error
keyEncoding: el.keyEncoding as SecretKeyEncoding, keyEncoding: el.keyEncoding as SecretKeyEncoding,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This will be removed in next cycle so ignore the ts missing error
iv: el.encryptedDataIV, iv: el.encryptedDataIV,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This will be removed in next cycle so ignore the ts missing error
tag: el.encryptedDataTag, tag: el.encryptedDataTag,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This will be removed in next cycle so ignore the ts missing error
ciphertext: el.encryptedData ciphertext: el.encryptedData
}) })
: null; : "";
const encryptedRotationData = decryptedRotationData const encryptedRotationData = projectKmsService.encryptor({
? projectKmsService.encryptor({ plainText: Buffer.from(decryptedRotationData)
plainText: Buffer.from(decryptedRotationData) }).cipherTextBlob;
})
: null;
return { ...el, encryptedRotationData }; return { ...el, encryptedRotationData };
}) })
); );
@@ -3,6 +3,7 @@ import { Knex } from "knex";
import { inMemoryKeyStore } from "@app/keystore/memory"; import { inMemoryKeyStore } from "@app/keystore/memory";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { selectAllTableCols } from "@app/lib/knex"; import { selectAllTableCols } from "@app/lib/knex";
import { initLogger } from "@app/lib/logger";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { SecretKeyEncoding, TableName } from "../schemas"; import { SecretKeyEncoding, TableName } from "../schemas";
@@ -21,6 +22,7 @@ export async function up(knex: Knex): Promise<void> {
}); });
} }
await initLogger();
const envConfig = getMigrationEnvConfig(); const envConfig = getMigrationEnvConfig();
const keyStore = inMemoryKeyStore(); const keyStore = inMemoryKeyStore();
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
@@ -34,31 +36,40 @@ export async function up(knex: Knex): Promise<void> {
.select(knex.ref("projectId").withSchema(TableName.Environment)); .select(knex.ref("projectId").withSchema(TableName.Environment));
const updatedDynamicSecrets = await Promise.all( const updatedDynamicSecrets = await Promise.all(
dynamicSecretRootCredentials.map(async (el) => { dynamicSecretRootCredentials.map(async ({ projectId, ...el }) => {
let projectKmsService = projectEncryptionRingBuffer.getItem(el.projectId); let projectKmsService = projectEncryptionRingBuffer.getItem(projectId);
if (!projectKmsService) { if (!projectKmsService) {
projectKmsService = await kmsService.createCipherPairWithDataKey({ projectKmsService = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId: el.projectId projectId
}); });
projectEncryptionRingBuffer.push(el.projectId, projectKmsService); projectEncryptionRingBuffer.push(projectId, projectKmsService);
} }
const decryptedInputData = const decryptedInputData =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This will be removed in next cycle so ignore the ts missing error
el.inputIV && el.inputTag && el.inputCiphertext && el.keyEncoding el.inputIV && el.inputTag && el.inputCiphertext && el.keyEncoding
? infisicalSymmetricDecrypt({ ? infisicalSymmetricDecrypt({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This will be removed in next cycle so ignore the ts missing error
keyEncoding: el.keyEncoding as SecretKeyEncoding, keyEncoding: el.keyEncoding as SecretKeyEncoding,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This will be removed in next cycle so ignore the ts missing error
iv: el.inputIV, iv: el.inputIV,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This will be removed in next cycle so ignore the ts missing error
tag: el.inputTag, tag: el.inputTag,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This will be removed in next cycle so ignore the ts missing error
ciphertext: el.inputCiphertext ciphertext: el.inputCiphertext
}) })
: null; : "";
const encryptedInput = projectKmsService.encryptor({
plainText: Buffer.from(decryptedInputData)
}).cipherTextBlob;
const encryptedInput = decryptedInputData
? projectKmsService.encryptor({
plainText: Buffer.from(decryptedInputData)
})
: null;
return { ...el, encryptedInput }; return { ...el, encryptedInput };
}) })
); );
@@ -19,6 +19,8 @@ type TDependencies = {
}; };
export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => { export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => {
// eslint-disable-next-line no-param-reassign
db.replicaNode = () => db;
const hsmModule = initializeHsmModule(envConfig); const hsmModule = initializeHsmModule(envConfig);
hsmModule.initialize(); hsmModule.initialize();
@@ -45,6 +47,7 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }
}); });
await hsmService.startService(); await hsmService.startService();
await kmsService.startService();
return { kmsService }; return { kmsService };
}; };
+4 -1
View File
@@ -5,6 +5,8 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({ export const DynamicSecretsSchema = z.object({
@@ -23,7 +25,8 @@ export const DynamicSecretsSchema = z.object({
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(),
encryptedInput: zodBuffer
}); });
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>; export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
+4 -1
View File
@@ -5,6 +5,8 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const SecretRotationsSchema = z.object({ export const SecretRotationsSchema = z.object({
@@ -22,7 +24,8 @@ export const SecretRotationsSchema = z.object({
keyEncoding: z.string().nullable().optional(), keyEncoding: z.string().nullable().optional(),
envId: z.string().uuid(), envId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
encryptedRotationData: zodBuffer
}); });
export type TSecretRotations = z.infer<typeof SecretRotationsSchema>; export type TSecretRotations = z.infer<typeof SecretRotationsSchema>;
+5 -1
View File
@@ -5,6 +5,8 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const WebhooksSchema = z.object({ export const WebhooksSchema = z.object({
@@ -25,7 +27,9 @@ export const WebhooksSchema = z.object({
urlCipherText: z.string().nullable().optional(), urlCipherText: z.string().nullable().optional(),
urlIV: z.string().nullable().optional(), urlIV: z.string().nullable().optional(),
urlTag: z.string().nullable().optional(), urlTag: z.string().nullable().optional(),
type: z.string().default("general").nullable().optional() type: z.string().default("general").nullable().optional(),
encryptedPassKey: zodBuffer.nullable().optional(),
encryptedUrl: zodBuffer
}); });
export type TWebhooks = z.infer<typeof WebhooksSchema>; export type TWebhooks = z.infer<typeof WebhooksSchema>;
@@ -37,11 +37,7 @@ 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("encryptedInput").withSchema(TableName.DynamicSecret).as("dynEncryptedInput"),
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"),
@@ -59,11 +55,7 @@ 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, encryptedInput: doc.dynEncryptedInput,
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,8 +1,10 @@
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 { NotFoundError } 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 { KmsDataKey } from "@app/services/kms/kms-types";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
@@ -14,6 +16,8 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">; dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">; dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>; dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
}; };
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>; export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
@@ -22,7 +26,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
queueService, queueService,
dynamicSecretDAL, dynamicSecretDAL,
dynamicSecretProviders, dynamicSecretProviders,
dynamicSecretLeaseDAL dynamicSecretLeaseDAL,
kmsService,
folderDAL
}: TDynamicSecretLeaseQueueServiceFactoryDep) => { }: TDynamicSecretLeaseQueueServiceFactoryDep) => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queue( await queueService.queue(
@@ -76,15 +82,21 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId);
if (!folder)
throw new NotFoundError({
message: `Failed to find folder with ${dynamicSecretLease.dynamicSecret.folderId}`
});
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: folder.projectId
});
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({ secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object; ) as object;
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
@@ -100,16 +112,22 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting) if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
throw new DisableRotationErrors({ message: "Document not deleted" }); throw new DisableRotationErrors({ message: "Document not deleted" });
const folder = await folderDAL.findById(dynamicSecretCfg.folderId);
if (!folder)
throw new NotFoundError({
message: `Failed to find folder with ${dynamicSecretCfg.folderId}`
});
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: folder.projectId
});
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];
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({ secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object; ) as object;
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
@@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { ActionProjectType, SecretKeyEncoding } from "@app/db/schemas"; import { ActionProjectType } 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 { import {
@@ -9,9 +9,10 @@ import {
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } 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, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } 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 { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -37,6 +38,7 @@ type TDynamicSecretLeaseServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>; export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
@@ -49,7 +51,8 @@ export const dynamicSecretLeaseServiceFactory = ({
permissionService, permissionService,
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
licenseService licenseService,
kmsService
}: TDynamicSecretLeaseServiceFactoryDep) => { }: TDynamicSecretLeaseServiceFactoryDep) => {
const create = async ({ const create = async ({
environmentSlug, environmentSlug,
@@ -104,13 +107,14 @@ export const dynamicSecretLeaseServiceFactory = ({
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({ secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object; ) as object;
const selectedTTL = ttl || dynamicSecretCfg.defaultTTL; const selectedTTL = ttl || dynamicSecretCfg.defaultTTL;
@@ -160,6 +164,11 @@ export const dynamicSecretLeaseServiceFactory = ({
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
); );
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.dynamicSecret) { if (!plan?.dynamicSecret) {
throw new BadRequestError({ throw new BadRequestError({
@@ -181,12 +190,7 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({ secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object; ) as object;
const selectedTTL = ttl || dynamicSecretCfg.defaultTTL; const selectedTTL = ttl || dynamicSecretCfg.defaultTTL;
@@ -240,6 +244,11 @@ export const dynamicSecretLeaseServiceFactory = ({
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
); );
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
if (!folder) if (!folder)
throw new NotFoundError({ throw new NotFoundError({
@@ -253,12 +262,7 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({ secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object; ) as object;
const revokeResponse = await selectedProvider const revokeResponse = await selectedProvider
@@ -1,15 +1,16 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, SecretKeyEncoding } from "@app/db/schemas"; import { ActionProjectType } 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 { import {
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -42,6 +43,7 @@ type TDynamicSecretServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>; export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@@ -54,7 +56,8 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretProviders, dynamicSecretProviders,
permissionService, permissionService,
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL projectDAL,
kmsService
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep) => {
const create = async ({ const create = async ({
path, path,
@@ -108,16 +111,15 @@ export const dynamicSecretServiceFactory = ({
const isConnected = await selectedProvider.validateConnection(provider.inputs); const isConnected = await selectedProvider.validateConnection(provider.inputs);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretCfg = await dynamicSecretDAL.create({ const dynamicSecretCfg = await dynamicSecretDAL.create({
type: provider.type, type: provider.type,
version: 1, version: 1,
inputIV: encryptedInput.iv, encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL, maxTTL,
defaultTTL, defaultTTL,
folderId: folder.id, folderId: folder.id,
@@ -180,15 +182,15 @@ export const dynamicSecretServiceFactory = ({
if (existingDynamicSecret) if (existingDynamicSecret)
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" }); throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
} }
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({ secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object; ) as object;
const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const updatedInput = await selectedProvider.validateProviderInputs(newInput); const updatedInput = await selectedProvider.validateProviderInputs(newInput);
@@ -196,13 +198,8 @@ export const dynamicSecretServiceFactory = ({
const isConnected = await selectedProvider.validateConnection(newInput); const isConnected = await selectedProvider.validateConnection(newInput);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
inputIV: encryptedInput.iv, encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL, maxTTL,
defaultTTL, defaultTTL,
name: newName ?? name, name: newName ?? name,
@@ -315,13 +312,13 @@ export const dynamicSecretServiceFactory = ({
if (!dynamicSecretCfg) { if (!dynamicSecretCfg) {
throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` }); throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` });
} }
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedStoredInput = JSON.parse( const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({ secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object; ) as object;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
@@ -5,13 +5,9 @@ import {
IAMClient IAMClient
} from "@aws-sdk/client-iam"; } from "@aws-sdk/client-iam";
import { SecretKeyEncoding, SecretType } from "@app/db/schemas"; import { SecretType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption";
encryptSymmetric128BitHexKeyUTF8,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -135,20 +131,15 @@ export const secretRotationQueueFactory = ({
// deep copy // deep copy
const provider = JSON.parse(JSON.stringify(rotationProvider)) as TSecretRotationProviderTemplate; const provider = JSON.parse(JSON.stringify(rotationProvider)) as TSecretRotationProviderTemplate;
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: secretRotation.projectId
});
// now get the encrypted variable values const decryptedData = secretManagerDecryptor({
// in includes the inputs, the previous outputs cipherTextBlob: secretRotation.encryptedRotationData
// internal mapping variables etc }).toString();
const { encryptedDataTag, encryptedDataIV, encryptedData, keyEncoding } = secretRotation;
if (!encryptedDataTag || !encryptedDataIV || !encryptedData || !keyEncoding) {
throw new DisableRotationErrors({ message: "No inputs found" });
}
const decryptedData = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: encryptedData,
iv: encryptedDataIV,
tag: encryptedDataTag
});
const variables = JSON.parse(decryptedData) as TSecretRotationEncData; const variables = JSON.parse(decryptedData) as TSecretRotationEncData;
// rotation set cycle // rotation set cycle
@@ -303,11 +294,9 @@ export const secretRotationQueueFactory = ({
outputs: newCredential.outputs, outputs: newCredential.outputs,
internal: newCredential.internal internal: newCredential.internal
}); });
const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables)); const encryptedRotationData = secretManagerEncryptor({
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ plainText: Buffer.from(JSON.stringify(variables))
type: KmsDataKey.SecretManager, }).cipherTextBlob;
projectId: secretRotation.projectId
});
const numberOfSecretsRotated = rotationOutputs.length; const numberOfSecretsRotated = rotationOutputs.length;
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
@@ -323,11 +312,7 @@ export const secretRotationQueueFactory = ({
await secretRotationDAL.updateById( await secretRotationDAL.updateById(
rotationId, rotationId,
{ {
encryptedData: encVarData.ciphertext, encryptedRotationData,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
lastRotatedAt: new Date(), lastRotatedAt: new Date(),
statusMessage: "Rotated successfull", statusMessage: "Rotated successfull",
status: "success" status: "success"
@@ -371,11 +356,7 @@ export const secretRotationQueueFactory = ({
await secretRotationDAL.updateById( await secretRotationDAL.updateById(
rotationId, rotationId,
{ {
encryptedData: encVarData.ciphertext, encryptedRotationData,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
lastRotatedAt: new Date(), lastRotatedAt: new Date(),
statusMessage: "Rotated successfull", statusMessage: "Rotated successfull",
status: "success" status: "success"
@@ -2,9 +2,11 @@ import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv"; import Ajv from "ajv";
import { ActionProjectType, ProjectVersion, TableName } from "@app/db/schemas"; import { ActionProjectType, ProjectVersion, TableName } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
@@ -30,6 +32,7 @@ type TSecretRotationServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretRotationQueue: TSecretRotationQueueFactory; secretRotationQueue: TSecretRotationQueueFactory;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TSecretRotationServiceFactory = ReturnType<typeof secretRotationServiceFactory>; export type TSecretRotationServiceFactory = ReturnType<typeof secretRotationServiceFactory>;
@@ -44,7 +47,8 @@ export const secretRotationServiceFactory = ({
folderDAL, folderDAL,
secretDAL, secretDAL,
projectBotService, projectBotService,
secretV2BridgeDAL secretV2BridgeDAL,
kmsService
}: TSecretRotationServiceFactoryDep) => { }: TSecretRotationServiceFactoryDep) => {
const getProviderTemplates = async ({ const getProviderTemplates = async ({
actor, actor,
@@ -156,7 +160,11 @@ export const secretRotationServiceFactory = ({
inputs: formattedInputs, inputs: formattedInputs,
creds: [] creds: []
}; };
const encData = infisicalSymmetricEncypt(JSON.stringify(unencryptedData)); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const secretRotation = await secretRotationDAL.transaction(async (tx) => { const secretRotation = await secretRotationDAL.transaction(async (tx) => {
const doc = await secretRotationDAL.create( const doc = await secretRotationDAL.create(
{ {
@@ -164,11 +172,8 @@ export const secretRotationServiceFactory = ({
secretPath, secretPath,
interval, interval,
envId: folder.envId, envId: folder.envId,
encryptedDataTag: encData.tag, encryptedRotationData: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(unencryptedData)) })
encryptedDataIV: encData.iv, .cipherTextBlob
encryptedData: encData.ciphertext,
algorithm: encData.algorithm,
keyEncoding: encData.encoding
}, },
tx tx
); );
+11 -5
View File
@@ -973,7 +973,8 @@ export const registerRoutes = async (
permissionService, permissionService,
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
projectDAL projectDAL,
kmsService
}); });
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
@@ -1153,7 +1154,8 @@ export const registerRoutes = async (
secretDAL, secretDAL,
folderDAL, folderDAL,
projectBotService, projectBotService,
secretV2BridgeDAL secretV2BridgeDAL,
kmsService
}); });
const integrationService = integrationServiceFactory({ const integrationService = integrationServiceFactory({
@@ -1293,7 +1295,9 @@ export const registerRoutes = async (
queueService, queueService,
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
dynamicSecretProviders, dynamicSecretProviders,
dynamicSecretDAL dynamicSecretDAL,
folderDAL,
kmsService
}); });
const dynamicSecretService = dynamicSecretServiceFactory({ const dynamicSecretService = dynamicSecretServiceFactory({
projectDAL, projectDAL,
@@ -1303,7 +1307,8 @@ export const registerRoutes = async (
dynamicSecretProviders, dynamicSecretProviders,
folderDAL, folderDAL,
permissionService, permissionService,
licenseService licenseService,
kmsService
}); });
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
projectDAL, projectDAL,
@@ -1313,7 +1318,8 @@ export const registerRoutes = async (
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
dynamicSecretProviders, dynamicSecretProviders,
folderDAL, folderDAL,
licenseService licenseService,
kmsService
}); });
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
auditLogDAL, auditLogDAL,
@@ -201,11 +201,7 @@ export const SanitizedRoleSchemaV1 = ProjectRolesSchema.extend({
}); });
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true, encryptedInput: true
inputTag: true,
inputCiphertext: true,
keyEncoding: true,
algorithm: true
}); });
export const SanitizedAuditLogStreamSchema = z.object({ export const SanitizedAuditLogStreamSchema = z.object({
+12 -1
View File
@@ -1488,7 +1488,18 @@ export const secretQueueFactory = ({
}); });
queueService.start(QueueName.SecretWebhook, async (job) => { queueService.start(QueueName.SecretWebhook, async (job) => {
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL }); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: job.data.projectId
});
await fnTriggerWebhook({
...job.data,
projectEnvDAL,
webhookDAL,
projectDAL,
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
});
}); });
return { return {
+17 -25
View File
@@ -3,9 +3,8 @@ import crypto from "node:crypto";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import picomatch from "picomatch"; import picomatch from "picomatch";
import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas"; import { TWebhooks } 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 { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -16,28 +15,14 @@ import { WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
export const decryptWebhookDetails = (webhook: TWebhooks) => { export const decryptWebhookDetails = (webhook: TWebhooks, decryptor: (value: Buffer) => string) => {
const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook; const { encryptedPassKey, encryptedUrl } = webhook;
const decryptedUrl = decryptor(encryptedUrl);
let decryptedSecretKey = ""; let decryptedSecretKey = "";
let decryptedUrl = url; if (encryptedPassKey) {
decryptedSecretKey = decryptor(encryptedPassKey);
if (encryptedSecretKey) {
decryptedSecretKey = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: encryptedSecretKey,
iv: iv as string,
tag: tag as string
});
}
if (urlCipherText) {
decryptedUrl = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: urlCipherText,
iv: urlIV as string,
tag: urlTag as string
});
} }
return { return {
@@ -46,10 +31,14 @@ export const decryptWebhookDetails = (webhook: TWebhooks) => {
}; };
}; };
export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record<string, unknown>) => { export const triggerWebhookRequest = async (
webhook: TWebhooks,
decryptor: (value: Buffer) => string,
data: Record<string, unknown>
) => {
const headers: Record<string, string> = {}; const headers: Record<string, string> = {};
const payload = { ...data, timestamp: Date.now() }; const payload = { ...data, timestamp: Date.now() };
const { secretKey, url } = decryptWebhookDetails(webhook); const { secretKey, url } = decryptWebhookDetails(webhook, decryptor);
if (secretKey) { if (secretKey) {
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex"); const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
@@ -124,6 +113,7 @@ export type TFnTriggerWebhookDTO = {
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">; webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
secretManagerDecryptor: (value: Buffer) => string;
}; };
// this is reusable function // this is reusable function
@@ -134,7 +124,8 @@ export const fnTriggerWebhook = async ({
projectId, projectId,
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
projectDAL projectDAL,
secretManagerDecryptor
}: TFnTriggerWebhookDTO) => { }: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter( const toBeTriggeredHooks = webhooks.filter(
@@ -148,6 +139,7 @@ export const fnTriggerWebhook = async ({
toBeTriggeredHooks.map((hook) => toBeTriggeredHooks.map((hook) =>
triggerWebhookRequest( triggerWebhookRequest(
hook, hook,
secretManagerDecryptor,
getWebhookPayload("secrets.modified", { getWebhookPayload("secrets.modified", {
workspaceName: project.name, workspaceName: project.name,
workspaceId: projectId, workspaceId: projectId,
+23 -20
View File
@@ -3,9 +3,10 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, TWebhooksInsert } from "@app/db/schemas"; import { ActionProjectType, 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 { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal"; import { TWebhookDALFactory } from "./webhook-dal";
@@ -23,6 +24,7 @@ type TWebhookServiceFactoryDep = {
projectEnvDAL: TProjectEnvDALFactory; projectEnvDAL: TProjectEnvDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>; export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
@@ -31,7 +33,8 @@ export const webhookServiceFactory = ({
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
permissionService, permissionService,
projectDAL projectDAL,
kmsService
}: TWebhookServiceFactoryDep) => { }: TWebhookServiceFactoryDep) => {
const createWebhook = async ({ const createWebhook = async ({
actor, actor,
@@ -60,30 +63,20 @@ export const webhookServiceFactory = ({
message: `Environment with slug '${environment}' in project with ID '${projectId}' not found` message: `Environment with slug '${environment}' in project with ID '${projectId}' not found`
}); });
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const insertDoc: TWebhooksInsert = { const insertDoc: TWebhooksInsert = {
url: "", // deprecated - we are moving away from plaintext URLs
envId: env.id, envId: env.id,
isDisabled: false, isDisabled: false,
secretPath: secretPath || "/", secretPath: secretPath || "/",
type type,
encryptedUrl: secretManagerEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob
}; };
if (webhookSecretKey) { if (webhookSecretKey) {
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey); insertDoc.encryptedPassKey = secretManagerEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob;
insertDoc.encryptedSecretKey = ciphertext;
insertDoc.iv = iv;
insertDoc.tag = tag;
insertDoc.algorithm = algorithm;
insertDoc.keyEncoding = encoding;
}
if (webhookUrl) {
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl);
insertDoc.urlCipherText = ciphertext;
insertDoc.urlIV = iv;
insertDoc.urlTag = tag;
insertDoc.algorithm = algorithm;
insertDoc.keyEncoding = encoding;
} }
const webhook = await webhookDAL.create(insertDoc); const webhook = await webhookDAL.create(insertDoc);
@@ -140,12 +133,17 @@ export const webhookServiceFactory = ({
}); });
const project = await projectDAL.findById(webhook.projectId); const project = await projectDAL.findById(webhook.projectId);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: project.id
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
let webhookError: string | undefined; let webhookError: string | undefined;
try { try {
await triggerWebhookRequest( await triggerWebhookRequest(
webhook, webhook,
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
getWebhookPayload("test", { getWebhookPayload("test", {
workspaceName: project.name, workspaceName: project.name,
workspaceId: webhook.projectId, workspaceId: webhook.projectId,
@@ -185,8 +183,13 @@ export const webhookServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
return webhooks.map((w) => { return webhooks.map((w) => {
const { url } = decryptWebhookDetails(w); const { url } = decryptWebhookDetails(w, (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString());
return { return {
...w, ...w,
url url
+3 -1
View File
@@ -1,7 +1,8 @@
{ {
"ts-node": { "ts-node": {
// Do not forget to `npm i -D tsconfig-paths` // Do not forget to `npm i -D tsconfig-paths`
"require": ["tsconfig-paths/register"] "require": ["tsconfig-paths/register"],
"files": true
}, },
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "esnext",
@@ -19,6 +20,7 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"moduleResolution": "Node", "moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"skipLibCheck": true, "skipLibCheck": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {