diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 9d54335bb..6d05dc3b2 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -59,6 +59,9 @@ import { TDynamicSecrets, TDynamicSecretsInsert, TDynamicSecretsUpdate, + TExternalKms, + TExternalKmsInsert, + TExternalKmsUpdate, TGitAppInstallSessions, TGitAppInstallSessionsInsert, TGitAppInstallSessionsUpdate, @@ -122,6 +125,9 @@ import { TIntegrations, TIntegrationsInsert, TIntegrationsUpdate, + TInternalKms, + TInternalKmsInsert, + TInternalKmsUpdate, TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate, @@ -648,6 +654,8 @@ declare module "knex/types/tables" { TKmsRootConfigInsert, TKmsRootConfigUpdate >; + [TableName.InternalKms]: KnexOriginal.CompositeTableType; + [TableName.ExternalKms]: KnexOriginal.CompositeTableType; [TableName.KmsKey]: KnexOriginal.CompositeTableType; [TableName.KmsKeyVersion]: KnexOriginal.CompositeTableType< TKmsKeyVersions, diff --git a/backend/src/services/kms/internal-kms-dal.ts b/backend/src/services/kms/internal-kms-dal.ts new file mode 100644 index 000000000..f038fc3db --- /dev/null +++ b/backend/src/services/kms/internal-kms-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TInternalKmsDALFactory = ReturnType; + +export const internalKmsDALFactory = (db: TDbClient) => { + const internalKmsOrm = ormify(db, TableName.InternalKms); + return internalKmsOrm; +}; diff --git a/backend/src/services/kms/kms-dal.ts b/backend/src/services/kms/kms-dal.ts deleted file mode 100644 index bee667e10..000000000 --- a/backend/src/services/kms/kms-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TKmsDALFactory = ReturnType; - -export const kmsDALFactory = (db: TDbClient) => { - const kmsOrm = ormify(db, TableName.KmsKey); - return kmsOrm; -}; diff --git a/backend/src/services/kms/kms-key-dal.ts b/backend/src/services/kms/kms-key-dal.ts new file mode 100644 index 000000000..8e1e17cd1 --- /dev/null +++ b/backend/src/services/kms/kms-key-dal.ts @@ -0,0 +1,64 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { KmsKeysSchema, TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TKmsKeyDALFactory = ReturnType; + +export const kmskeyDALFactory = (db: TDbClient) => { + const kmsOrm = ormify(db, TableName.KmsKey); + + const findByIdWithAssociatedKms = async (id: string, tx?: Knex) => { + try { + const result = await (tx || db.replicaNode())(TableName.KmsKey) + .where({ [`${TableName.KmsKey}.id` as "id"]: id }) + .leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`) + .leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`) + .first() + .select(selectAllTableCols(TableName.KmsKey)) + .select( + db.ref("id").withSchema(TableName.InternalKms).as("internalKmsId"), + db.ref("encryptedKey").withSchema(TableName.InternalKms).as("internalKmsEncryptedKey"), + db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms).as("internalKmsEncryptionAlgorithm"), + db.ref("version").withSchema(TableName.InternalKms).as("internalKmsVersion"), + db.ref("id").withSchema(TableName.InternalKms).as("internalKmsId") + ) + .select( + db.ref("id").withSchema(TableName.ExternalKms).as("externalKmsId"), + db.ref("provider").withSchema(TableName.ExternalKms).as("externalKmsProvider"), + db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"), + db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"), + db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails") + ); + + const data = { + ...KmsKeysSchema.parse(result), + isExternal: Boolean(result?.externalKmsId), + externalKms: result?.externalKmsId + ? { + id: result.externalKmsId, + provider: result.externalKmsProvider, + encryptedProviderInput: result.externalKmsEncryptedProviderInput, + status: result.externalKmsStatus, + statusDetails: result.externalKmsStatusDetails + } + : undefined, + internalKms: result?.internalKmsId + ? { + id: result.internalKmsId, + encryptedKey: result.internalKmsEncryptedKey, + encryptionAlgorithm: result.internalKmsEncryptionAlgorithm, + version: result.internalKmsVersion + } + : undefined + }; + return data; + } catch (error) { + throw new DatabaseError({ error, name: "Find by id" }); + } + }; + + return { ...kmsOrm, findByIdWithAssociatedKms }; +}; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 63aba8939..0c0f48f97 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -1,18 +1,28 @@ +import slugify from "@sindresorhus/slugify"; +import { Knex } from "knex"; + import { 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 { logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { TKmsDALFactory } from "./kms-dal"; +import { TOrgDALFactory } from "../org/org-dal"; +import { TProjectDALFactory } from "../project/project-dal"; +import { TInternalKmsDALFactory } from "./internal-kms-dal"; +import { TKmsKeyDALFactory } from "./kms-key-dal"; import { TKmsRootConfigDALFactory } from "./kms-root-config-dal"; -import { TDecryptWithKmsDTO, TEncryptWithKmsDTO, TGenerateKMSDTO } from "./kms-types"; +import { EncryptionMode, TGenerateKMSDTO, TKmsServiceDecryptionDTO, TKmsServiceEncryptionDTO } from "./kms-types"; type TKmsServiceFactoryDep = { - kmsDAL: TKmsDALFactory; + kmsDAL: TKmsKeyDALFactory; + projectDAL: Pick; + orgDAL: Pick; kmsRootConfigDAL: Pick; keyStore: Pick; + internalKmsDAL: Pick; }; export type TKmsServiceFactory = ReturnType; @@ -25,36 +35,71 @@ const KMS_ROOT_CREATION_WAIT_TIME = 10; // akhilmhdh: Don't edit this value. This is measured for blob concatination in kms const KMS_VERSION = "v01"; const KMS_VERSION_BLOB_LENGTH = 3; -export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsServiceFactoryDep) => { +export const kmsServiceFactory = ({ + kmsDAL, + kmsRootConfigDAL, + keyStore, + internalKmsDAL, + orgDAL, + projectDAL +}: TKmsServiceFactoryDep) => { let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); // this is used symmetric encryption - const generateKmsKey = async ({ scopeId, scopeType, isReserved = true, tx }: TGenerateKMSDTO) => { + const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => { const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const kmsKeyMaterial = randomSecureBytes(32); const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY); + const sanitizedSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(32)); + const dbQuery = async (db: Knex) => { + const kmsDoc = await kmsDAL.create({ + slug: sanitizedSlug, + orgId, + isReserved + }); - const { encryptedKey, ...doc } = await kmsDAL.create( - { - version: 1, - encryptedKey: encryptedKeyMaterial, - encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, - isReserved, - orgId: scopeType === "org" ? scopeId : undefined, - projectId: scopeType === "project" ? scopeId : undefined - }, - tx - ); + const { encryptedKey, ...doc } = await internalKmsDAL.create( + { + version: 1, + encryptedKey: encryptedKeyMaterial, + encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, + kmsKeyId: kmsDoc.id + }, + db + ); + return doc; + }; + if (tx) return dbQuery(tx); + const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2)); return doc; }; - const encrypt = async ({ kmsId, plainText }: TEncryptWithKmsDTO) => { - const kmsDoc = await kmsDAL.findById(kmsId); - if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); + /* + * KMS encryption service + * Function to handle various kinds of encryption like + * Normal encryption + * Encrypt with KMS key - internal or external + */ + const encrypt = async (encryptionDetails: TKmsServiceEncryptionDTO) => { // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + // instead of using kms key encrypt with the provided key + if (encryptionDetails.type === EncryptionMode.EncryptionKey) { + const { plainText, encryptionKey } = encryptionDetails; - const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY); + const encryptedPlainTextBlob = cipher.encrypt(plainText, encryptionKey); + // 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 }; + } + + // this mean use kms to encrypt it + const { plainText, kmsId } = encryptionDetails; + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); + + 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 @@ -63,18 +108,96 @@ export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsSe return { cipherTextBlob }; }; - const decrypt = async ({ cipherTextBlob: versionedCipherTextBlob, kmsId }: TDecryptWithKmsDTO) => { - const kmsDoc = await kmsDAL.findById(kmsId); - if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); + /* + * KMS decryption service + * Function to handle various kinds of decryptionlike + * Normal decryption with a key + * Encrypt with KMS key - internal or external + */ + const decrypt = async (encryptionDetails: TKmsServiceDecryptionDTO) => { // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY); + if (encryptionDetails.type === EncryptionMode.EncryptionKey) { + const { cipherTextBlob: versionedCipherTextBlob, encryptionKey } = encryptionDetails; + const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); + const decryptedBlob = cipher.decrypt(cipherTextBlob, encryptionKey); + return decryptedBlob; + } + + const { cipherTextBlob: versionedCipherTextBlob, kmsId } = encryptionDetails; + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); + const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey); return decryptedBlob; }; + const getOrgKmsKeyId = async (orgId: string) => { + const keyId = await orgDAL.transaction(async (tx) => { + const org = await orgDAL.findById(orgId, tx); + if (!org) { + throw new BadRequestError({ message: "Org not found" }); + } + + if (!org.kmsDefaultKeyId) { + // create default kms key for certificate service + const key = await generateKmsKey({ + isReserved: true, + orgId: org.id, + tx + }); + + await orgDAL.updateById( + org.id, + { + kmsDefaultKeyId: key.id + }, + tx + ); + + return key.id; + } + + return org.kmsDefaultKeyId; + }); + + return keyId; + }; + + const getProjectSecretManagerKmsKeyId = async (projectId: string) => { + const keyId = await projectDAL.transaction(async (tx) => { + const project = await projectDAL.findById(projectId, tx); + if (!project) { + throw new BadRequestError({ message: "Project not found" }); + } + + if (!project.kmsSecretManagerKeyId) { + // create default kms key for certificate service + const key = await generateKmsKey({ + isReserved: true, + orgId: project.orgId, + tx + }); + + await projectDAL.updateById( + projectId, + { + kmsSecretManagerKeyId: key.id + }, + tx + ); + + return key.id; + } + + return project.kmsSecretManagerKeyId; + }); + + return keyId; + }; + const startService = async () => { const appCfg = getConfig(); // This will switch to a seal process and HMS flow in future @@ -124,6 +247,8 @@ export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsSe startService, generateKmsKey, encrypt, - decrypt + decrypt, + getOrgKmsKeyId, + getProjectSecretManagerKmsKeyId }; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index 63fdaf484..e1a152f06 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -1,18 +1,41 @@ import { Knex } from "knex"; export type TGenerateKMSDTO = { - scopeType: "project" | "org"; - scopeId: string; + orgId: string; isReserved?: boolean; + slug?: string; tx?: Knex; }; +export enum EncryptionMode { + KMS = "kms", + EncryptionKey = "encryption-key" +} + export type TEncryptWithKmsDTO = { + type?: EncryptionMode.KMS; kmsId: string; plainText: Buffer; }; +export type TEncryptionWithKeyDTO = { + type: EncryptionMode.EncryptionKey; + encryptionKey: Buffer; + plainText: Buffer; +}; + +export type TKmsServiceEncryptionDTO = TEncryptWithKmsDTO | TEncryptionWithKeyDTO; + export type TDecryptWithKmsDTO = { + type?: EncryptionMode.KMS; kmsId: string; cipherTextBlob: Buffer; }; + +export type TDecryptWithEncryptionKeyDTO = { + type: EncryptionMode.EncryptionKey; + encryptionKey: Buffer; + cipherTextBlob: Buffer; +}; + +export type TKmsServiceDecryptionDTO = TDecryptWithKmsDTO | TDecryptWithEncryptionKeyDTO; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index d518a698a..c792e0e45 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -207,9 +207,9 @@ export const orgDALFactory = (db: TDbClient) => { } }; - const updateById = async (orgId: string, data: Partial) => { + const updateById = async (orgId: string, data: Partial, tx?: Knex) => { try { - const [org] = await db(TableName.Organization) + const [org] = await (tx || db)(TableName.Organization) .where({ id: orgId }) .update({ ...data }) .returning("*"); diff --git a/backend/src/services/project/project-fns.ts b/backend/src/services/project/project-fns.ts index 78c7b442f..d6b010e0b 100644 --- a/backend/src/services/project/project-fns.ts +++ b/backend/src/services/project/project-fns.ts @@ -71,9 +71,8 @@ export const getProjectKmsCertificateKeyId = async ({ if (!project.kmsCertificateKeyId) { // create default kms key for certificate service const key = await kmsService.generateKmsKey({ - scopeId: projectId, - scopeType: "project", isReserved: true, + orgId: project.orgId, tx });