diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts index c31293516..03db346e4 100644 --- a/backend/src/ee/routes/v1/project-router.ts +++ b/backend/src/ee/routes/v1/project-router.ts @@ -226,7 +226,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { secretManagerKmsKey } = await server.services.kms.updateProjectKmsKey({ + const { secretManagerKmsKey } = await server.services.project.updateProjectKmsKey({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -235,6 +235,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { ...req.body }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.UPDATE_PROJECT_KMS, + metadata: { + secretManagerKmsKey: { + id: secretManagerKmsKey.id, + slug: secretManagerKmsKey.slug + } + } + } + }); + return { secretManagerKmsKey }; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index cfe671392..fca3206ae 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -143,7 +143,8 @@ export enum EventType { CREATE_KMS = "create-kms", UPDATE_KMS = "update-kms", DELETE_KMS = "delete-kms", - GET_KMS = "get-kms" + GET_KMS = "get-kms", + UPDATE_PROJECT_KMS = "update-project-kms" } interface UserActorMetadata { @@ -1212,6 +1213,16 @@ interface GetKmsEvent { }; } +interface UpdateProjectKmsEvent { + type: EventType.UPDATE_PROJECT_KMS; + metadata: { + secretManagerKmsKey: { + id: string; + slug: string; + }; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -1317,4 +1328,5 @@ export type Event = | CreateKmsEvent | UpdateKmsEvent | DeleteKmsEvent - | GetKmsEvent; + | GetKmsEvent + | UpdateProjectKmsEvent; diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts index 917c55a0f..2ef924ffb 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts @@ -72,7 +72,7 @@ export const certificateAuthorityCrlServiceFactory = ({ kmsId: keyId }); - const decryptedCrl = kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl }); + const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl }); const crl = new x509.X509Crl(decryptedCrl); const base64crl = crl.toString("base64"); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 4853faf61..7401d8dcd 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -28,7 +28,8 @@ export enum ProjectPermissionSub { SecretRotation = "secret-rotation", Identity = "identity", CertificateAuthorities = "certificate-authorities", - Certificates = "certificates" + Certificates = "certificates", + Kms = "kms" } type SubjectFields = { @@ -60,7 +61,8 @@ export type ProjectPermissionSet = | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] - | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]; + | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; const buildAdminPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); @@ -157,6 +159,8 @@ const buildAdminPermissionRules = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms); + return rules; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index c2a80a530..9fc47c046 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -310,8 +310,7 @@ export const registerRoutes = async ( kmsDAL, internalKmsDAL, orgDAL, - projectDAL, - permissionService + projectDAL }); const externalKmsService = externalKmsServiceFactory({ kmsDAL, @@ -627,7 +626,8 @@ export const registerRoutes = async ( certificateDAL, projectUserMembershipRoleDAL, identityProjectMembershipRoleDAL, - keyStore + keyStore, + kmsService }); const projectEnvService = projectEnvServiceFactory({ diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 9f98dcb83..32ac4bfb1 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -78,7 +78,7 @@ export const getCaCredentials = async ({ const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId: keyId }); - const decryptedPrivateKey = kmsDecryptor({ + const decryptedPrivateKey = await kmsDecryptor({ cipherTextBlob: caSecret.encryptedPrivateKey }); @@ -129,13 +129,13 @@ export const getCaCertChain = async ({ kmsId: keyId }); - const decryptedCaCert = kmsDecryptor({ + const decryptedCaCert = await kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificate }); const caCertObj = new x509.X509Certificate(decryptedCaCert); - const decryptedChain = kmsDecryptor({ + const decryptedChain = await kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificateChain }); @@ -176,7 +176,7 @@ export const rebuildCaCrl = async ({ kmsId: keyId }); - const privateKey = kmsDecryptor({ + const privateKey = await kmsDecryptor({ cipherTextBlob: caSecret.encryptedPrivateKey }); @@ -210,7 +210,7 @@ export const rebuildCaCrl = async ({ const kmsEncryptor = await kmsService.encryptWithKmsKey({ kmsId: keyId }); - const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ + const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({ plainText: Buffer.from(new Uint8Array(crl.rawData)) }); diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts index 30da119d0..1d7021937 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -91,7 +91,7 @@ export const certificateAuthorityQueueFactory = ({ const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId: keyId }); - const privateKey = kmsDecryptor({ + const privateKey = await kmsDecryptor({ cipherTextBlob: caSecret.encryptedPrivateKey }); @@ -125,7 +125,7 @@ export const certificateAuthorityQueueFactory = ({ const kmsEncryptor = await kmsService.encryptWithKmsKey({ kmsId: keyId }); - const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ + const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({ plainText: Buffer.from(new Uint8Array(crl.rawData)) }); diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index afc8d7efb..a1390b1f7 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -181,11 +181,11 @@ export const certificateAuthorityServiceFactory = ({ ] }); - const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ plainText: Buffer.from(new Uint8Array(cert.rawData)) }); - const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({ + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ plainText: Buffer.alloc(0) }); @@ -209,7 +209,7 @@ export const certificateAuthorityServiceFactory = ({ signingKey: keys.privateKey }); - const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ + const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({ plainText: Buffer.from(new Uint8Array(crl.rawData)) }); @@ -224,7 +224,7 @@ export const certificateAuthorityServiceFactory = ({ // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey const skObj = KeyObject.from(keys.privateKey); - const { cipherTextBlob: encryptedPrivateKey } = kmsEncryptor({ + const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ plainText: skObj.export({ type: "pkcs8", format: "der" @@ -458,7 +458,7 @@ export const certificateAuthorityServiceFactory = ({ }); const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); - const decryptedCaCert = kmsDecryptor({ + const decryptedCaCert = await kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificate }); @@ -615,11 +615,11 @@ export const certificateAuthorityServiceFactory = ({ kmsId: certificateManagerKmsId }); - const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ plainText: Buffer.from(new Uint8Array(certObj.rawData)) }); - const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({ + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ plainText: Buffer.from(certificateChain) }); @@ -693,7 +693,7 @@ export const certificateAuthorityServiceFactory = ({ kmsId: certificateManagerKmsId }); - const decryptedCaCert = kmsDecryptor({ + const decryptedCaCert = await kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificate }); @@ -803,7 +803,7 @@ export const certificateAuthorityServiceFactory = ({ const kmsEncryptor = await kmsService.encryptWithKmsKey({ kmsId: certificateManagerKmsId }); - const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }); diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 401a55cc9..f05ed8a87 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -173,7 +173,7 @@ export const certificateServiceFactory = ({ const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId: certificateManagerKeyId }); - const decryptedCert = kmsDecryptor({ + const decryptedCert = await kmsDecryptor({ cipherTextBlob: certBody.encryptedCertificate }); diff --git a/backend/src/services/kms/kms-key-dal.ts b/backend/src/services/kms/kms-key-dal.ts index 8e1e17cd1..c2b1c0358 100644 --- a/backend/src/services/kms/kms-key-dal.ts +++ b/backend/src/services/kms/kms-key-dal.ts @@ -14,6 +14,7 @@ export const kmskeyDALFactory = (db: TDbClient) => { try { const result = await (tx || db.replicaNode())(TableName.KmsKey) .where({ [`${TableName.KmsKey}.id` as "id"]: id }) + .join(TableName.Organization, `${TableName.KmsKey}.orgId`, `${TableName.Organization}.id`) .leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`) .leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`) .first() @@ -31,11 +32,19 @@ export const kmskeyDALFactory = (db: TDbClient) => { db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"), db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"), db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails") + ) + .select( + db.ref("kmsDefaultKeyId").withSchema(TableName.Organization).as("orgKmsDefaultKeyId"), + db.ref("kmsEncryptedDataKey").withSchema(TableName.Organization).as("orgKmsEncryptedDataKey") ); const data = { ...KmsKeysSchema.parse(result), isExternal: Boolean(result?.externalKmsId), + orgKms: { + id: result?.orgKmsDefaultKeyId, + encryptedDataKey: result?.orgKmsEncryptedDataKey + }, externalKms: result?.externalKmsId ? { id: result.externalKmsId, diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 6e2c830a8..1337a1f40 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -1,16 +1,17 @@ -import crypto from "node:crypto"; - -import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms"; +import { + ExternalKmsAwsSchema, + KmsProviders, + TExternalKmsProviderFns +} from "@app/ee/services/external-kms/providers/model"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { randomSecureBytes } from "@app/lib/crypto"; import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -24,8 +25,7 @@ import { TDecryptWithKmsDTO, TEncryptionWithKeyDTO, TEncryptWithKmsDTO, - TGenerateKMSDTO, - TUpdateProjectKmsDTO + TGenerateKMSDTO } from "./kms-types"; type TKmsServiceFactoryDep = { @@ -34,12 +34,12 @@ type TKmsServiceFactoryDep = { orgDAL: Pick; kmsRootConfigDAL: Pick; keyStore: Pick; - permissionService: Pick; internalKmsDAL: Pick; }; export type TKmsServiceFactory = ReturnType; +const INTERNAL_KMS_KEY_ID = "internal"; const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key"; @@ -54,8 +54,7 @@ export const kmsServiceFactory = ({ keyStore, internalKmsDAL, orgDAL, - projectDAL, - permissionService + projectDAL }: TKmsServiceFactoryDep) => { let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); @@ -91,22 +90,6 @@ export const kmsServiceFactory = ({ return doc; }; - const encryptWithKmsKey = async ({ kmsId }: Omit) => { - const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); - if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); - // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - return ({ plainText }: Pick) => { - const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); - const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey); - - // 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 }; - }; - }; - const encryptWithInputKey = async ({ key }: Omit) => { // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); @@ -119,19 +102,6 @@ export const kmsServiceFactory = ({ }; }; - const decryptWithKmsKey = async ({ kmsId }: Omit) => { - const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); - if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); - - return ({ cipherTextBlob: versionedCipherTextBlob }: Pick) => { - const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); - const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey); - return decryptedBlob; - }; - }; - const decryptWithInputKey = async ({ key }: Omit) => { const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); @@ -146,7 +116,7 @@ export const kmsServiceFactory = ({ const keyId = await orgDAL.transaction(async (tx) => { const org = await orgDAL.findById(orgId, tx); if (!org) { - throw new BadRequestError({ message: "Org not found" }); + throw new NotFoundError({ message: "Org not found" }); } if (!org.kmsDefaultKeyId) { @@ -174,23 +144,154 @@ export const kmsServiceFactory = ({ return keyId; }; + const decryptWithKmsKey = async ({ kmsId }: Omit) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: "KMS ID not found" }); + } + + if (kmsDoc.externalKms) { + let externalKms: TExternalKmsProviderFns; + + if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) { + throw new Error("Invalid organization KMS"); + } + + const orgKmsDecryptor = await decryptWithKmsKey({ + kmsId: kmsDoc.orgKms.id + }); + + // fetch encryptedDataKey straight from kmsDoc by joining it in query :D + const orgKmsDataKey = await orgKmsDecryptor({ + cipherTextBlob: kmsDoc.orgKms.encryptedDataKey + }); + + const kmsDecryptor = await decryptWithInputKey({ + key: orgKmsDataKey + }); + + const decryptedProviderInputBlob = kmsDecryptor({ + cipherTextBlob: kmsDoc.externalKms.encryptedProviderInput + }); + + switch (kmsDoc.externalKms.provider) { + case KmsProviders.Aws: { + const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString("utf8")) + ); + + externalKms = await AwsKmsProviderFactory({ + inputs: decryptedProviderInput + }); + break; + } + default: + throw new Error("Invalid KMS provider."); + } + + return async ({ cipherTextBlob }: Pick) => { + const { data } = await externalKms.decrypt(cipherTextBlob); + + return data; + }; + } + + // internal KMS + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + + return ({ cipherTextBlob: versionedCipherTextBlob }: Pick) => { + const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); + const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey); + return Promise.resolve(decryptedBlob); + }; + }; + + const encryptWithKmsKey = async ({ kmsId }: Omit, tx?: Knex) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx); + if (!kmsDoc) { + throw new NotFoundError({ message: "KMS ID not found" }); + } + + if (kmsDoc.externalKms) { + let externalKms: TExternalKmsProviderFns; + if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) { + throw new Error("Invalid organization KMS"); + } + + const orgKmsDecryptor = await decryptWithKmsKey({ + kmsId: kmsDoc.orgKms.id + }); + + const orgKmsDataKey = await orgKmsDecryptor({ + cipherTextBlob: kmsDoc.orgKms.encryptedDataKey + }); + + const kmsDecryptor = await decryptWithInputKey({ + key: orgKmsDataKey + }); + + const decryptedProviderInputBlob = kmsDecryptor({ + cipherTextBlob: kmsDoc.externalKms.encryptedProviderInput + }); + + switch (kmsDoc.externalKms.provider) { + case KmsProviders.Aws: { + const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString("utf8")) + ); + + externalKms = await AwsKmsProviderFactory({ + inputs: decryptedProviderInput + }); + break; + } + default: + throw new Error("Invalid KMS provider."); + } + + return async ({ plainText }: Pick) => { + const { encryptedBlob } = await externalKms.encrypt(plainText); + + return { cipherTextBlob: encryptedBlob }; + }; + } + + // internal KMS + // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + return ({ plainText }: Pick) => { + const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey); + + // 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 Promise.resolve({ cipherTextBlob }); + }; + }; + const getOrgKmsDataKey = async (orgId: string) => { const kmsKeyId = await getOrgKmsKeyId(orgId); const orgKmsDataKey = await orgDAL.transaction(async (tx) => { const org = await orgDAL.findById(orgId, tx); if (!org) { - throw new BadRequestError({ message: "Org not found" }); + throw new NotFoundError({ message: "Org not found" }); } let encryptedDataKey = org.kmsEncryptedDataKey; if (!encryptedDataKey) { - const dataKey = crypto.randomBytes(32); - const kmsEncryptor = await encryptWithKmsKey({ - kmsId: kmsKeyId - }); + const dataKey = randomSecureBytes(); + const kmsEncryptor = await encryptWithKmsKey( + { + kmsId: kmsKeyId + }, + tx + ); - const { cipherTextBlob } = kmsEncryptor({ + const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey }); @@ -221,11 +322,10 @@ export const kmsServiceFactory = ({ const getProjectSecretManagerKmsKeyId = async (projectId: string) => { let project = await projectDAL.findById(projectId); if (!project) { - throw new BadRequestError({ message: "Project not found" }); + throw new NotFoundError({ message: "Project not found" }); } if (!project.kmsSecretManagerKeyId) { - // create default kms key for certificate service const lock = await keyStore .acquireLock([KeyStorePrefixes.KmsProjectKeyCreation, projectId], 3000, { retryCount: 3 }) .catch(() => null); @@ -309,7 +409,7 @@ export const kmsServiceFactory = ({ kmsId: kmsKeyId }); - const { cipherTextBlob } = kmsEncryptor({ + const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey }); @@ -344,17 +444,38 @@ export const kmsServiceFactory = ({ const updateProjectSecretManagerKmsKey = async (projectId: string, kmsId: string) => { const currentKms = await getProjectSecretManagerKmsKey(projectId); - const dataKey = await getProjectSecretManagerKmsDataKey(projectId); - if (currentKms.isReserved && kmsId === "internal") { + if ((currentKms.isReserved && kmsId === INTERNAL_KMS_KEY_ID) || currentKms.id === kmsId) { return currentKms; } + if (kmsId !== INTERNAL_KMS_KEY_ID) { + const project = await projectDAL.findById(projectId); + if (!project) { + throw new NotFoundError({ + message: "Project not found." + }); + } + + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) { + throw new NotFoundError({ message: "KMS ID not found." }); + } + + if (kmsDoc.orgId !== project.orgId) { + throw new BadRequestError({ + message: "KMS ID does not belong in the organization." + }); + } + } + + const dataKey = await getProjectSecretManagerKmsDataKey(projectId); + return kmsDAL.transaction(async (tx) => { const project = await projectDAL.findById(projectId, tx); let newKmsId = kmsId; - if (newKmsId === "internal") { + if (newKmsId === INTERNAL_KMS_KEY_ID) { const key = await generateKmsKey({ isReserved: true, orgId: project.orgId, @@ -364,8 +485,8 @@ export const kmsServiceFactory = ({ newKmsId = key.id; } - const kmsEncryptor = await encryptWithKmsKey({ kmsId: newKmsId }); - const { cipherTextBlob } = kmsEncryptor({ plainText: dataKey }); + const kmsEncryptor = await encryptWithKmsKey({ kmsId: newKmsId }, tx); + const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey }); await projectDAL.updateById( projectId, { @@ -379,45 +500,10 @@ export const kmsServiceFactory = ({ await kmsDAL.deleteById(currentKms.id, tx); } - return kmsDAL.findByIdWithAssociatedKms(newKmsId); + return kmsDAL.findByIdWithAssociatedKms(newKmsId, tx); }); }; - const updateProjectKmsKey = async ({ - projectId, - secretManagerKmsKeyId, - actor, - actorId, - actorAuthMethod, - actorOrgId - }: TUpdateProjectKmsDTO) => { - const { permission } = await permissionService.getProjectPermission( - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId - ); - - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); - if (secretManagerKmsKeyId !== "internal") { - const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(secretManagerKmsKeyId); - if (!kmsDoc) { - throw new BadRequestError({ message: "KMS ID not found." }); - } - - if (kmsDoc.orgId !== actorOrgId) { - throw new BadRequestError({ - message: "KMS ID does not belong in the organization." - }); - } - } - - return { - secretManagerKmsKey: await updateProjectSecretManagerKmsKey(projectId, secretManagerKmsKeyId) - }; - }; - const startService = async () => { const appCfg = getConfig(); // This will switch to a seal process and HMS flow in future @@ -475,7 +561,6 @@ export const kmsServiceFactory = ({ getOrgKmsDataKey, getProjectSecretManagerKmsDataKey, getProjectSecretManagerKmsKey, - updateProjectKmsKey, updateProjectSecretManagerKmsKey }; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index 3f4170930..5ba6c1343 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -1,7 +1,5 @@ import { Knex } from "knex"; -import { TProjectPermission } from "@app/lib/types"; - export type TGenerateKMSDTO = { orgId: string; isReserved?: boolean; @@ -28,7 +26,3 @@ export type TDecryptWithKeyDTO = { key: Buffer; cipherTextBlob: Buffer; }; - -export type TUpdateProjectKmsDTO = { - secretManagerKmsKeyId: string; -} & TProjectPermission; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index b1a53408a..e8027a7bc 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -21,6 +21,7 @@ import { TCertificateAuthorityDALFactory } from "../certificate-authority/certif import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; +import { TKmsServiceFactory } from "../kms/kms-service"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; @@ -43,6 +44,7 @@ import { TToggleProjectAutoCapitalizationDTO, TUpdateAuditLogsRetentionDTO, TUpdateProjectDTO, + TUpdateProjectKmsDTO, TUpdateProjectNameDTO, TUpdateProjectVersionLimitDTO, TUpgradeProjectDTO @@ -76,6 +78,7 @@ type TProjectServiceFactoryDep = { licenseService: Pick; orgDAL: Pick; keyStore: Pick; + kmsService: Pick; }; export type TProjectServiceFactory = ReturnType; @@ -100,7 +103,8 @@ export const projectServiceFactory = ({ identityProjectMembershipRoleDAL, certificateAuthorityDAL, certificateDAL, - keyStore + keyStore, + kmsService }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -664,6 +668,31 @@ export const projectServiceFactory = ({ }; }; + const updateProjectKmsKey = async ({ + projectId, + secretManagerKmsKeyId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TUpdateProjectKmsDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms); + + const secretManagerKmsKey = await kmsService.updateProjectSecretManagerKmsKey(projectId, secretManagerKmsKeyId); + + return { + secretManagerKmsKey + }; + }; + return { createProject, deleteProject, @@ -677,6 +706,7 @@ export const projectServiceFactory = ({ listProjectCas, listProjectCertificates, updateVersionLimit, - updateAuditLogsRetention + updateAuditLogsRetention, + updateProjectKmsKey }; }; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index c49e51143..ff0cd0a91 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -103,3 +103,7 @@ export type TListProjectCertsDTO = { friendlyName?: string; commonName?: string; } & Omit; + +export type TUpdateProjectKmsDTO = { + secretManagerKmsKeyId: string; +} & TProjectPermission; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 113aaff19..fc163da13 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -26,7 +26,8 @@ export enum ProjectPermissionSub { SecretRotation = "secret-rotation", Identity = "identity", CertificateAuthorities = "certificate-authorities", - Certificates = "certificates" + Certificates = "certificates", + Kms = "kms" } type SubjectFields = { diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx index 7e4ae1713..f839a720c 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx @@ -1,10 +1,11 @@ +import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Button, ContentLoader, FormControl, Select, SelectItem } from "@app/components/v2"; +import { Button, FormControl, Select, SelectItem } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, @@ -19,30 +20,41 @@ const formSchema = z.object({ type TForm = z.infer; +const INTERNAL_KMS_KEY_ID = "internal"; + export const EncryptionTab = () => { const { currentOrg } = useOrganization(); const { currentWorkspace } = useWorkspace(); - const { data: externalKmsList, isLoading: isExternalKmsListLoading } = useGetExternalKmsList( - currentOrg?.id! - ); - const { data: activeKms, isLoading: isActiveKmsLoading } = useGetActiveProjectKms( - currentWorkspace?.id! - ); + const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!); + const { data: activeKms } = useGetActiveProjectKms(currentWorkspace?.id!); const { mutateAsync: updateProjectKms } = useUpdateProjectKms(currentWorkspace?.id!); + const [kmsKeyId, setKmsKeyId] = useState(""); const { handleSubmit, control, - formState: { isSubmitting } + setValue, + formState: { isSubmitting, isDirty } } = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - kmsKeyId: activeKms?.isExternal ? activeKms?.id : "internal" - } + resolver: zodResolver(formSchema) }); + useEffect(() => { + if (activeKms) { + setKmsKeyId(activeKms.isExternal ? activeKms.id : INTERNAL_KMS_KEY_ID); + } else { + setKmsKeyId(INTERNAL_KMS_KEY_ID); + } + }, [activeKms]); + + useEffect(() => { + if (kmsKeyId) { + setValue("kmsKeyId", kmsKeyId); + } + }, [kmsKeyId]); + const onFormSubmit = async (data: TForm) => { try { await updateProjectKms({ @@ -68,41 +80,42 @@ export const EncryptionTab = () => { Select which Key Management System to use for encrypting your project data

- {isExternalKmsListLoading || isActiveKmsLoading ? ( - - ) : ( - ( - - { + onChange(e); + }} + className="w-3/4 bg-mineshaft-600" + > + + Default Infisical KMS - ))} - - - )} - control={control} - name="kmsKeyId" - /> - )} + {externalKmsList?.map((kms) => ( + + {kms.slug} + + ))} + + + )} + control={control} + name="kmsKeyId" + /> + )} +
{(isAllowed) => (