feat: updated kms service to return only kms details and some more minor changes

This commit is contained in:
=
2024-07-30 23:19:06 +05:30
parent f7ef86eb11
commit 3e0ae5765f
11 changed files with 200 additions and 142 deletions
+7 -3
View File
@@ -7,6 +7,7 @@ import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { KmsType } from "@app/services/kms/kms-types";
export const registerProjectRouter = async (server: FastifyZodProvider) => { export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -194,7 +195,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const kmsKeys = await server.services.project.getProjectKmsKeys({ const kmsKey = await server.services.project.getProjectKmsKeys({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -202,7 +203,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId projectId: req.params.workspaceId
}); });
return kmsKeys; return kmsKey;
} }
}); });
@@ -217,7 +218,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
workspaceId: z.string().trim() workspaceId: z.string().trim()
}), }),
body: z.object({ body: z.object({
secretManagerKmsKeyId: z.string() kms: z.discriminatedUnion("type", [
z.object({ type: z.literal(KmsType.Internal) }),
z.object({ type: z.literal(KmsType.External), kmsId: z.string() })
])
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -5,6 +5,7 @@ import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -23,10 +24,7 @@ import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model";
type TExternalKmsServiceFactoryDep = { type TExternalKmsServiceFactoryDep = {
externalKmsDAL: TExternalKmsDALFactory; externalKmsDAL: TExternalKmsDALFactory;
kmsService: Pick< kmsService: Pick<TKmsServiceFactory, "getOrgKmsKeyId" | "createCipherPairWithDataKey">;
TKmsServiceFactory,
"getOrgKmsKeyId" | "decryptWithInputKey" | "encryptWithInputKey" | "getOrgKmsDataKey"
>;
kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">; kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -84,12 +82,12 @@ export const externalKmsServiceFactory = ({
throw new BadRequestError({ message: "external kms provided is invalid" }); throw new BadRequestError({ message: "external kms provided is invalid" });
} }
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId); const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
const kmsEncryptor = await kmsService.encryptWithInputKey({ type: KmsDataKey.Organization,
key: orgKmsDataKey orgId: actorOrgId
}); });
const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({ const { cipherTextBlob: encryptedProviderInputs } = orgDataKeyEncryptor({
plainText: Buffer.from(sanitizedProviderInput, "utf8") plainText: Buffer.from(sanitizedProviderInput, "utf8")
}); });
@@ -150,13 +148,13 @@ export const externalKmsServiceFactory = ({
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
let sanitizedProviderInput = ""; let sanitizedProviderInput = "";
if (provider) { const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId); await kmsService.createCipherPairWithDataKey({
const kmsDecryptor = await kmsService.decryptWithInputKey({ type: KmsDataKey.Organization,
key: orgKmsDataKey orgId: actorOrgId
}); });
if (provider) {
const decryptedProviderInputBlob = kmsDecryptor({ const decryptedProviderInputBlob = orgDataKeyDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs cipherTextBlob: externalKmsDoc.encryptedProviderInputs
}); });
@@ -179,11 +177,7 @@ export const externalKmsServiceFactory = ({
let encryptedProviderInputs: Buffer | undefined; let encryptedProviderInputs: Buffer | undefined;
if (sanitizedProviderInput) { if (sanitizedProviderInput) {
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(actorOrgId); const { cipherTextBlob } = orgDataKeyEncryptor({
const kmsEncryptor = await kmsService.encryptWithInputKey({
key: orgKmsDataKey
});
const { cipherTextBlob } = kmsEncryptor({
plainText: Buffer.from(sanitizedProviderInput, "utf8") plainText: Buffer.from(sanitizedProviderInput, "utf8")
}); });
encryptedProviderInputs = cipherTextBlob; encryptedProviderInputs = cipherTextBlob;
@@ -266,12 +260,12 @@ export const externalKmsServiceFactory = ({
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId); const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
const kmsDecryptor = await kmsService.decryptWithInputKey({ type: KmsDataKey.Organization,
key: orgKmsDataKey orgId: actorOrgId
}); });
const decryptedProviderInputBlob = kmsDecryptor({ const decryptedProviderInputBlob = orgDataKeyDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs cipherTextBlob: externalKmsDoc.encryptedProviderInputs
}); });
switch (externalKmsDoc.provider) { switch (externalKmsDoc.provider) {
@@ -306,12 +300,12 @@ export const externalKmsServiceFactory = ({
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
const orgKmsDataKey = await kmsService.getOrgKmsDataKey(kmsDoc.orgId); const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
const kmsDecryptor = await kmsService.decryptWithInputKey({ type: KmsDataKey.Organization,
key: orgKmsDataKey orgId: actorOrgId
}); });
const decryptedProviderInputBlob = kmsDecryptor({ const decryptedProviderInputBlob = orgDataKeyDecryptor({
cipherTextBlob: externalKmsDoc.encryptedProviderInputs cipherTextBlob: externalKmsDoc.encryptedProviderInputs
}); });
@@ -17,6 +17,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { 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 { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
@@ -89,10 +90,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">; projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
kmsService: Pick< kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
TKmsServiceFactory,
"getProjectSecretManagerKmsDataKey" | "encryptWithInputKey" | "decryptWithInputKey"
>;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
@@ -214,8 +212,10 @@ export const secretApprovalRequestServiceFactory = ({
let secrets; let secrets;
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
const secretManagerDataKey = await kmsService.getProjectSecretManagerKmsDataKey(projectId); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
const secretManagerDecryptor = await kmsService.decryptWithInputKey({ key: secretManagerDataKey }); type: KmsDataKey.SecretManager,
projectId
});
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2( const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
secretApprovalRequest.id secretApprovalRequest.id
); );
@@ -427,8 +427,10 @@ export const secretApprovalRequestServiceFactory = ({
); );
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" }); if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
const secretManagerDataKey = await kmsService.getProjectSecretManagerKmsDataKey(projectId); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
const secretManagerDecryptor = await kmsService.decryptWithInputKey({ key: secretManagerDataKey }); type: KmsDataKey.SecretManager,
projectId
});
const conflicts: Array<{ secretId: string; op: SecretOperations }> = []; const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create); let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
@@ -1074,8 +1076,10 @@ export const secretApprovalRequestServiceFactory = ({
const commits: Omit<TSecretApprovalRequestsSecretsV2Insert, "requestId">[] = []; const commits: Omit<TSecretApprovalRequestsSecretsV2Insert, "requestId">[] = [];
const commitTagIds: Record<string, string[]> = {}; const commitTagIds: Record<string, string[]> = {};
const secretManagerDataKey = await kmsService.getProjectSecretManagerKmsDataKey(projectId); const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
const secretManagerEncryptor = await kmsService.encryptWithInputKey({ key: secretManagerDataKey }); type: KmsDataKey.SecretManager,
projectId
});
// for created secret approval change // for created secret approval change
const createdSecrets = data[SecretOperations.Create]; const createdSecrets = data[SecretOperations.Create];
+2
View File
@@ -10,6 +10,8 @@ export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>;
export const kmskeyDALFactory = (db: TDbClient) => { export const kmskeyDALFactory = (db: TDbClient) => {
const kmsOrm = ormify(db, TableName.KmsKey); const kmsOrm = ormify(db, TableName.KmsKey);
// akhilmhdh: this function should never be called outside kms service
// why: because the encrypted key should never be shared with another service
const findByIdWithAssociatedKms = async (id: string, tx?: Knex) => { const findByIdWithAssociatedKms = async (id: string, tx?: Knex) => {
try { try {
const result = await (tx || db.replicaNode())(TableName.KmsKey) const result = await (tx || db.replicaNode())(TableName.KmsKey)
+104 -74
View File
@@ -1,6 +1,8 @@
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { Knex } from "knex"; import { Knex } from "knex";
import { z } from "zod";
import { KmsKeysSchema } from "@app/db/schemas";
import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms"; import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
import { import {
ExternalKmsAwsSchema, ExternalKmsAwsSchema,
@@ -23,12 +25,14 @@ import { TKmsKeyDALFactory } from "./kms-key-dal";
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal"; import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
import { import {
KmsDataKey, KmsDataKey,
KmsType,
TDecryptWithKeyDTO, TDecryptWithKeyDTO,
TDecryptWithKmsDTO, TDecryptWithKmsDTO,
TEncryptionWithKeyDTO, TEncryptionWithKeyDTO,
TEncryptWithKmsDataKeyDTO, TEncryptWithKmsDataKeyDTO,
TEncryptWithKmsDTO, TEncryptWithKmsDTO,
TGenerateKMSDTO TGenerateKMSDTO,
TUpdateProjectSecretManagerKmsKeyDTO
} from "./kms-types"; } from "./kms-types";
type TKmsServiceFactoryDep = { type TKmsServiceFactoryDep = {
@@ -42,7 +46,6 @@ type TKmsServiceFactoryDep = {
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>; export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
export const INTERNAL_KMS_KEY_ID = "internal";
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key"; const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
@@ -51,6 +54,8 @@ const KMS_ROOT_CREATION_WAIT_TIME = 10;
// akhilmhdh: Don't edit this value. This is measured for blob concatination in kms // akhilmhdh: Don't edit this value. This is measured for blob concatination in kms
const KMS_VERSION = "v01"; const KMS_VERSION = "v01";
const KMS_VERSION_BLOB_LENGTH = 3; const KMS_VERSION_BLOB_LENGTH = 3;
const KmsSanitizedSchema = KmsKeysSchema.extend({ isExternal: z.boolean() });
export const kmsServiceFactory = ({ export const kmsServiceFactory = ({
kmsDAL, kmsDAL,
kmsRootConfigDAL, kmsRootConfigDAL,
@@ -61,7 +66,11 @@ export const kmsServiceFactory = ({
}: TKmsServiceFactoryDep) => { }: TKmsServiceFactoryDep) => {
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
// this is used symmetric encryption /*
* Generate KMS Key
* This function is responsibile for generating the infisical internal KMS for various entities
* Like for secret manager, cert manager or for organization
*/
const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => { const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKeyMaterial = randomSecureBytes(32); const kmsKeyMaterial = randomSecureBytes(32);
@@ -93,6 +102,11 @@ export const kmsServiceFactory = ({
return doc; return doc;
}; };
/*
* Simple encryption service function to do all the encryption tasks in infisical
* This can be even later exposed directly as api for encryption as function
* The encrypted binary even has everything into it. The IV, the version etc
*/
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => { const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
@@ -105,6 +119,10 @@ export const kmsServiceFactory = ({
}; };
}; };
/*
* Simple decryption service function to do all the encryption tasks in infisical
* This can be even later exposed directly as api for encryption as function
*/
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => { const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
@@ -115,6 +133,13 @@ export const kmsServiceFactory = ({
}; };
}; };
/*
* Function to generate a KMS for an org
* We handle concurrent with redis locking and waitReady
* What happens is first we check kms is assigned else first we acquire lock and create the kms with connection
* In mean time the rest of the request will wait until creation is finished followed by getting the created on
* In real time this would be milliseconds
*/
const getOrgKmsKeyId = async (orgId: string) => { const getOrgKmsKeyId = async (orgId: string) => {
let org = await orgDAL.findById(orgId); let org = await orgDAL.findById(orgId);
@@ -176,7 +201,12 @@ export const kmsServiceFactory = ({
return org.kmsDefaultKeyId; return org.kmsDefaultKeyId;
}; };
const decryptWithKmsKey = async ({ kmsId }: Omit<TDecryptWithKmsDTO, "cipherTextBlob">) => { const decryptWithKmsKey = async ({
kmsId,
depth = 0
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number }) => {
if (depth > 2) throw new BadRequestError({ message: "KMS depth max limit" });
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
if (!kmsDoc) { if (!kmsDoc) {
throw new NotFoundError({ message: "KMS ID not found" }); throw new NotFoundError({ message: "KMS ID not found" });
@@ -189,8 +219,12 @@ export const kmsServiceFactory = ({
throw new Error("Invalid organization KMS"); throw new Error("Invalid organization KMS");
} }
// The idea is external kms connection info is encrypted by an org default KMS
// This could be external kms(in future) but at the end of the day, the end KMS will be an infisical internal one
// we put a limit of depth to avoid too many cycles
const orgKmsDecryptor = await decryptWithKmsKey({ const orgKmsDecryptor = await decryptWithKmsKey({
kmsId: kmsDoc.orgKms.id kmsId: kmsDoc.orgKms.id,
depth: depth + 1
}); });
const orgKmsDataKey = await orgKmsDecryptor({ const orgKmsDataKey = await orgKmsDecryptor({
@@ -303,7 +337,7 @@ export const kmsServiceFactory = ({
}; };
}; };
const getOrgKmsDataKey = async (orgId: string) => { const $getOrgKmsDataKey = async (orgId: string) => {
const kmsKeyId = await getOrgKmsKeyId(orgId); const kmsKeyId = await getOrgKmsKeyId(orgId);
let org = await orgDAL.findById(orgId); let org = await orgDAL.findById(orgId);
@@ -313,7 +347,7 @@ export const kmsServiceFactory = ({
if (!org.kmsEncryptedDataKey) { if (!org.kmsEncryptedDataKey) {
const lock = await keyStore const lock = await keyStore
.acquireLock([KeyStorePrefixes.KmsOrgDataKeyCreation, orgId], 3000, { retryCount: 3 }) .acquireLock([KeyStorePrefixes.KmsOrgDataKeyCreation, orgId], 500, { retryCount: 0 })
.catch(() => null); .catch(() => null);
try { try {
@@ -448,14 +482,7 @@ export const kmsServiceFactory = ({
return project.kmsSecretManagerKeyId; return project.kmsSecretManagerKeyId;
}; };
const getProjectSecretManagerKmsKey = async (projectId: string) => { const $getProjectSecretManagerKmsDataKey = async (projectId: string) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
const kmsKey = await kmsDAL.findByIdWithAssociatedKms(kmsKeyId);
return kmsKey;
};
const getProjectSecretManagerKmsDataKey = async (projectId: string) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId); const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
let project = await projectDAL.findById(projectId); let project = await projectDAL.findById(projectId);
@@ -528,21 +555,58 @@ export const kmsServiceFactory = ({
}); });
}; };
const updateProjectSecretManagerKmsKey = async (projectId: string, kmsId: string) => { const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO) => {
const currentKms = await getProjectSecretManagerKmsKey(projectId); switch (dto.type) {
case KmsDataKey.SecretManager: {
return $getProjectSecretManagerKmsDataKey(dto.projectId);
}
default: {
return $getOrgKmsDataKey(dto.orgId);
}
}
};
if ((currentKms.isReserved && kmsId === INTERNAL_KMS_KEY_ID) || currentKms.id === kmsId) { // by keeping the decrypted data key in inner scope
return currentKms; // none of the entities outside can interact directly or expose the data key
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => {
const dataKey = await $getDataKey(encryptionContext);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return {
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey);
// Buffer#1 encrypted text + Buffer#2 version number
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
return { cipherTextBlob };
},
decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey);
return decryptedBlob;
}
};
};
const updateProjectSecretManagerKmsKey = async ({ projectId, kms }: TUpdateProjectSecretManagerKmsKeyDTO) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
const currentKms = await kmsDAL.findById(kmsKeyId);
// case: internal kms -> internal kms. no change needed
if (kms.type === KmsType.Internal && currentKms.isReserved) {
return KmsSanitizedSchema.parseAsync({ isExternal: false, ...currentKms });
} }
if (kmsId !== INTERNAL_KMS_KEY_ID) { if (kms.type === KmsType.External) {
// validate kms is scoped in org
const { kmsId } = kms;
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
if (!project) { if (!project) {
throw new NotFoundError({ throw new NotFoundError({
message: "Project not found." message: "Project not found."
}); });
} }
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
if (!kmsDoc) { if (!kmsDoc) {
throw new NotFoundError({ message: "KMS ID not found." }); throw new NotFoundError({ message: "KMS ID not found." });
@@ -555,38 +619,36 @@ export const kmsServiceFactory = ({
} }
} }
const dataKey = await getProjectSecretManagerKmsDataKey(projectId); const dataKey = await $getProjectSecretManagerKmsDataKey(projectId);
return kmsDAL.transaction(async (tx) => { return kmsDAL.transaction(async (tx) => {
const project = await projectDAL.findById(projectId, tx); const project = await projectDAL.findById(projectId, tx);
let newKmsId = kmsId; let kmsId;
if (kms.type === KmsType.Internal) {
if (newKmsId === INTERNAL_KMS_KEY_ID) { const internalKms = await generateKmsKey({
const key = await generateKmsKey({
isReserved: true, isReserved: true,
orgId: project.orgId, orgId: project.orgId,
tx tx
}); });
kmsId = internalKms.id;
newKmsId = key.id; } else {
kmsId = kms.kmsId;
} }
const kmsEncryptor = await encryptWithKmsKey({ kmsId: newKmsId }, tx); const kmsEncryptor = await encryptWithKmsKey({ kmsId }, tx);
const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey }); const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey });
await projectDAL.updateById( await projectDAL.updateById(
projectId, projectId,
{ {
kmsSecretManagerKeyId: newKmsId, kmsSecretManagerKeyId: kmsId,
kmsSecretManagerEncryptedDataKey: cipherTextBlob kmsSecretManagerEncryptedDataKey: cipherTextBlob
}, },
tx tx
); );
if (currentKms.isReserved) { if (currentKms.isReserved) {
await kmsDAL.deleteById(currentKms.id, tx); await kmsDAL.deleteById(currentKms.id, tx);
} }
const newKms = await kmsDAL.findById(kmsId, tx);
return kmsDAL.findByIdWithAssociatedKms(newKmsId, tx); return KmsSanitizedSchema.parseAsync({ isExternal: !currentKms.isReserved, ...newKms });
}); });
}; };
@@ -598,13 +660,15 @@ export const kmsServiceFactory = ({
}); });
} }
const secretManagerDataKey = await getProjectSecretManagerKmsDataKey(projectId); const secretManagerDataKey = await $getProjectSecretManagerKmsDataKey(projectId);
const kmsKeyIdForEncrypt = await getOrgKmsKeyId(project.orgId); const kmsKeyIdForEncrypt = await getOrgKmsKeyId(project.orgId);
const kmsEncryptor = await encryptWithKmsKey({ kmsId: kmsKeyIdForEncrypt }); const kmsEncryptor = await encryptWithKmsKey({ kmsId: kmsKeyIdForEncrypt });
const { cipherTextBlob: encryptedSecretManagerDataKey } = await kmsEncryptor({ plainText: secretManagerDataKey }); const { cipherTextBlob: encryptedSecretManagerDataKeyWithOrgKms } = await kmsEncryptor({
plainText: secretManagerDataKey
});
// backup format: version.projectId.kmsFunction.kmsId.Base64(encryptedDataKey).verificationHash // backup format: version.projectId.kmsFunction.kmsId.Base64(encryptedDataKey).verificationHash
let secretManagerBackup = `v1.${projectId}.secretManager.${kmsKeyIdForEncrypt}.${encryptedSecretManagerDataKey.toString( let secretManagerBackup = `v1.${projectId}.secretManager.${kmsKeyIdForEncrypt}.${encryptedSecretManagerDataKeyWithOrgKms.toString(
"base64" "base64"
)}`; )}`;
@@ -678,39 +742,8 @@ export const kmsServiceFactory = ({
message: "KMS not found" message: "KMS not found"
}); });
} }
const { id, slug, orgId, isExternal } = kms;
return kms; return { id, slug, orgId, isExternal };
};
const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO) => {
switch (dto.type) {
case KmsDataKey.SecretManager: {
return getProjectSecretManagerKmsDataKey(dto.projectId);
}
default: {
return getProjectSecretManagerKmsDataKey(dto.orgId);
}
}
};
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => {
const dataKey = await $getDataKey(encryptionContext);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return {
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey);
// Buffer#1 encrypted text + Buffer#2 version number
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
return { cipherTextBlob };
},
decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey);
return decryptedBlob;
}
};
}; };
const startService = async () => { const startService = async () => {
@@ -762,14 +795,11 @@ export const kmsServiceFactory = ({
startService, startService,
generateKmsKey, generateKmsKey,
encryptWithKmsKey, encryptWithKmsKey,
encryptWithInputKey,
decryptWithKmsKey, decryptWithKmsKey,
encryptWithInputKey,
decryptWithInputKey, decryptWithInputKey,
getOrgKmsKeyId, getOrgKmsKeyId,
getProjectSecretManagerKmsKeyId, getProjectSecretManagerKmsKeyId,
getOrgKmsDataKey,
getProjectSecretManagerKmsDataKey,
getProjectSecretManagerKmsKey,
updateProjectSecretManagerKmsKey, updateProjectSecretManagerKmsKey,
getProjectKeyBackup, getProjectKeyBackup,
loadProjectKeyBackup, loadProjectKeyBackup,
+24 -14
View File
@@ -1,5 +1,25 @@
import { Knex } from "knex"; import { Knex } from "knex";
export enum KmsDataKey {
Organization,
SecretManager
// CertificateManager
}
export enum KmsType {
External = "external",
Internal = "internal"
}
export type TEncryptWithKmsDataKeyDTO =
| { type: KmsDataKey.Organization; orgId: string }
| { type: KmsDataKey.SecretManager; projectId: string };
// akhilmhdh: not implemented yet
// | {
// type: KmsDataKey.CertificateManager;
// projectId: string;
// };
export type TGenerateKMSDTO = { export type TGenerateKMSDTO = {
orgId: string; orgId: string;
isReserved?: boolean; isReserved?: boolean;
@@ -27,17 +47,7 @@ export type TDecryptWithKeyDTO = {
cipherTextBlob: Buffer; cipherTextBlob: Buffer;
}; };
export enum KmsDataKey { export type TUpdateProjectSecretManagerKmsKeyDTO = {
Organization, projectId: string;
SecretManager kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
// CertificateManager };
}
export type TEncryptWithKmsDataKeyDTO =
| { type: KmsDataKey.Organization; orgId: string }
| { type: KmsDataKey.SecretManager; projectId: string };
// akhilmhdh: not implemented yet
// | {
// type: KmsDataKey.CertificateManager;
// projectId: string;
// };
@@ -650,7 +650,7 @@ export const projectServiceFactory = ({
const updateProjectKmsKey = async ({ const updateProjectKmsKey = async ({
projectId, projectId,
secretManagerKmsKeyId, kms,
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
@@ -666,7 +666,10 @@ export const projectServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
const secretManagerKmsKey = await kmsService.updateProjectSecretManagerKmsKey(projectId, secretManagerKmsKeyId); const secretManagerKmsKey = await kmsService.updateProjectSecretManagerKmsKey({
projectId,
kms
});
return { return {
secretManagerKmsKey secretManagerKmsKey
@@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { CaStatus } from "../certificate-authority/certificate-authority-types"; import { CaStatus } from "../certificate-authority/certificate-authority-types";
import { KmsType } from "../kms/kms-types";
export enum ProjectFilterType { export enum ProjectFilterType {
ID = "id", ID = "id",
@@ -106,7 +107,7 @@ export type TListProjectCertsDTO = {
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TUpdateProjectKmsDTO = { export type TUpdateProjectKmsDTO = {
secretManagerKmsKeyId: string; kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
} & TProjectPermission; } & TProjectPermission;
export type TLoadProjectKmsBackupDTO = { export type TLoadProjectKmsBackupDTO = {
+4 -2
View File
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { kmsKeys } from "./queries"; import { kmsKeys } from "./queries";
import { AddExternalKmsType } from "./types"; import { AddExternalKmsType, KmsType } from "./types";
export const useAddExternalKms = (orgId: string) => { export const useAddExternalKms = (orgId: string) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -66,7 +66,9 @@ export const useRemoveExternalKms = (orgId: string) => {
export const useUpdateProjectKms = (projectId: string) => { export const useUpdateProjectKms = (projectId: string) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (updatedData: { secretManagerKmsKeyId: string }) => { mutationFn: async (
updatedData: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string }
) => {
const { data } = await apiRequest.patch(`/api/v1/workspace/${projectId}/kms`, updatedData); const { data } = await apiRequest.patch(`/api/v1/workspace/${projectId}/kms`, updatedData);
return data; return data;
+5
View File
@@ -29,6 +29,11 @@ export type KmsListEntry = {
}; };
}; };
export enum KmsType {
Internal = "internal",
External = "external"
}
export enum ExternalKmsProvider { export enum ExternalKmsProvider {
AWS = "aws" AWS = "aws"
} }
@@ -31,7 +31,7 @@ import {
useUpdateProjectKms useUpdateProjectKms
} from "@app/hooks/api"; } from "@app/hooks/api";
import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries"; import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { INTERNAL_KMS_KEY_ID, KmsType } from "@app/hooks/api/kms/types";
import { Organization, Workspace } from "@app/hooks/api/types"; import { Organization, Workspace } from "@app/hooks/api/types";
const formSchema = z.object({ const formSchema = z.object({
@@ -207,7 +207,9 @@ export const EncryptionTab = () => {
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!); const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
const { data: activeKms } = useGetActiveProjectKms(currentWorkspace?.id!); const { data: activeKms } = useGetActiveProjectKms(currentWorkspace?.id!);
const { mutateAsync: updateProjectKms } = useUpdateProjectKms(currentWorkspace?.id!); const { mutateAsync: updateProjectKms, isLoading: isUpdatingProjectKms } = useUpdateProjectKms(
currentWorkspace?.id!
);
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([
"createBackupConfirmation", "createBackupConfirmation",
"loadBackup" "loadBackup"
@@ -239,9 +241,11 @@ export const EncryptionTab = () => {
const onUpdateProjectKms = async (data: TForm) => { const onUpdateProjectKms = async (data: TForm) => {
try { try {
await updateProjectKms({ await updateProjectKms(
secretManagerKmsKeyId: data.kmsKeyId data.kmsKeyId === INTERNAL_KMS_KEY_ID
}); ? { type: KmsType.Internal }
: { type: KmsType.External, kmsId: data.kmsKeyId }
);
createNotification({ createNotification({
text: "Successfully updated project KMS", text: "Successfully updated project KMS",
@@ -287,10 +291,9 @@ export const EncryptionTab = () => {
<FormControl errorText={error?.message} isError={Boolean(error)}> <FormControl errorText={error?.message} isError={Boolean(error)}>
<Select <Select
{...field} {...field}
isDisabled={!isAllowed} isDisabled={!isAllowed || isUpdatingProjectKms}
onValueChange={(e) => { onValueChange={onChange}
onChange(e); isLoading={isUpdatingProjectKms}
}}
className="w-3/4 bg-mineshaft-600" className="w-3/4 bg-mineshaft-600"
> >
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal"> <SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">