From d918f3ecdf50fb1e6eb2939ddcc8d2cb725c3af6 Mon Sep 17 00:00:00 2001
From: Sheen Capadngan
Date: Thu, 18 Jul 2024 20:50:31 +0800
Subject: [PATCH] misc: finalized switching of project KMS
---
backend/src/ee/routes/v1/project-router.ts | 16 +-
.../ee/services/audit-log/audit-log-types.ts | 16 +-
.../certificate-authority-crl-service.ts | 2 +-
.../services/permission/project-permission.ts | 8 +-
backend/src/server/routes/index.ts | 6 +-
.../certificate-authority-fns.ts | 10 +-
.../certificate-authority-queue.ts | 4 +-
.../certificate-authority-service.ts | 18 +-
.../certificate/certificate-service.ts | 2 +-
backend/src/services/kms/kms-key-dal.ts | 9 +
backend/src/services/kms/kms-service.ts | 269 ++++++++++++------
backend/src/services/kms/kms-types.ts | 6 -
.../src/services/project/project-service.ts | 34 ++-
backend/src/services/project/project-types.ts | 4 +
.../context/ProjectPermissionContext/types.ts | 3 +-
.../EncryptionTab/EncryptionTab.tsx | 93 +++---
16 files changed, 333 insertions(+), 167 deletions(-)
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 ? (
-
- ) : (
-
(
-
-
+
+ )}
+ control={control}
+ name="kmsKeyId"
+ />
+ )}
+
{(isAllowed) => (