misc: finalized switching of project KMS

This commit is contained in:
Sheen Capadngan
2024-07-30 23:03:53 +05:30
committed by =
parent 7e5c3e8163
commit d918f3ecdf
16 changed files with 333 additions and 167 deletions
+15 -1
View File
@@ -226,7 +226,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const { secretManagerKmsKey } = await server.services.kms.updateProjectKmsKey({ const { secretManagerKmsKey } = await server.services.project.updateProjectKmsKey({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -235,6 +235,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
...req.body ...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 { return {
secretManagerKmsKey secretManagerKmsKey
}; };
@@ -143,7 +143,8 @@ export enum EventType {
CREATE_KMS = "create-kms", CREATE_KMS = "create-kms",
UPDATE_KMS = "update-kms", UPDATE_KMS = "update-kms",
DELETE_KMS = "delete-kms", DELETE_KMS = "delete-kms",
GET_KMS = "get-kms" GET_KMS = "get-kms",
UPDATE_PROJECT_KMS = "update-project-kms"
} }
interface UserActorMetadata { interface UserActorMetadata {
@@ -1212,6 +1213,16 @@ interface GetKmsEvent {
}; };
} }
interface UpdateProjectKmsEvent {
type: EventType.UPDATE_PROJECT_KMS;
metadata: {
secretManagerKmsKey: {
id: string;
slug: string;
};
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -1317,4 +1328,5 @@ export type Event =
| CreateKmsEvent | CreateKmsEvent
| UpdateKmsEvent | UpdateKmsEvent
| DeleteKmsEvent | DeleteKmsEvent
| GetKmsEvent; | GetKmsEvent
| UpdateProjectKmsEvent;
@@ -72,7 +72,7 @@ export const certificateAuthorityCrlServiceFactory = ({
kmsId: keyId kmsId: keyId
}); });
const decryptedCrl = kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl }); const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl); const crl = new x509.X509Crl(decryptedCrl);
const base64crl = crl.toString("base64"); const base64crl = crl.toString("base64");
@@ -28,7 +28,8 @@ export enum ProjectPermissionSub {
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
Identity = "identity", Identity = "identity",
CertificateAuthorities = "certificate-authorities", CertificateAuthorities = "certificate-authorities",
Certificates = "certificates" Certificates = "certificates",
Kms = "kms"
} }
type SubjectFields = { type SubjectFields = {
@@ -60,7 +61,8 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]; | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
const buildAdminPermissionRules = () => { const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
@@ -157,6 +159,8 @@ const buildAdminPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
return rules; return rules;
}; };
+3 -3
View File
@@ -310,8 +310,7 @@ export const registerRoutes = async (
kmsDAL, kmsDAL,
internalKmsDAL, internalKmsDAL,
orgDAL, orgDAL,
projectDAL, projectDAL
permissionService
}); });
const externalKmsService = externalKmsServiceFactory({ const externalKmsService = externalKmsServiceFactory({
kmsDAL, kmsDAL,
@@ -627,7 +626,8 @@ export const registerRoutes = async (
certificateDAL, certificateDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
keyStore keyStore,
kmsService
}); });
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
@@ -78,7 +78,7 @@ export const getCaCredentials = async ({
const kmsDecryptor = await kmsService.decryptWithKmsKey({ const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId kmsId: keyId
}); });
const decryptedPrivateKey = kmsDecryptor({ const decryptedPrivateKey = await kmsDecryptor({
cipherTextBlob: caSecret.encryptedPrivateKey cipherTextBlob: caSecret.encryptedPrivateKey
}); });
@@ -129,13 +129,13 @@ export const getCaCertChain = async ({
kmsId: keyId kmsId: keyId
}); });
const decryptedCaCert = kmsDecryptor({ const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate cipherTextBlob: caCert.encryptedCertificate
}); });
const caCertObj = new x509.X509Certificate(decryptedCaCert); const caCertObj = new x509.X509Certificate(decryptedCaCert);
const decryptedChain = kmsDecryptor({ const decryptedChain = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificateChain cipherTextBlob: caCert.encryptedCertificateChain
}); });
@@ -176,7 +176,7 @@ export const rebuildCaCrl = async ({
kmsId: keyId kmsId: keyId
}); });
const privateKey = kmsDecryptor({ const privateKey = await kmsDecryptor({
cipherTextBlob: caSecret.encryptedPrivateKey cipherTextBlob: caSecret.encryptedPrivateKey
}); });
@@ -210,7 +210,7 @@ export const rebuildCaCrl = async ({
const kmsEncryptor = await kmsService.encryptWithKmsKey({ const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: keyId kmsId: keyId
}); });
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(crl.rawData)) plainText: Buffer.from(new Uint8Array(crl.rawData))
}); });
@@ -91,7 +91,7 @@ export const certificateAuthorityQueueFactory = ({
const kmsDecryptor = await kmsService.decryptWithKmsKey({ const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId kmsId: keyId
}); });
const privateKey = kmsDecryptor({ const privateKey = await kmsDecryptor({
cipherTextBlob: caSecret.encryptedPrivateKey cipherTextBlob: caSecret.encryptedPrivateKey
}); });
@@ -125,7 +125,7 @@ export const certificateAuthorityQueueFactory = ({
const kmsEncryptor = await kmsService.encryptWithKmsKey({ const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: keyId kmsId: keyId
}); });
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(crl.rawData)) plainText: Buffer.from(new Uint8Array(crl.rawData))
}); });
@@ -181,11 +181,11 @@ export const certificateAuthorityServiceFactory = ({
] ]
}); });
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(cert.rawData)) plainText: Buffer.from(new Uint8Array(cert.rawData))
}); });
const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({ const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.alloc(0) plainText: Buffer.alloc(0)
}); });
@@ -209,7 +209,7 @@ export const certificateAuthorityServiceFactory = ({
signingKey: keys.privateKey signingKey: keys.privateKey
}); });
const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ const { cipherTextBlob: encryptedCrl } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(crl.rawData)) plainText: Buffer.from(new Uint8Array(crl.rawData))
}); });
@@ -224,7 +224,7 @@ export const certificateAuthorityServiceFactory = ({
// https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey
const skObj = KeyObject.from(keys.privateKey); const skObj = KeyObject.from(keys.privateKey);
const { cipherTextBlob: encryptedPrivateKey } = kmsEncryptor({ const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: skObj.export({ plainText: skObj.export({
type: "pkcs8", type: "pkcs8",
format: "der" format: "der"
@@ -458,7 +458,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
const decryptedCaCert = kmsDecryptor({ const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate cipherTextBlob: caCert.encryptedCertificate
}); });
@@ -615,11 +615,11 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(certObj.rawData)) plainText: Buffer.from(new Uint8Array(certObj.rawData))
}); });
const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({ const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChain) plainText: Buffer.from(certificateChain)
}); });
@@ -693,7 +693,7 @@ export const certificateAuthorityServiceFactory = ({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
const decryptedCaCert = kmsDecryptor({ const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate cipherTextBlob: caCert.encryptedCertificate
}); });
@@ -803,7 +803,7 @@ export const certificateAuthorityServiceFactory = ({
const kmsEncryptor = await kmsService.encryptWithKmsKey({ const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId kmsId: certificateManagerKmsId
}); });
const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData)) plainText: Buffer.from(new Uint8Array(leafCert.rawData))
}); });
@@ -173,7 +173,7 @@ export const certificateServiceFactory = ({
const kmsDecryptor = await kmsService.decryptWithKmsKey({ const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKeyId kmsId: certificateManagerKeyId
}); });
const decryptedCert = kmsDecryptor({ const decryptedCert = await kmsDecryptor({
cipherTextBlob: certBody.encryptedCertificate cipherTextBlob: certBody.encryptedCertificate
}); });
+9
View File
@@ -14,6 +14,7 @@ export const kmskeyDALFactory = (db: TDbClient) => {
try { try {
const result = await (tx || db.replicaNode())(TableName.KmsKey) const result = await (tx || db.replicaNode())(TableName.KmsKey)
.where({ [`${TableName.KmsKey}.id` as "id"]: id }) .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.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
.leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`) .leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`)
.first() .first()
@@ -31,11 +32,19 @@ export const kmskeyDALFactory = (db: TDbClient) => {
db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"), db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"),
db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"), db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"),
db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails") 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 = { const data = {
...KmsKeysSchema.parse(result), ...KmsKeysSchema.parse(result),
isExternal: Boolean(result?.externalKmsId), isExternal: Boolean(result?.externalKmsId),
orgKms: {
id: result?.orgKmsDefaultKeyId,
encryptedDataKey: result?.orgKmsEncryptedDataKey
},
externalKms: result?.externalKmsId externalKms: result?.externalKmsId
? { ? {
id: result.externalKmsId, id: result.externalKmsId,
+177 -92
View File
@@ -1,16 +1,17 @@
import crypto from "node:crypto";
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { Knex } from "knex"; import { Knex } from "knex";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ExternalKmsAwsSchema,
KmsProviders,
TExternalKmsProviderFns
} from "@app/ee/services/external-kms/providers/model";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { randomSecureBytes } from "@app/lib/crypto"; import { randomSecureBytes } from "@app/lib/crypto";
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; 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 { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -24,8 +25,7 @@ import {
TDecryptWithKmsDTO, TDecryptWithKmsDTO,
TEncryptionWithKeyDTO, TEncryptionWithKeyDTO,
TEncryptWithKmsDTO, TEncryptWithKmsDTO,
TGenerateKMSDTO, TGenerateKMSDTO
TUpdateProjectKmsDTO
} from "./kms-types"; } from "./kms-types";
type TKmsServiceFactoryDep = { type TKmsServiceFactoryDep = {
@@ -34,12 +34,12 @@ type TKmsServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findById" | "updateById" | "transaction">; orgDAL: Pick<TOrgDALFactory, "findById" | "updateById" | "transaction">;
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">; kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">; keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
internalKmsDAL: Pick<TInternalKmsDALFactory, "create">; internalKmsDAL: Pick<TInternalKmsDALFactory, "create">;
}; };
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>; export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
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";
@@ -54,8 +54,7 @@ export const kmsServiceFactory = ({
keyStore, keyStore,
internalKmsDAL, internalKmsDAL,
orgDAL, orgDAL,
projectDAL, projectDAL
permissionService
}: TKmsServiceFactoryDep) => { }: TKmsServiceFactoryDep) => {
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
@@ -91,22 +90,6 @@ export const kmsServiceFactory = ({
return doc; return doc;
}; };
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">) => {
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<TEncryptWithKmsDTO, "plainText">) => {
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<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);
@@ -119,19 +102,6 @@ export const kmsServiceFactory = ({
}; };
}; };
const decryptWithKmsKey = async ({ kmsId }: Omit<TDecryptWithKmsDTO, "cipherTextBlob">) => {
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<TDecryptWithKmsDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
return decryptedBlob;
};
};
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);
@@ -146,7 +116,7 @@ export const kmsServiceFactory = ({
const keyId = await orgDAL.transaction(async (tx) => { const keyId = await orgDAL.transaction(async (tx) => {
const org = await orgDAL.findById(orgId, tx); const org = await orgDAL.findById(orgId, tx);
if (!org) { if (!org) {
throw new BadRequestError({ message: "Org not found" }); throw new NotFoundError({ message: "Org not found" });
} }
if (!org.kmsDefaultKeyId) { if (!org.kmsDefaultKeyId) {
@@ -174,23 +144,154 @@ export const kmsServiceFactory = ({
return keyId; return keyId;
}; };
const decryptWithKmsKey = async ({ kmsId }: Omit<TDecryptWithKmsDTO, "cipherTextBlob">) => {
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<TDecryptWithKmsDTO, "cipherTextBlob">) => {
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<TDecryptWithKmsDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
return Promise.resolve(decryptedBlob);
};
};
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, 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<TEncryptWithKmsDTO, "plainText">) => {
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<TEncryptWithKmsDTO, "plainText">) => {
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 getOrgKmsDataKey = async (orgId: string) => {
const kmsKeyId = await getOrgKmsKeyId(orgId); const kmsKeyId = await getOrgKmsKeyId(orgId);
const orgKmsDataKey = await orgDAL.transaction(async (tx) => { const orgKmsDataKey = await orgDAL.transaction(async (tx) => {
const org = await orgDAL.findById(orgId, tx); const org = await orgDAL.findById(orgId, tx);
if (!org) { if (!org) {
throw new BadRequestError({ message: "Org not found" }); throw new NotFoundError({ message: "Org not found" });
} }
let encryptedDataKey = org.kmsEncryptedDataKey; let encryptedDataKey = org.kmsEncryptedDataKey;
if (!encryptedDataKey) { if (!encryptedDataKey) {
const dataKey = crypto.randomBytes(32); const dataKey = randomSecureBytes();
const kmsEncryptor = await encryptWithKmsKey({ const kmsEncryptor = await encryptWithKmsKey(
kmsId: kmsKeyId {
}); kmsId: kmsKeyId
},
tx
);
const { cipherTextBlob } = kmsEncryptor({ const { cipherTextBlob } = await kmsEncryptor({
plainText: dataKey plainText: dataKey
}); });
@@ -221,11 +322,10 @@ export const kmsServiceFactory = ({
const getProjectSecretManagerKmsKeyId = async (projectId: string) => { const getProjectSecretManagerKmsKeyId = async (projectId: string) => {
let project = await projectDAL.findById(projectId); let project = await projectDAL.findById(projectId);
if (!project) { if (!project) {
throw new BadRequestError({ message: "Project not found" }); throw new NotFoundError({ message: "Project not found" });
} }
if (!project.kmsSecretManagerKeyId) { if (!project.kmsSecretManagerKeyId) {
// create default kms key for certificate service
const lock = await keyStore const lock = await keyStore
.acquireLock([KeyStorePrefixes.KmsProjectKeyCreation, projectId], 3000, { retryCount: 3 }) .acquireLock([KeyStorePrefixes.KmsProjectKeyCreation, projectId], 3000, { retryCount: 3 })
.catch(() => null); .catch(() => null);
@@ -309,7 +409,7 @@ export const kmsServiceFactory = ({
kmsId: kmsKeyId kmsId: kmsKeyId
}); });
const { cipherTextBlob } = kmsEncryptor({ const { cipherTextBlob } = await kmsEncryptor({
plainText: dataKey plainText: dataKey
}); });
@@ -344,17 +444,38 @@ export const kmsServiceFactory = ({
const updateProjectSecretManagerKmsKey = async (projectId: string, kmsId: string) => { const updateProjectSecretManagerKmsKey = async (projectId: string, kmsId: string) => {
const currentKms = await getProjectSecretManagerKmsKey(projectId); 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; 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) => { return kmsDAL.transaction(async (tx) => {
const project = await projectDAL.findById(projectId, tx); const project = await projectDAL.findById(projectId, tx);
let newKmsId = kmsId; let newKmsId = kmsId;
if (newKmsId === "internal") { if (newKmsId === INTERNAL_KMS_KEY_ID) {
const key = await generateKmsKey({ const key = await generateKmsKey({
isReserved: true, isReserved: true,
orgId: project.orgId, orgId: project.orgId,
@@ -364,8 +485,8 @@ export const kmsServiceFactory = ({
newKmsId = key.id; newKmsId = key.id;
} }
const kmsEncryptor = await encryptWithKmsKey({ kmsId: newKmsId }); const kmsEncryptor = await encryptWithKmsKey({ kmsId: newKmsId }, tx);
const { cipherTextBlob } = kmsEncryptor({ plainText: dataKey }); const { cipherTextBlob } = await kmsEncryptor({ plainText: dataKey });
await projectDAL.updateById( await projectDAL.updateById(
projectId, projectId,
{ {
@@ -379,45 +500,10 @@ export const kmsServiceFactory = ({
await kmsDAL.deleteById(currentKms.id, tx); 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 startService = async () => {
const appCfg = getConfig(); const appCfg = getConfig();
// This will switch to a seal process and HMS flow in future // This will switch to a seal process and HMS flow in future
@@ -475,7 +561,6 @@ export const kmsServiceFactory = ({
getOrgKmsDataKey, getOrgKmsDataKey,
getProjectSecretManagerKmsDataKey, getProjectSecretManagerKmsDataKey,
getProjectSecretManagerKmsKey, getProjectSecretManagerKmsKey,
updateProjectKmsKey,
updateProjectSecretManagerKmsKey updateProjectSecretManagerKmsKey
}; };
}; };
-6
View File
@@ -1,7 +1,5 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TProjectPermission } from "@app/lib/types";
export type TGenerateKMSDTO = { export type TGenerateKMSDTO = {
orgId: string; orgId: string;
isReserved?: boolean; isReserved?: boolean;
@@ -28,7 +26,3 @@ export type TDecryptWithKeyDTO = {
key: Buffer; key: Buffer;
cipherTextBlob: Buffer; cipherTextBlob: Buffer;
}; };
export type TUpdateProjectKmsDTO = {
secretManagerKmsKeyId: string;
} & TProjectPermission;
@@ -21,6 +21,7 @@ import { TCertificateAuthorityDALFactory } from "../certificate-authority/certif
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
@@ -43,6 +44,7 @@ import {
TToggleProjectAutoCapitalizationDTO, TToggleProjectAutoCapitalizationDTO,
TUpdateAuditLogsRetentionDTO, TUpdateAuditLogsRetentionDTO,
TUpdateProjectDTO, TUpdateProjectDTO,
TUpdateProjectKmsDTO,
TUpdateProjectNameDTO, TUpdateProjectNameDTO,
TUpdateProjectVersionLimitDTO, TUpdateProjectVersionLimitDTO,
TUpgradeProjectDTO TUpgradeProjectDTO
@@ -76,6 +78,7 @@ type TProjectServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">; keyStore: Pick<TKeyStoreFactory, "deleteItem">;
kmsService: Pick<TKmsServiceFactory, "updateProjectSecretManagerKmsKey">;
}; };
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>; export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -100,7 +103,8 @@ export const projectServiceFactory = ({
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
keyStore keyStore,
kmsService
}: TProjectServiceFactoryDep) => { }: TProjectServiceFactoryDep) => {
/* /*
* Create workspace. Make user the admin * 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 { return {
createProject, createProject,
deleteProject, deleteProject,
@@ -677,6 +706,7 @@ export const projectServiceFactory = ({
listProjectCas, listProjectCas,
listProjectCertificates, listProjectCertificates,
updateVersionLimit, updateVersionLimit,
updateAuditLogsRetention updateAuditLogsRetention,
updateProjectKmsKey
}; };
}; };
@@ -103,3 +103,7 @@ export type TListProjectCertsDTO = {
friendlyName?: string; friendlyName?: string;
commonName?: string; commonName?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TUpdateProjectKmsDTO = {
secretManagerKmsKeyId: string;
} & TProjectPermission;
@@ -26,7 +26,8 @@ export enum ProjectPermissionSub {
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
Identity = "identity", Identity = "identity",
CertificateAuthorities = "certificate-authorities", CertificateAuthorities = "certificate-authorities",
Certificates = "certificates" Certificates = "certificates",
Kms = "kms"
} }
type SubjectFields = { type SubjectFields = {
@@ -1,10 +1,11 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; 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 { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
@@ -19,30 +20,41 @@ const formSchema = z.object({
type TForm = z.infer<typeof formSchema>; type TForm = z.infer<typeof formSchema>;
const INTERNAL_KMS_KEY_ID = "internal";
export const EncryptionTab = () => { export const EncryptionTab = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: externalKmsList, isLoading: isExternalKmsListLoading } = useGetExternalKmsList( const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!);
currentOrg?.id! const { data: activeKms } = useGetActiveProjectKms(currentWorkspace?.id!);
);
const { data: activeKms, isLoading: isActiveKmsLoading } = useGetActiveProjectKms(
currentWorkspace?.id!
);
const { mutateAsync: updateProjectKms } = useUpdateProjectKms(currentWorkspace?.id!); const { mutateAsync: updateProjectKms } = useUpdateProjectKms(currentWorkspace?.id!);
const [kmsKeyId, setKmsKeyId] = useState("");
const { const {
handleSubmit, handleSubmit,
control, control,
formState: { isSubmitting } setValue,
formState: { isSubmitting, isDirty }
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema)
defaultValues: {
kmsKeyId: activeKms?.isExternal ? activeKms?.id : "internal"
}
}); });
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) => { const onFormSubmit = async (data: TForm) => {
try { try {
await updateProjectKms({ await updateProjectKms({
@@ -68,41 +80,42 @@ export const EncryptionTab = () => {
Select which Key Management System to use for encrypting your project data Select which Key Management System to use for encrypting your project data
</p> </p>
<div className="mb-6 max-w-md"> <div className="mb-6 max-w-md">
{isExternalKmsListLoading || isActiveKmsLoading ? ( <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Kms}>
<ContentLoader /> {(isAllowed) => (
) : ( <Controller
<Controller render={({ field: { onChange, ...field }, fieldState: { error } }) => (
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( <FormControl errorText={error?.message} isError={Boolean(error)}>
<FormControl errorText={error?.message} isError={Boolean(error)}> <Select
<Select {...field}
{...field} isDisabled={!isAllowed}
onValueChange={(e) => { onValueChange={(e) => {
onChange(e); onChange(e);
}} }}
className="w-3/4 bg-mineshaft-600" className="w-3/4 bg-mineshaft-600"
> >
<SelectItem value="internal" key="kms-internal"> <SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
Default Infisical KMS Default Infisical KMS
</SelectItem>
{externalKmsList?.map((kms) => (
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
{kms.slug}
</SelectItem> </SelectItem>
))} {externalKmsList?.map((kms) => (
</Select> <SelectItem value={kms.id} key={`kms-${kms.id}`}>
</FormControl> {kms.slug}
)} </SelectItem>
control={control} ))}
name="kmsKeyId" </Select>
/> </FormControl>
)} )}
control={control}
name="kmsKeyId"
/>
)}
</ProjectPermissionCan>
</div> </div>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}> <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" colorSchema="secondary"
type="submit" type="submit"
isDisabled={!isAllowed || isSubmitting} isDisabled={!isAllowed || isSubmitting || !isDirty}
isLoading={isSubmitting} isLoading={isSubmitting}
> >
Save Save