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

This commit is contained in:
=
2024-11-29 14:23:34 +05:30
parent 3e5a58eec4
commit 5f6870fda8
19 changed files with 254 additions and 198 deletions

View File

@@ -33,7 +33,18 @@ export async function up(knex: Knex): Promise<void> {
const webhooks = await knex(TableName.Webhook)
.where({})
.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));
const updatedWebhooks = await Promise.all(
@@ -55,7 +66,9 @@ export async function up(knex: Knex): Promise<void> {
tag: el.tag,
ciphertext: el.encryptedSecretKey
});
encryptedSecretKey = projectKmsService.encryptor({ plainText: Buffer.from(decyptedSecretKey, "utf8") });
encryptedSecretKey = projectKmsService.encryptor({
plainText: Buffer.from(decyptedSecretKey, "utf8")
}).cipherTextBlob;
}
const decryptedUrl =
@@ -68,7 +81,9 @@ export async function up(knex: Knex): Promise<void> {
})
: 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 };
})
);

View File

@@ -3,6 +3,7 @@ import { Knex } from "knex";
import { inMemoryKeyStore } from "@app/keystore/memory";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { selectAllTableCols } from "@app/lib/knex";
import { initLogger } from "@app/lib/logger";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { SecretKeyEncoding, TableName } from "../schemas";
@@ -21,6 +22,7 @@ export async function up(knex: Knex): Promise<void> {
});
}
await initLogger();
const envConfig = getMigrationEnvConfig();
const keyStore = inMemoryKeyStore();
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));
const updatedRotationData = await Promise.all(
secretRotations.map(async (el) => {
let projectKmsService = projectEncryptionRingBuffer.getItem(el.projectId);
secretRotations.map(async ({ projectId, ...el }) => {
let projectKmsService = projectEncryptionRingBuffer.getItem(projectId);
if (!projectKmsService) {
projectKmsService = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: el.projectId
projectId
});
projectEncryptionRingBuffer.push(el.projectId, projectKmsService);
projectEncryptionRingBuffer.push(projectId, projectKmsService);
}
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
? 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,
// 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,
// 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,
// 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
})
: null;
: "";
const encryptedRotationData = decryptedRotationData
? projectKmsService.encryptor({
plainText: Buffer.from(decryptedRotationData)
})
: null;
const encryptedRotationData = projectKmsService.encryptor({
plainText: Buffer.from(decryptedRotationData)
}).cipherTextBlob;
return { ...el, encryptedRotationData };
})
);

View File

@@ -3,6 +3,7 @@ import { Knex } from "knex";
import { inMemoryKeyStore } from "@app/keystore/memory";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { selectAllTableCols } from "@app/lib/knex";
import { initLogger } from "@app/lib/logger";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { SecretKeyEncoding, TableName } from "../schemas";
@@ -21,6 +22,7 @@ export async function up(knex: Knex): Promise<void> {
});
}
await initLogger();
const envConfig = getMigrationEnvConfig();
const keyStore = inMemoryKeyStore();
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));
const updatedDynamicSecrets = await Promise.all(
dynamicSecretRootCredentials.map(async (el) => {
let projectKmsService = projectEncryptionRingBuffer.getItem(el.projectId);
dynamicSecretRootCredentials.map(async ({ projectId, ...el }) => {
let projectKmsService = projectEncryptionRingBuffer.getItem(projectId);
if (!projectKmsService) {
projectKmsService = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: el.projectId
projectId
});
projectEncryptionRingBuffer.push(el.projectId, projectKmsService);
projectEncryptionRingBuffer.push(projectId, projectKmsService);
}
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
? 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,
// 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,
// 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,
// 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
})
: null;
: "";
const encryptedInput = projectKmsService.encryptor({
plainText: Buffer.from(decryptedInputData)
}).cipherTextBlob;
const encryptedInput = decryptedInputData
? projectKmsService.encryptor({
plainText: Buffer.from(decryptedInputData)
})
: null;
return { ...el, encryptedInput };
})
);

View File

@@ -19,6 +19,8 @@ type TDependencies = {
};
export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => {
// eslint-disable-next-line no-param-reassign
db.replicaNode = () => db;
const hsmModule = initializeHsmModule(envConfig);
hsmModule.initialize();
@@ -45,6 +47,7 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }
});
await hsmService.startService();
await kmsService.startService();
return { kmsService };
};

View File

@@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({
@@ -23,7 +25,8 @@ export const DynamicSecretsSchema = z.object({
status: z.string().nullable().optional(),
statusDetails: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
encryptedInput: zodBuffer
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

View File

@@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SecretRotationsSchema = z.object({
@@ -22,7 +24,8 @@ export const SecretRotationsSchema = z.object({
keyEncoding: z.string().nullable().optional(),
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
encryptedRotationData: zodBuffer
});
export type TSecretRotations = z.infer<typeof SecretRotationsSchema>;

View File

@@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const WebhooksSchema = z.object({
@@ -25,7 +27,9 @@ export const WebhooksSchema = z.object({
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(),
encryptedPassKey: zodBuffer.nullable().optional(),
encryptedUrl: zodBuffer
});
export type TWebhooks = z.infer<typeof WebhooksSchema>;

View File

@@ -37,11 +37,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"),
db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"),
db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"),
db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"),
db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"),
db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"),
db.ref("encryptedInput").withSchema(TableName.DynamicSecret).as("dynEncryptedInput"),
db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
@@ -59,11 +55,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
type: doc.dynType,
defaultTTL: doc.dynDefaultTTL,
maxTTL: doc.dynMaxTTL,
inputIV: doc.dynInputIV,
inputTag: doc.dynInputTag,
inputCiphertext: doc.dynInputCiphertext,
algorithm: doc.dynAlgorithm,
keyEncoding: doc.dynKeyEncoding,
encryptedInput: doc.dynEncryptedInput,
folderId: doc.dynFolderId,
status: doc.dynStatus,
statusDetails: doc.dynStatusDetails,

View File

@@ -1,8 +1,10 @@
import { SecretKeyEncoding } from "@app/db/schemas";
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
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 { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
@@ -14,6 +16,8 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
};
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
@@ -22,7 +26,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
queueService,
dynamicSecretDAL,
dynamicSecretProviders,
dynamicSecretLeaseDAL
dynamicSecretLeaseDAL,
kmsService,
folderDAL
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queue(
@@ -76,15 +82,21 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
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 selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object;
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
@@ -100,16 +112,22 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
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 });
if (dynamicSecretLeases.length) {
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object;
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));

View File

@@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
@@ -9,9 +9,10 @@ import {
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
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 { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -37,6 +38,7 @@ type TDynamicSecretLeaseServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
@@ -49,7 +51,8 @@ export const dynamicSecretLeaseServiceFactory = ({
permissionService,
dynamicSecretQueueService,
projectDAL,
licenseService
licenseService,
kmsService
}: TDynamicSecretLeaseServiceFactoryDep) => {
const create = async ({
environmentSlug,
@@ -104,13 +107,14 @@ export const dynamicSecretLeaseServiceFactory = ({
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
) as object;
const selectedTTL = ttl || dynamicSecretCfg.defaultTTL;
@@ -160,6 +164,11 @@ export const dynamicSecretLeaseServiceFactory = ({
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.dynamicSecret) {
throw new BadRequestError({
@@ -181,12 +190,7 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
) as object;
const selectedTTL = ttl || dynamicSecretCfg.defaultTTL;
@@ -240,6 +244,11 @@ export const dynamicSecretLeaseServiceFactory = ({
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);
if (!folder)
throw new NotFoundError({
@@ -253,12 +262,7 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
) as object;
const revokeResponse = await selectedProvider

View File

@@ -1,15 +1,16 @@
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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionDynamicSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
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 { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -42,6 +43,7 @@ type TDynamicSecretServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@@ -54,7 +56,8 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretProviders,
permissionService,
dynamicSecretQueueService,
projectDAL
projectDAL,
kmsService
}: TDynamicSecretServiceFactoryDep) => {
const create = async ({
path,
@@ -108,16 +111,15 @@ export const dynamicSecretServiceFactory = ({
const isConnected = await selectedProvider.validateConnection(provider.inputs);
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({
type: provider.type,
version: 1,
inputIV: encryptedInput.iv,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
maxTTL,
defaultTTL,
folderId: folder.id,
@@ -180,15 +182,15 @@ export const dynamicSecretServiceFactory = ({
if (existingDynamicSecret)
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 decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object;
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
@@ -196,13 +198,8 @@ export const dynamicSecretServiceFactory = ({
const isConnected = await selectedProvider.validateConnection(newInput);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
inputIV: encryptedInput.iv,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob,
maxTTL,
defaultTTL,
name: newName ?? name,
@@ -315,13 +312,13 @@ export const dynamicSecretServiceFactory = ({
if (!dynamicSecretCfg) {
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(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;

View File

@@ -5,13 +5,9 @@ import {
IAMClient
} 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 {
encryptSymmetric128BitHexKeyUTF8,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
@@ -135,20 +131,15 @@ export const secretRotationQueueFactory = ({
// deep copy
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
// in includes the inputs, the previous outputs
// internal mapping variables etc
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 decryptedData = secretManagerDecryptor({
cipherTextBlob: secretRotation.encryptedRotationData
}).toString();
const variables = JSON.parse(decryptedData) as TSecretRotationEncData;
// rotation set cycle
@@ -303,11 +294,9 @@ export const secretRotationQueueFactory = ({
outputs: newCredential.outputs,
internal: newCredential.internal
});
const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables));
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: secretRotation.projectId
});
const encryptedRotationData = secretManagerEncryptor({
plainText: Buffer.from(JSON.stringify(variables))
}).cipherTextBlob;
const numberOfSecretsRotated = rotationOutputs.length;
if (shouldUseSecretV2Bridge) {
@@ -323,11 +312,7 @@ export const secretRotationQueueFactory = ({
await secretRotationDAL.updateById(
rotationId,
{
encryptedData: encVarData.ciphertext,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
encryptedRotationData,
lastRotatedAt: new Date(),
statusMessage: "Rotated successfull",
status: "success"
@@ -371,11 +356,7 @@ export const secretRotationQueueFactory = ({
await secretRotationDAL.updateById(
rotationId,
{
encryptedData: encVarData.ciphertext,
encryptedDataIV: encVarData.iv,
encryptedDataTag: encVarData.tag,
keyEncoding: encVarData.encoding,
algorithm: encVarData.algorithm,
encryptedRotationData,
lastRotatedAt: new Date(),
statusMessage: "Rotated successfull",
status: "success"

View File

@@ -2,9 +2,11 @@ import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv";
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 { 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 { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
@@ -30,6 +32,7 @@ type TSecretRotationServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretRotationQueue: TSecretRotationQueueFactory;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TSecretRotationServiceFactory = ReturnType<typeof secretRotationServiceFactory>;
@@ -44,7 +47,8 @@ export const secretRotationServiceFactory = ({
folderDAL,
secretDAL,
projectBotService,
secretV2BridgeDAL
secretV2BridgeDAL,
kmsService
}: TSecretRotationServiceFactoryDep) => {
const getProviderTemplates = async ({
actor,
@@ -156,7 +160,11 @@ export const secretRotationServiceFactory = ({
inputs: formattedInputs,
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 doc = await secretRotationDAL.create(
{
@@ -164,11 +172,8 @@ export const secretRotationServiceFactory = ({
secretPath,
interval,
envId: folder.envId,
encryptedDataTag: encData.tag,
encryptedDataIV: encData.iv,
encryptedData: encData.ciphertext,
algorithm: encData.algorithm,
keyEncoding: encData.encoding
encryptedRotationData: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(unencryptedData)) })
.cipherTextBlob
},
tx
);

View File

@@ -973,7 +973,8 @@ export const registerRoutes = async (
permissionService,
webhookDAL,
projectEnvDAL,
projectDAL
projectDAL,
kmsService
});
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
@@ -1153,7 +1154,8 @@ export const registerRoutes = async (
secretDAL,
folderDAL,
projectBotService,
secretV2BridgeDAL
secretV2BridgeDAL,
kmsService
});
const integrationService = integrationServiceFactory({
@@ -1293,7 +1295,9 @@ export const registerRoutes = async (
queueService,
dynamicSecretLeaseDAL,
dynamicSecretProviders,
dynamicSecretDAL
dynamicSecretDAL,
folderDAL,
kmsService
});
const dynamicSecretService = dynamicSecretServiceFactory({
projectDAL,
@@ -1303,7 +1307,8 @@ export const registerRoutes = async (
dynamicSecretProviders,
folderDAL,
permissionService,
licenseService
licenseService,
kmsService
});
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
projectDAL,
@@ -1313,7 +1318,8 @@ export const registerRoutes = async (
dynamicSecretLeaseDAL,
dynamicSecretProviders,
folderDAL,
licenseService
licenseService,
kmsService
});
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
auditLogDAL,

View File

@@ -201,11 +201,7 @@ export const SanitizedRoleSchemaV1 = ProjectRolesSchema.extend({
});
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true,
inputTag: true,
inputCiphertext: true,
keyEncoding: true,
algorithm: true
encryptedInput: true
});
export const SanitizedAuditLogStreamSchema = z.object({

View File

@@ -1488,7 +1488,18 @@ export const secretQueueFactory = ({
});
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 {

View File

@@ -3,9 +3,8 @@ import crypto from "node:crypto";
import { AxiosError } from "axios";
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 { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
@@ -16,28 +15,14 @@ import { WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
export const decryptWebhookDetails = (webhook: TWebhooks) => {
const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook;
export const decryptWebhookDetails = (webhook: TWebhooks, decryptor: (value: Buffer) => string) => {
const { encryptedPassKey, encryptedUrl } = webhook;
const decryptedUrl = decryptor(encryptedUrl);
let decryptedSecretKey = "";
let decryptedUrl = url;
if (encryptedSecretKey) {
decryptedSecretKey = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: encryptedSecretKey,
iv: iv as string,
tag: tag as string
});
}
if (urlCipherText) {
decryptedUrl = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: urlCipherText,
iv: urlIV as string,
tag: urlTag as string
});
if (encryptedPassKey) {
decryptedSecretKey = decryptor(encryptedPassKey);
}
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 payload = { ...data, timestamp: Date.now() };
const { secretKey, url } = decryptWebhookDetails(webhook);
const { secretKey, url } = decryptWebhookDetails(webhook, decryptor);
if (secretKey) {
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">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">;
secretManagerDecryptor: (value: Buffer) => string;
};
// this is reusable function
@@ -134,7 +124,8 @@ export const fnTriggerWebhook = async ({
projectId,
webhookDAL,
projectEnvDAL,
projectDAL
projectDAL,
secretManagerDecryptor
}: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter(
@@ -148,6 +139,7 @@ export const fnTriggerWebhook = async ({
toBeTriggeredHooks.map((hook) =>
triggerWebhookRequest(
hook,
secretManagerDecryptor,
getWebhookPayload("secrets.modified", {
workspaceName: project.name,
workspaceId: projectId,

View File

@@ -3,9 +3,10 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, TWebhooksInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
@@ -23,6 +24,7 @@ type TWebhookServiceFactoryDep = {
projectEnvDAL: TProjectEnvDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
@@ -31,7 +33,8 @@ export const webhookServiceFactory = ({
webhookDAL,
projectEnvDAL,
permissionService,
projectDAL
projectDAL,
kmsService
}: TWebhookServiceFactoryDep) => {
const createWebhook = async ({
actor,
@@ -60,30 +63,20 @@ export const webhookServiceFactory = ({
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 = {
url: "", // deprecated - we are moving away from plaintext URLs
envId: env.id,
isDisabled: false,
secretPath: secretPath || "/",
type
type,
encryptedUrl: secretManagerEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob
};
if (webhookSecretKey) {
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey);
insertDoc.encryptedSecretKey = ciphertext;
insertDoc.iv = iv;
insertDoc.tag = tag;
insertDoc.algorithm = algorithm;
insertDoc.keyEncoding = encoding;
}
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;
insertDoc.encryptedPassKey = secretManagerEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob;
}
const webhook = await webhookDAL.create(insertDoc);
@@ -140,12 +133,17 @@ export const webhookServiceFactory = ({
});
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);
let webhookError: string | undefined;
try {
await triggerWebhookRequest(
webhook,
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
getWebhookPayload("test", {
workspaceName: project.name,
workspaceId: webhook.projectId,
@@ -185,8 +183,13 @@ export const webhookServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
return webhooks.map((w) => {
const { url } = decryptWebhookDetails(w);
const { url } = decryptWebhookDetails(w, (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString());
return {
...w,
url

View File

@@ -1,7 +1,8 @@
{
"ts-node": {
// Do not forget to `npm i -D tsconfig-paths`
"require": ["tsconfig-paths/register"]
"require": ["tsconfig-paths/register"],
"files": true
},
"compilerOptions": {
"target": "esnext",
@@ -19,6 +20,7 @@
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {