From 71c49c8b9008a9ae01cd4db7787a3860f33fca15 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 10 Jul 2024 12:10:03 +0530 Subject: [PATCH 1/6] feat: kms db schema changes to support external and internal kms uniformly --- .../migrations/20240708100026_external-kms.ts | 188 ++++++++++++++++++ backend/src/db/schemas/external-kms.ts | 23 +++ backend/src/db/schemas/index.ts | 2 + .../db/schemas/internal-kms-key-version.ts | 21 ++ backend/src/db/schemas/internal-kms.ts | 22 ++ backend/src/db/schemas/kms-keys.ts | 9 +- backend/src/db/schemas/models.ts | 5 +- backend/src/db/schemas/organizations.ts | 3 +- backend/src/db/schemas/projects.ts | 3 +- 9 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 backend/src/db/migrations/20240708100026_external-kms.ts create mode 100644 backend/src/db/schemas/external-kms.ts create mode 100644 backend/src/db/schemas/internal-kms-key-version.ts create mode 100644 backend/src/db/schemas/internal-kms.ts diff --git a/backend/src/db/migrations/20240708100026_external-kms.ts b/backend/src/db/migrations/20240708100026_external-kms.ts new file mode 100644 index 000000000..3c4bceb93 --- /dev/null +++ b/backend/src/db/migrations/20240708100026_external-kms.ts @@ -0,0 +1,188 @@ +import slugify from "@sindresorhus/slugify"; +import { Knex } from "knex"; + +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + // rename old kms key table to internal kms table + // the kms key table would be a container to hold external and internal respectively + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + const doesOldKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.KmsKeyVersion); + const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); + + if (doesOldKmsKeyTableExist && !doesInternalKmsTableExist) { + await knex.schema.createTable(TableName.InternalKms, (tb) => { + tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + tb.binary("encryptedKey").notNullable(); + tb.string("encryptionAlgorithm").notNullable(); + tb.integer("version").defaultTo(1).notNullable(); + tb.uuid("kmsKeyId").unique().notNullable(); + tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE"); + }); + // copy the old kms and build the data + const oldKmsKey = await knex(TableName.KmsKey).select("version", "encryptedKey", "encryptionAlgorithm", "id"); + if (oldKmsKey.length) { + await knex(TableName.InternalKms).insert( + oldKmsKey.map((el) => ({ + encryptionAlgorithm: el.encryptionAlgorithm, + encryptedKey: el.encryptedKey, + kmsKeyId: el.id, + version: el.version + })) + ); + } + + if (doesOldKmsKeyVersionTableExist) { + // because we haven't started using versioning for kms thus no data exist + await knex.schema.renameTable(TableName.KmsKeyVersion, TableName.InternalKmsKeyVersion); + await knex.schema.alterTable(TableName.InternalKmsKeyVersion, (tb) => { + tb.dropColumn("kmsKeyId"); + tb.uuid("internalKmsId").notNullable(); + tb.foreign("internalKmsId").references("id").inTable(TableName.InternalKms).onDelete("CASCADE"); + }); + } + + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + tb.string("slug", 32); + tb.dropColumn("encryptedKey"); + tb.dropColumn("encryptionAlgorithm"); + tb.dropColumn("version"); + }); + // backfill all org id in kms key + await knex(TableName.KmsKey) + .whereNull("orgId") + .update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + orgId: knex(TableName.Project) + .select("orgId") + .where("id", knex.raw("??", [`${TableName.KmsKey}.projectId`])) + }); + // backfill slugs in kms + const missingSlugs = await knex(TableName.KmsKey).whereNull("slug").select("id"); + if (missingSlugs.length) { + await knex(TableName.KmsKey) + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + .insert(missingSlugs.map(({ id }) => ({ id, slug: slugify(alphaNumericNanoId(32)) }))) + .onConflict("id") + .merge(); + } + + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + tb.uuid("orgId").notNullable().alter(); + tb.string("slug", 32).notNullable().alter(); + tb.dropColumn("projectId"); + }); + } + + const doesExternalKmsServiceExist = await knex.schema.hasTable(TableName.ExternalKms); + if (!doesExternalKmsServiceExist) { + await knex.schema.createTable(TableName.ExternalKms, (tb) => { + tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + tb.string("provider").notNullable(); + tb.binary("encryptedProviderInputs").notNullable(); + tb.string("status"); + tb.string("statusDetails"); + tb.uuid("kmsKeyId").unique().notNullable(); + tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE"); + }); + } + + const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId"); + if (!doesOrgKmsKeyExist) { + await knex.schema.alterTable(TableName.Organization, (tb) => { + tb.uuid("kmsDefaultKeyId").nullable(); + tb.foreign("kmsDefaultKeyId").references("id").inTable(TableName.KmsKey); + }); + } + + const doesProjectKmsSecretManagerKeyExist = await knex.schema.hasColumn(TableName.Project, "kmsSecretManagerKeyId"); + if (!doesProjectKmsSecretManagerKeyExist) { + await knex.schema.alterTable(TableName.Project, (tb) => { + tb.uuid("kmsSecretManagerKeyId").nullable(); + tb.foreign("kmsSecretManagerKeyId").references("id").inTable(TableName.KmsKey); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId"); + if (doesOrgKmsKeyExist) { + await knex.schema.alterTable(TableName.Organization, (tb) => { + tb.dropColumn("kmsDefaultKeyId"); + }); + } + + const doesProjectKmsSecretManagerKeyExist = await knex.schema.hasColumn(TableName.Project, "kmsSecretManagerKeyId"); + if (doesProjectKmsSecretManagerKeyExist) { + await knex.schema.alterTable(TableName.Project, (tb) => { + tb.dropColumn("kmsSecretManagerKeyId"); + }); + } + + const doesInternalKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.InternalKmsKeyVersion); + const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); + if (doesInternalKmsKeyVersionTableExist) { + // because we haven't started using versioning for kms thus no data exist + await knex.schema.renameTable(TableName.InternalKmsKeyVersion, TableName.KmsKeyVersion); + await knex.schema.alterTable(TableName.KmsKeyVersion, (tb) => { + tb.dropColumn("internalKmsId"); + tb.uuid("kmsKeyId").notNullable(); + tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE"); + }); + } + + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + const doesKmsSlugExist = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + if (doesInternalKmsTableExist && doesOldKmsKeyTableExist) { + // converting kms key to old one + // backfill so not setting it as not nullable + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + tb.binary("encryptedKey"); + tb.string("encryptionAlgorithm"); + tb.integer("version").defaultTo(1); + tb.string("projectId"); + tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + if (doesKmsSlugExist) { + tb.dropColumn("slug"); + } + }); + // backfill kms key with internal kms data + await knex(TableName.KmsKey).update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + encryptedKey: knex(TableName.InternalKms) + .select("encryptedKey") + .where("kmsKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])), + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + encryptionAlgorithm: knex(TableName.InternalKms) + .select("encryptionAlgorithm") + .where("kmsKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])), + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + projectId: knex(TableName.Project) + .select("id") + .where("kmsCertificateKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])) + }); + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + tb.binary("encryptedKey").notNullable().alter(); + tb.string("encryptionAlgorithm").notNullable().alter(); + }); + await knex.schema.alterTable(TableName.InternalKms, (tb) => { + tb.dropForeign("kmsKeyId"); + }); + await knex.schema.dropTable(TableName.InternalKms); + } + + const doesExternalKmsServiceExist = await knex.schema.hasTable(TableName.ExternalKms); + if (doesExternalKmsServiceExist) { + await knex.schema.alterTable(TableName.ExternalKms, (tb) => { + tb.dropForeign("kmsKeyId"); + }); + await knex.schema.dropTable(TableName.ExternalKms); + } +} diff --git a/backend/src/db/schemas/external-kms.ts b/backend/src/db/schemas/external-kms.ts new file mode 100644 index 000000000..810c3f70f --- /dev/null +++ b/backend/src/db/schemas/external-kms.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ExternalKmsSchema = z.object({ + id: z.string().uuid(), + provider: z.string(), + encryptedProviderInputs: zodBuffer, + status: z.string().nullable().optional(), + statusDetails: z.string().nullable().optional(), + kmsKeyId: z.string().uuid() +}); + +export type TExternalKms = z.infer; +export type TExternalKmsInsert = Omit, TImmutableDBKeys>; +export type TExternalKmsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index bce99dfea..ff8d44de2 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -17,6 +17,7 @@ export * from "./certificate-secrets"; export * from "./certificates"; export * from "./dynamic-secret-leases"; export * from "./dynamic-secrets"; +export * from "./external-kms"; export * from "./git-app-install-sessions"; export * from "./git-app-org"; export * from "./group-project-membership-roles"; @@ -38,6 +39,7 @@ export * from "./identity-universal-auths"; export * from "./incident-contacts"; export * from "./integration-auths"; export * from "./integrations"; +export * from "./internal-kms"; export * from "./kms-key-versions"; export * from "./kms-keys"; export * from "./kms-root-config"; diff --git a/backend/src/db/schemas/internal-kms-key-version.ts b/backend/src/db/schemas/internal-kms-key-version.ts new file mode 100644 index 000000000..fc1e3c3db --- /dev/null +++ b/backend/src/db/schemas/internal-kms-key-version.ts @@ -0,0 +1,21 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const InternalKmsKeyVersionSchema = z.object({ + id: z.string().uuid(), + encryptedKey: zodBuffer, + version: z.number(), + internalKmsId: z.string().uuid() +}); + +export type TInternalKmsKeyVersion = z.infer; +export type TInternalKmsKeyVersionInsert = Omit, TImmutableDBKeys>; +export type TInternalKmsKeyVersionUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/internal-kms.ts b/backend/src/db/schemas/internal-kms.ts new file mode 100644 index 000000000..38e64dc5b --- /dev/null +++ b/backend/src/db/schemas/internal-kms.ts @@ -0,0 +1,22 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const InternalKmsSchema = z.object({ + id: z.string().uuid(), + encryptedKey: zodBuffer, + encryptionAlgorithm: z.string(), + version: z.number().default(1), + kmsKeyId: z.string().uuid() +}); + +export type TInternalKms = z.infer; +export type TInternalKmsInsert = Omit, TImmutableDBKeys>; +export type TInternalKmsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/kms-keys.ts b/backend/src/db/schemas/kms-keys.ts index 503c270d9..5e8dcf166 100644 --- a/backend/src/db/schemas/kms-keys.ts +++ b/backend/src/db/schemas/kms-keys.ts @@ -5,20 +5,15 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const KmsKeysSchema = z.object({ id: z.string().uuid(), - encryptedKey: zodBuffer, - encryptionAlgorithm: z.string(), - version: z.number().default(1), description: z.string().nullable().optional(), isDisabled: z.boolean().default(false).nullable().optional(), isReserved: z.boolean().default(true).nullable().optional(), - projectId: z.string().nullable().optional(), - orgId: z.string().uuid().nullable().optional() + orgId: z.string().uuid(), + slug: z.string() }); export type TKmsKeys = z.infer; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 1dba71209..646e0455e 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -95,7 +95,10 @@ export enum TableName { // KMS Service KmsServerRootConfig = "kms_root_config", KmsKey = "kms_keys", - KmsKeyVersion = "kms_key_versions" + KmsKeyVersion = "kms_key_versions", + ExternalKms = "external_kms", + InternalKms = "internal_kms", + InternalKmsKeyVersion = "internal_kms_key_version" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index f2933af86..7b7a004fc 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -15,7 +15,8 @@ export const OrganizationsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), authEnforced: z.boolean().default(false).nullable().optional(), - scimEnabled: z.boolean().default(false).nullable().optional() + scimEnabled: z.boolean().default(false).nullable().optional(), + kmsDefaultKeyId: z.string().uuid().nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index f776e864c..19597c9df 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -19,7 +19,8 @@ export const ProjectsSchema = z.object({ upgradeStatus: z.string().nullable().optional(), pitVersionLimit: z.number().default(10), kmsCertificateKeyId: z.string().uuid().nullable().optional(), - auditLogsRetentionDays: z.number().nullable().optional() + auditLogsRetentionDays: z.number().nullable().optional(), + kmsSecretManagerKeyId: z.string().uuid().nullable().optional() }); export type TProjects = z.infer; From 7ca7a950708e366de688082ddbaeca7a5490764a Mon Sep 17 00:00:00 2001 From: = Date: Wed, 10 Jul 2024 12:12:01 +0530 Subject: [PATCH 2/6] feat: kms service changes for db change --- backend/src/@types/knex.d.ts | 8 + backend/src/services/kms/internal-kms-dal.ts | 10 ++ backend/src/services/kms/kms-dal.ts | 10 -- backend/src/services/kms/kms-key-dal.ts | 64 +++++++ backend/src/services/kms/kms-service.ts | 175 ++++++++++++++++--- backend/src/services/kms/kms-types.ts | 27 ++- backend/src/services/org/org-dal.ts | 4 +- backend/src/services/project/project-fns.ts | 3 +- 8 files changed, 260 insertions(+), 41 deletions(-) create mode 100644 backend/src/services/kms/internal-kms-dal.ts delete mode 100644 backend/src/services/kms/kms-dal.ts create mode 100644 backend/src/services/kms/kms-key-dal.ts 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 }); From 2e7baf8c892fc371cb019d1fbf454569b44e3b03 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 10 Jul 2024 12:12:43 +0530 Subject: [PATCH 3/6] feat: added external kms router but not connected with the server yet --- backend/package-lock.json | 1062 +++++++++++++++++ backend/package.json | 3 +- .../services/external-kms/external-kms-dal.ts | 47 + .../external-kms/external-kms-service.ts | 298 +++++ .../external-kms/external-kms-types.ts | 30 + .../external-kms/providers/aws-kms.ts | 102 ++ .../services/external-kms/providers/model.ts | 61 + .../services/permission/permission-service.ts | 3 + 8 files changed, 1605 insertions(+), 1 deletion(-) create mode 100644 backend/src/ee/services/external-kms/external-kms-dal.ts create mode 100644 backend/src/ee/services/external-kms/external-kms-service.ts create mode 100644 backend/src/ee/services/external-kms/external-kms-types.ts create mode 100644 backend/src/ee/services/external-kms/providers/aws-kms.ts create mode 100644 backend/src/ee/services/external-kms/providers/model.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index ad341d15c..3b6cf66e5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-iam": "^3.525.0", + "@aws-sdk/client-kms": "^3.609.0", "@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", @@ -1199,6 +1200,1067 @@ } } }, + "node_modules/@aws-sdk/client-kms": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.609.0.tgz", + "integrity": "sha512-tKXOHnmwdN7a7i213xDWtjlpkpRfRzdPjHXz1dl0ofUe/DYFph14O9TFWqCXQe25ilcjPuR8cmVJqHYy0kS/4A==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.609.0", + "@aws-sdk/client-sts": "3.609.0", + "@aws-sdk/core": "3.609.0", + "@aws-sdk/credential-provider-node": "3.609.0", + "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.609.0", + "@aws-sdk/region-config-resolver": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.609.0", + "@smithy/config-resolver": "^3.0.4", + "@smithy/core": "^2.2.4", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.3", + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.7", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.7", + "@smithy/util-defaults-mode-node": "^3.0.7", + "@smithy/util-endpoints": "^2.0.4", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/client-sso": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.609.0.tgz", + "integrity": "sha512-gqXGFDkIpKHCKAbeJK4aIDt3tiwJ26Rf5Tqw9JS6BYXsdMeOB8FTzqD9R+Yc1epHd8s5L94sdqXT5PapgxFZrg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.609.0", + "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.609.0", + "@aws-sdk/region-config-resolver": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.609.0", + "@smithy/config-resolver": "^3.0.4", + "@smithy/core": "^2.2.4", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.3", + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.7", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.7", + "@smithy/util-defaults-mode-node": "^3.0.7", + "@smithy/util-endpoints": "^2.0.4", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.609.0.tgz", + "integrity": "sha512-0bNPAyPdkWkS9EGB2A9BZDkBNrnVCBzk5lYRezoT4K3/gi9w1DTYH5tuRdwaTZdxW19U1mq7CV0YJJARKO1L9Q==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.609.0", + "@aws-sdk/credential-provider-node": "3.609.0", + "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.609.0", + "@aws-sdk/region-config-resolver": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.609.0", + "@smithy/config-resolver": "^3.0.4", + "@smithy/core": "^2.2.4", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.3", + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.7", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.7", + "@smithy/util-defaults-mode-node": "^3.0.7", + "@smithy/util-endpoints": "^2.0.4", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/core": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.609.0.tgz", + "integrity": "sha512-ptqw+DTxLr01+pKjDUuo53SEDzI+7nFM3WfQaEo0yhDg8vWw8PER4sWj1Ysx67ksctnZesPUjqxd5SHbtdBxiA==", + "dependencies": { + "@smithy/core": "^2.2.4", + "@smithy/protocol-http": "^4.0.3", + "@smithy/signature-v4": "^3.1.2", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "fast-xml-parser": "4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.609.0.tgz", + "integrity": "sha512-v69ZCWcec2iuV9vLVJMa6fAb5xwkzN4jYIT8yjo2c4Ia/j976Q+TPf35Pnz5My48Xr94EFcaBazrWedF+kwfuQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.609.0.tgz", + "integrity": "sha512-GQQfB9Mk4XUZwaPsk4V3w8MqleS6ApkZKVQn3vTLAKa8Y7B2Imcpe5zWbKYjDd8MPpMWjHcBGFTVlDRFP4zwSQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.609.0.tgz", + "integrity": "sha512-hwaBfXuBTv6/eAdEsDfGcteYUW6Km7lvvubbxEdxIuJNF3vswR7RMGIXaEC37hhPkTTgd3H0TONammhwZIfkog==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.609.0", + "@aws-sdk/credential-provider-http": "3.609.0", + "@aws-sdk/credential-provider-process": "3.609.0", + "@aws-sdk/credential-provider-sso": "3.609.0", + "@aws-sdk/credential-provider-web-identity": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.609.0.tgz", + "integrity": "sha512-4J8/JRuqfxJDGD9jTHVCBxCvYt7/Vgj2Stlhj930mrjFPO/yRw8ilAAZxBWe0JHPX3QwepCmh4ErZe53F5ysxQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.609.0", + "@aws-sdk/credential-provider-http": "3.609.0", + "@aws-sdk/credential-provider-ini": "3.609.0", + "@aws-sdk/credential-provider-process": "3.609.0", + "@aws-sdk/credential-provider-sso": "3.609.0", + "@aws-sdk/credential-provider-web-identity": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.609.0.tgz", + "integrity": "sha512-Ux35nGOSJKZWUIM3Ny0ROZ8cqPRUEkh+tR3X2o9ydEbFiLq3eMMyEnHJqx4EeUjLRchidlm4CCid9GxMe5/gdw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.609.0.tgz", + "integrity": "sha512-oQPGDKMMIxjvTcm86g07RPYeC7mCNk+29dPpY15ZAPRpAF7F0tircsC3wT9fHzNaKShEyK5LuI5Kg/uxsdy+Iw==", + "dependencies": { + "@aws-sdk/client-sso": "3.609.0", + "@aws-sdk/token-providers": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.609.0.tgz", + "integrity": "sha512-U+PG8NhlYYF45zbr1km3ROtBMYqyyj/oK8NRp++UHHeuavgrP+4wJ4wQnlEaKvJBjevfo3+dlIBcaeQ7NYejWg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.609.0.tgz", + "integrity": "sha512-iTKfo158lc4jLDfYeZmYMIBHsn8m6zX+XB6birCSNZ/rrlzAkPbGE43CNdKfvjyWdqgLMRXF+B+OcZRvqhMXPQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-logger": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.609.0.tgz", + "integrity": "sha512-S62U2dy4jMDhDFDK5gZ4VxFdWzCtLzwbYyFZx2uvPYTECkepLUfzLic2BHg2Qvtu4QjX+oGE3P/7fwaGIsGNuQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.609.0.tgz", + "integrity": "sha512-6sewsYB7/o/nbUfA99Aa/LokM+a/u4Wpm/X2o0RxOsDtSB795ObebLJe2BxY5UssbGaWkn7LswyfvrdZNXNj1w==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.609.0.tgz", + "integrity": "sha512-nbq7MXRmeXm4IDqh+sJRAxGPAq0OfGmGIwKvJcw66hLoG8CmhhVMZmIAEBDFr57S+YajGwnLLRt+eMI05MMeVA==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.609.0.tgz", + "integrity": "sha512-lMHBG8zg9GWYBc9/XVPKyuAUd7iKqfPP7z04zGta2kGNOKbUTeqmAdc1gJGku75p4kglIPlGBorOxti8DhRmKw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/token-providers": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.609.0.tgz", + "integrity": "sha512-WvhW/7XSf+H7YmtiIigQxfDVZVZI7mbKikQ09YpzN7FeN3TmYib1+0tB+EE9TbICkwssjiFc71FEBEh4K9grKQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", + "integrity": "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/util-endpoints": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.609.0.tgz", + "integrity": "sha512-Rh+3V8dOvEeE1aQmUy904DYWtLUEJ7Vf5XBPlQ6At3pBhp+zpXbsnpZzVL33c8lW1xfj6YPwtO6gOeEsl1juCQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.609.0.tgz", + "integrity": "sha512-fojPU+mNahzQ0YHYBsx0ZIhmMA96H+ZIZ665ObU9tl+SGdbLneVZVikGve+NmHTQwHzwkFsZYYnVKAkreJLAtA==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.609.0.tgz", + "integrity": "sha512-DlZBwQ/HkZyf3pOWc7+wjJRk5R7x9YxHhs2szHwtv1IW30KMabjjjX0GMlGJ9LLkBHkbaaEY/w9Tkj12XRLhRg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz", + "integrity": "sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/config-resolver": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.4.tgz", + "integrity": "sha512-VwiOk7TwXoE7NlNguV/aPq1hFH72tqkHCw8eWXbr2xHspRyyv9DLpLXhq+Ieje+NwoqXrY0xyQjPXdOE6cGcHA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/core": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.2.5.tgz", + "integrity": "sha512-0kqyj93/Aa30TEXnnWRBetN8fDGjFF+u8cdIiMI8YS6CrUF2dLTavRfHKfWh5cL5d6s2ZNyEnLjBitdcKmkETQ==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.8", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.6", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/credential-provider-imds": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.1.3.tgz", + "integrity": "sha512-U1Yrv6hx/mRK6k8AncuI6jLUx9rn0VVSd9NPEX6pyYFBfkSkChOc/n4zUb8alHUVg83TbI4OdZVo1X0Zfj3ijA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.1.tgz", + "integrity": "sha512-0w0bgUvZmfa0vHN8a+moByhCJT07WN6AHKEhFSOLsDpnszm+5dLVv5utGaqbhOrZ/aF5x3xuPMs/oMCd+4O5xg==", + "dependencies": { + "@smithy/protocol-http": "^4.0.3", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/hash-node": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.3.tgz", + "integrity": "sha512-2ctBXpPMG+B3BtWSGNnKELJ7SH9e4TNefJS0cd2eSkOOROeBnnVBnAy9LtJ8tY4vUEoe55N4CNPxzbWvR39iBw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/hash-node/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/invalid-dependency": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.3.tgz", + "integrity": "sha512-ID1eL/zpDULmHJbflb864k72/SNOZCADRc9i7Exq3RUNJw6raWUSlFEQ+3PX3EYs++bTxZB2dE9mEHTQLv61tw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/middleware-content-length": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.3.tgz", + "integrity": "sha512-Dbz2bzexReYIQDWMr+gZhpwBetNXzbhnEMhYKA6urqmojO14CsXjnsoPYO8UL/xxcawn8ZsuVU61ElkLSltIUQ==", + "dependencies": { + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/middleware-endpoint": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.0.4.tgz", + "integrity": "sha512-whUJMEPwl3ANIbXjBXZVdJNgfV2ZU8ayln7xUM47rXL2txuenI7jQ/VFFwCzy5lCmXScjp6zYtptW5Evud8e9g==", + "dependencies": { + "@smithy/middleware-serde": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/middleware-retry": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.8.tgz", + "integrity": "sha512-wmIw3t6ZbeqstUFdXtStzSSltoYrcfc28ndnr0mDSMmtMSRNduNbmneA7xiE224fVFXzbf24+0oREks1u2X7Mw==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/smithy-client": "^3.1.6", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/middleware-serde": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.3.tgz", + "integrity": "sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/middleware-stack": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.3.tgz", + "integrity": "sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/node-config-provider": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.3.tgz", + "integrity": "sha512-rxdpAZczzholz6CYZxtqDu/aKTxATD5DAUDVj7HoEulq+pDSQVWzbg0btZDlxeFfa6bb2b5tUvgdX5+k8jUqcg==", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/node-http-handler": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.1.2.tgz", + "integrity": "sha512-Td3rUNI7qqtoSLTsJBtsyfoG4cF/XMFmJr6Z2dX8QNzIi6tIW6YmuyFml8mJ2cNpyWNqITKbROMOFrvQjmsOvw==", + "dependencies": { + "@smithy/abort-controller": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/property-provider": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.3.tgz", + "integrity": "sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/protocol-http": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.0.3.tgz", + "integrity": "sha512-x5jmrCWwQlx+Zv4jAtc33ijJ+vqqYN+c/ZkrnpvEe/uDas7AT7A/4Rc2CdfxgWv4WFGmEqODIrrUToPN6DDkGw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/querystring-builder": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.3.tgz", + "integrity": "sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/querystring-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.3.tgz", + "integrity": "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/service-error-classification": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.3.tgz", + "integrity": "sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ==", + "dependencies": { + "@smithy/types": "^3.3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.3.tgz", + "integrity": "sha512-Z8Y3+08vgoDgl4HENqNnnzSISAaGrF2RoKupoC47u2wiMp+Z8P/8mDh1CL8+8ujfi2U5naNvopSBmP/BUj8b5w==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/signature-v4": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-3.1.2.tgz", + "integrity": "sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/smithy-client": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.1.6.tgz", + "integrity": "sha512-w9oboI661hfptr26houZ5mdKc//DMxkuOMXSaIiALqGn4bHYT9S4U69BBS6tHX4TZHgShmhcz0d6aXk7FY5soA==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/url-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.3.tgz", + "integrity": "sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A==", + "dependencies": { + "@smithy/querystring-parser": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-base64/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.8.tgz", + "integrity": "sha512-eLRHCvM1w3ZJkYcd60yKqM3d70dPB+071EDpf9ZGYqFed3xcm/+pWwNS/xM0JXRrjm0yAA19dWcdFN2IE/66pQ==", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.1.6", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.8.tgz", + "integrity": "sha512-Tajvdyg5+k77j6AOrwSCZgi7KdBizqPNs3HCnFGRoxDjzh+CjPLaLrXbIRB0lsAmqYmRHIU34IogByaqvDrkBQ==", + "dependencies": { + "@smithy/config-resolver": "^3.0.4", + "@smithy/credential-provider-imds": "^3.1.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.1.6", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-endpoints": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.0.4.tgz", + "integrity": "sha512-ZAtNf+vXAsgzgRutDDiklU09ZzZiiV/nATyqde4Um4priTmasDH+eLpp3tspL0hS2dEootyFMhu1Y6Y+tzpWBQ==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.3.tgz", + "integrity": "sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-retry": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.3.tgz", + "integrity": "sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w==", + "dependencies": { + "@smithy/service-error-classification": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-stream": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.0.6.tgz", + "integrity": "sha512-w9i//7egejAIvplX821rPWWgaiY1dxsQUw0hXX7qwa/uZ9U3zplqTQ871jWadkcVB9gFDhkPWYVZf4yfFbZ0xA==", + "dependencies": { + "@smithy/fetch-http-handler": "^3.2.1", + "@smithy/node-http-handler": "^3.1.2", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-stream/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-secrets-manager": { "version": "3.504.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.504.0.tgz", diff --git a/backend/package.json b/backend/package.json index b2f3cba0c..192102799 100644 --- a/backend/package.json +++ b/backend/package.json @@ -106,6 +106,7 @@ }, "dependencies": { "@aws-sdk/client-iam": "^3.525.0", + "@aws-sdk/client-kms": "^3.609.0", "@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", @@ -125,8 +126,8 @@ "@peculiar/asn1-schema": "^2.3.8", "@peculiar/x509": "^1.10.0", "@serdnam/pino-cloudwatch-transport": "^1.0.4", - "@team-plain/typescript-sdk": "^4.6.1", "@sindresorhus/slugify": "1.1.0", + "@team-plain/typescript-sdk": "^4.6.1", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", diff --git a/backend/src/ee/services/external-kms/external-kms-dal.ts b/backend/src/ee/services/external-kms/external-kms-dal.ts new file mode 100644 index 000000000..bb9a6ce8d --- /dev/null +++ b/backend/src/ee/services/external-kms/external-kms-dal.ts @@ -0,0 +1,47 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName, TKmsKeys } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TExternalKmsDALFactory = ReturnType; + +export const externalKmsDALFactory = (db: TDbClient) => { + const externalKmsOrm = ormify(db, TableName.ExternalKms); + + const find = async (filter: Partial, tx?: Knex) => { + try { + const result = await (tx || db.replicaNode())(TableName.ExternalKms) + .join(TableName.KmsKey, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`) + .where(filter) + .select(selectAllTableCols(TableName.KmsKey)) + .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") + ); + + return result.map((el) => ({ + id: el.id, + description: el.description, + isDisabled: el.isDisabled, + isReserved: el.isReserved, + orgId: el.orgId, + slug: el.slug, + externalKms: { + id: el.externalKmsId, + provider: el.externalKmsProvider, + status: el.externalKmsStatus, + statusDetails: el.externalKmsStatusDetails + } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find" }); + } + }; + + return { ...externalKmsOrm, find }; +}; diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts new file mode 100644 index 000000000..4b3e693f9 --- /dev/null +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -0,0 +1,298 @@ +import { ForbiddenError } from "@casl/ability"; +import slugify from "@sindresorhus/slugify"; + +import { BadRequestError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TExternalKmsDALFactory } from "./external-kms-dal"; +import { + TCreateExternalKmsDTO, + TDeleteExternalKmsDTO, + TGetExternalKmsByIdDTO, + TGetExternalKmsBySlugDTO, + TListExternalKmsDTO, + TUpdateExternalKmsDTO +} from "./external-kms-types"; +import { AwsKmsProviderFactory } from "./providers/aws-kms"; +import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model"; + +type TExternalKmsServiceFactoryDep = { + externalKmsDAL: TExternalKmsDALFactory; + kmsService: Pick; + kmsDAL: Pick; + permissionService: Pick; +}; + +export type TExternalKmsServiceFactory = ReturnType; + +export const externalKmsServiceFactory = ({ + externalKmsDAL, + permissionService, + kmsService, + kmsDAL +}: TExternalKmsServiceFactoryDep) => { + const create = async ({ + provider, + description, + actor, + slug, + actorId, + actorOrgId, + actorAuthMethod + }: TCreateExternalKmsDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(32)); + + let sanitizedProviderInput = ""; + switch (provider.type) { + case KmsProviders.Aws: + { + const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs }); + await externalKms.validateConnection(); + // if missing kms key this generate a new kms key id and returns new provider input + const newProviderInput = await externalKms.generateInputKmsKey(); + sanitizedProviderInput = JSON.stringify(newProviderInput); + } + break; + default: + throw new BadRequestError({ message: "external kms provided is invalid" }); + } + + const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId); + const { cipherTextBlob: encryptedProviderInputs } = await kmsService.encrypt({ + kmsId: orgKmsKeyId, + plainText: Buffer.from(sanitizedProviderInput, "utf8") + }); + + const externalKms = await externalKmsDAL.transaction(async (tx) => { + const kms = await kmsDAL.create( + { + isReserved: false, + description, + slug: kmsSlug, + orgId: actorOrgId + }, + tx + ); + const externalKmsCfg = await externalKmsDAL.create( + { + provider: provider.type, + encryptedProviderInputs, + kmsKeyId: kms.id + }, + tx + ); + return { ...kms, external: externalKmsCfg }; + }); + + return externalKms; + }; + + const updateById = async ({ + provider, + description, + actor, + id: kmsId, + slug, + actorId, + actorOrgId, + actorAuthMethod + }: TUpdateExternalKmsDTO) => { + const kmsDoc = await kmsDAL.findById(kmsId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + kmsDoc.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + const kmsSlug = slug ? slugify(slug) : undefined; + + const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); + if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); + + const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); + let sanitizedProviderInput = ""; + if (provider) { + const decryptedProviderInputBlob = await kmsService.decrypt({ + kmsId: orgDefaultKmsId, + cipherTextBlob: externalKmsDoc.encryptedProviderInputs + }); + + switch (provider.type) { + case KmsProviders.Aws: + { + const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString("utf8")) + ); + const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs }; + const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput }); + await externalKms.validateConnection(); + sanitizedProviderInput = JSON.stringify(updatedProviderInput); + } + break; + default: + throw new BadRequestError({ message: "external kms provided is invalid" }); + } + } + + let encryptedProviderInputs: Buffer | undefined; + if (sanitizedProviderInput) { + const { cipherTextBlob } = await kmsService.encrypt({ + kmsId: orgDefaultKmsId, + plainText: Buffer.from(sanitizedProviderInput, "utf8") + }); + encryptedProviderInputs = cipherTextBlob; + } + + const externalKms = await externalKmsDAL.transaction(async (tx) => { + const kms = await kmsDAL.updateById( + kmsDoc.id, + { + description, + slug: kmsSlug + }, + tx + ); + if (encryptedProviderInputs) { + const externalKmsCfg = await externalKmsDAL.updateById( + externalKmsDoc.id, + { + encryptedProviderInputs + }, + tx + ); + return { ...kms, external: externalKmsCfg }; + } + return { ...kms, external: externalKmsDoc }; + }); + + return externalKms; + }; + + const deleteById = async ({ actor, id: kmsId, actorId, actorOrgId, actorAuthMethod }: TDeleteExternalKmsDTO) => { + const kmsDoc = await kmsDAL.findById(kmsId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + kmsDoc.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); + if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); + + const externalKms = await externalKmsDAL.transaction(async (tx) => { + const kms = await kmsDAL.deleteById(kmsDoc.id, tx); + return { ...kms, external: externalKmsDoc }; + }); + + return externalKms; + }; + + const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListExternalKmsDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId }); + + return externalKmsDocs; + }; + + const findById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id: kmsId }: TGetExternalKmsByIdDTO) => { + const kmsDoc = await kmsDAL.findById(kmsId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + kmsDoc.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); + if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); + + const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); + const decryptedProviderInputBlob = await kmsService.decrypt({ + kmsId: orgDefaultKmsId, + cipherTextBlob: externalKmsDoc.encryptedProviderInputs + }); + switch (externalKmsDoc.provider) { + case KmsProviders.Aws: { + const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString("utf8")) + ); + return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } }; + } + default: + throw new BadRequestError({ message: "external kms provided is invalid" }); + } + }; + + const findBySlug = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + slug: kmsSlug + }: TGetExternalKmsBySlugDTO) => { + const kmsDoc = await kmsDAL.findOne({ slug: kmsSlug, orgId: actorOrgId }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + kmsDoc.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); + if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); + + const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); + const decryptedProviderInputBlob = await kmsService.decrypt({ + kmsId: orgDefaultKmsId, + cipherTextBlob: externalKmsDoc.encryptedProviderInputs + }); + switch (externalKmsDoc.provider) { + case KmsProviders.Aws: { + const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString("utf8")) + ); + return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } }; + } + default: + throw new BadRequestError({ message: "external kms provided is invalid" }); + } + }; + + return { + create, + updateById, + deleteById, + list, + findById, + findBySlug + }; +}; diff --git a/backend/src/ee/services/external-kms/external-kms-types.ts b/backend/src/ee/services/external-kms/external-kms-types.ts new file mode 100644 index 000000000..6254a80ef --- /dev/null +++ b/backend/src/ee/services/external-kms/external-kms-types.ts @@ -0,0 +1,30 @@ +import { TOrgPermission } from "@app/lib/types"; + +import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model"; + +export type TCreateExternalKmsDTO = { + slug?: string; + description?: string; + provider: TExternalKmsInputSchema; +} & Omit; + +export type TUpdateExternalKmsDTO = { + id: string; + slug?: string; + description?: string; + provider?: TExternalKmsInputUpdateSchema; +} & Omit; + +export type TDeleteExternalKmsDTO = { + id: string; +} & Omit; + +export type TListExternalKmsDTO = Omit; + +export type TGetExternalKmsByIdDTO = { + id: string; +} & Omit; + +export type TGetExternalKmsBySlugDTO = { + slug: string; +} & Omit; diff --git a/backend/src/ee/services/external-kms/providers/aws-kms.ts b/backend/src/ee/services/external-kms/providers/aws-kms.ts new file mode 100644 index 000000000..5a437fd2d --- /dev/null +++ b/backend/src/ee/services/external-kms/providers/aws-kms.ts @@ -0,0 +1,102 @@ +import { CreateKeyCommand, DecryptCommand, DescribeKeyCommand, EncryptCommand, KMSClient } from "@aws-sdk/client-kms"; +import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; +import { randomUUID } from "crypto"; + +import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model"; + +const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => { + if (providerInputs.credential.type === KmsAwsCredentialType.AssumeRole) { + const awsCredential = providerInputs.credential.data; + const stsClient = new STSClient({ + region: providerInputs.awsRegion + }); + const command = new AssumeRoleCommand({ + RoleArn: awsCredential.assumeRoleArn, + RoleSessionName: `infisical-kms-${randomUUID()}`, + DurationSeconds: 900, // 15mins + ExternalId: awsCredential.externalId + }); + const response = await stsClient.send(command); + if (!response.Credentials?.AccessKeyId || !response.Credentials?.SecretAccessKey) + throw new Error("Failed to assume role"); + + const kmsClient = new KMSClient({ + region: providerInputs.awsRegion, + credentials: { + accessKeyId: response.Credentials.AccessKeyId, + secretAccessKey: response.Credentials.SecretAccessKey, + sessionToken: response.Credentials.SessionToken, + expiration: response.Credentials.Expiration + } + }); + return kmsClient; + } + const awsCredential = providerInputs.credential.data; + const kmsClient = new KMSClient({ + region: providerInputs.awsRegion, + credentials: { + accessKeyId: awsCredential.accessKey, + secretAccessKey: awsCredential.secretKey + } + }); + return kmsClient; +}; + +type AwsKmsProviderArgs = { + inputs: unknown; +}; +type TAwsKmsProviderFactoryReturn = TExternalKmsProviderFns & { + generateInputKmsKey: () => Promise; +}; + +export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Promise => { + const providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs); + const awsClient = await getAwsKmsClient(providerInputs); + + const generateInputKmsKey = async () => { + if (providerInputs.kmsKeyId) return providerInputs; + + const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] }); + const kmsKey = await awsClient.send(command); + if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key"); + + return { ...providerInputs, kmsKeyId: kmsKey.KeyMetadata?.KeyId }; + }; + + const validateConnection = async () => { + const command = new DescribeKeyCommand({ + KeyId: providerInputs.kmsKeyId + }); + const isConnected = await awsClient.send(command).then(() => true); + return isConnected; + }; + + const encrypt = async (data: Buffer) => { + const command = new EncryptCommand({ + KeyId: providerInputs.kmsKeyId, + Plaintext: data + }); + const encryptionCommand = await awsClient.send(command); + if (!encryptionCommand.CiphertextBlob) throw new Error("encryption failed"); + + return { encryptedBlob: Buffer.from(encryptionCommand.CiphertextBlob) }; + }; + + const decrypt = async (encryptedBlob: Buffer) => { + const command = new DecryptCommand({ + KeyId: providerInputs.kmsKeyId, + CiphertextBlob: encryptedBlob + }); + const decryptionCommand = await awsClient.send(command); + if (!decryptionCommand.Plaintext) throw new Error("decryption failed"); + + return { data: Buffer.from(decryptionCommand.Plaintext) }; + }; + + return { + generateInputKmsKey, + validateConnection, + encrypt, + decrypt + }; +}; diff --git a/backend/src/ee/services/external-kms/providers/model.ts b/backend/src/ee/services/external-kms/providers/model.ts new file mode 100644 index 000000000..5a87e0c98 --- /dev/null +++ b/backend/src/ee/services/external-kms/providers/model.ts @@ -0,0 +1,61 @@ +import { z } from "zod"; + +export enum KmsProviders { + Aws = "aws" +} + +export enum KmsAwsCredentialType { + AssumeRole = "assume-role", + AccessKey = "access-key" +} + +export const ExternalKmsAwsSchema = z.object({ + credential: z + .discriminatedUnion("type", [ + z.object({ + type: z.literal(KmsAwsCredentialType.AccessKey), + data: z.object({ + accessKey: z.string().trim().min(1).describe("AWS user account access key"), + secretKey: z.string().trim().min(1).describe("AWS user account secret key") + }) + }), + z.object({ + type: z.literal(KmsAwsCredentialType.AssumeRole), + data: z.object({ + assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"), + externalId: z + .string() + .trim() + .min(1) + .optional() + .describe("AWS assume role external id for furthur security in authentication") + }) + }) + ]) + .describe("AWS credential information to connect"), + awsRegion: z.string().min(1).trim().describe("AWS region to connect"), + kmsKeyId: z + .string() + .trim() + .optional() + .describe("A pre existing AWS KMS key id to be used for encryption. If not provided a kms key will be generated.") +}); +export type TExternalKmsAwsSchema = z.infer; + +// The root schema of the JSON +export const ExternalKmsInputSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema }) +]); +export type TExternalKmsInputSchema = z.infer; + +export const ExternalKmsInputUpdateSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema.partial() }) +]); +export type TExternalKmsInputUpdateSchema = z.infer; + +// generic function shared by all provider +export type TExternalKmsProviderFns = { + validateConnection: () => Promise; + encrypt: (data: Buffer) => Promise<{ encryptedBlob: Buffer }>; + decrypt: (encryptedBlob: Buffer) => Promise<{ data: Buffer }>; +}; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index f4e423797..72a35a326 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -109,6 +109,9 @@ export const permissionServiceFactory = ({ authMethod: ActorAuthMethod, userOrgId?: string ) => { + // when token is scoped, ensure the passed org id is same as user org id + if (userOrgId && userOrgId !== orgId) + throw new BadRequestError({ message: "Invalid user token. Scoped to different organization." }); const membership = await permissionDAL.getOrgPermission(userId, orgId); if (!membership) throw new UnauthorizedError({ name: "User not in org" }); if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { From 654dd97793d8323d5641eff00f19e81b1c14d86a Mon Sep 17 00:00:00 2001 From: = Date: Wed, 10 Jul 2024 12:13:22 +0530 Subject: [PATCH 4/6] feat: external kms router defined not plugged in --- backend/src/@types/fastify.d.ts | 2 + .../src/ee/routes/v1/external-kms-router.ts | 190 ++++++++++++++++++ backend/src/server/routes/index.ts | 23 ++- 3 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 backend/src/ee/routes/v1/external-kms-router.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 7a5682b30..b55e88144 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -9,6 +9,7 @@ import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; +import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; @@ -161,6 +162,7 @@ declare module "fastify" { secretSharing: TSecretSharingServiceFactory; rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; + externalKms: TExternalKmsServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/ee/routes/v1/external-kms-router.ts b/backend/src/ee/routes/v1/external-kms-router.ts new file mode 100644 index 000000000..fee358b0b --- /dev/null +++ b/backend/src/ee/routes/v1/external-kms-router.ts @@ -0,0 +1,190 @@ +import { z } from "zod"; + +import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; +import { + ExternalKmsAwsSchema, + ExternalKmsInputSchema, + ExternalKmsInputUpdateSchema +} from "@app/ee/services/external-kms/providers/model"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const sanitizedExternalSchema = KmsKeysSchema.extend({ + external: ExternalKmsSchema.pick({ + id: true, + status: true, + statusDetails: true, + provider: true + }) +}); + +const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({ + external: ExternalKmsSchema.pick({ + id: true, + status: true, + statusDetails: true, + provider: true + }).extend({ + providerInput: ExternalKmsAwsSchema + }) +}); + +export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + slug: z.string().min(1).trim().optional(), + description: z.string().min(1).trim().optional(), + provider: ExternalKmsInputSchema + }), + response: { + 200: z.object({ + externalKms: sanitizedExternalSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.create({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.body.slug, + provider: req.body.provider, + description: req.body.description + }); + return { externalKms }; + } + }); + + server.route({ + method: "PATCH", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + body: z.object({ + slug: z.string().min(1).trim().optional(), + description: z.string().min(1).trim().optional(), + provider: ExternalKmsInputUpdateSchema + }), + response: { + 200: z.object({ + externalKms: sanitizedExternalSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.updateById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.body.slug, + provider: req.body.provider, + description: req.body.description, + id: req.params.id + }); + return { externalKms }; + } + }); + + server.route({ + method: "DELETE", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + response: { + 200: z.object({ + externalKms: sanitizedExternalSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.deleteById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + return { externalKms }; + } + }); + + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + response: { + 200: z.object({ + externalKms: sanitizedExternalSchemaForGetById + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.findById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + return { externalKms }; + } + }); + + server.route({ + method: "GET", + url: "/slug/:slug", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + slug: z.string().trim().min(1) + }), + response: { + 200: z.object({ + externalKms: sanitizedExternalSchemaForGetById + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.findBySlug({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.params.slug + }); + return { externalKms }; + } + }); +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 069d61569..050d4f752 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -22,6 +22,8 @@ import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/pro import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal"; import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue"; import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; +import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal"; +import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -114,7 +116,8 @@ import { integrationDALFactory } from "@app/services/integration/integration-dal import { integrationServiceFactory } from "@app/services/integration/integration-service"; import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal"; import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; -import { kmsDALFactory } from "@app/services/kms/kms-dal"; +import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; +import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { kmsServiceFactory } from "@app/services/kms/kms-service"; import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal"; @@ -285,7 +288,9 @@ export const registerRoutes = async ( const dynamicSecretDAL = dynamicSecretDALFactory(db); const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db); - const kmsDAL = kmsDALFactory(db); + const kmsDAL = kmskeyDALFactory(db); + const internalKmsDAL = internalKmsDALFactory(db); + const externalKmsDAL = externalKmsDALFactory(db); const kmsRootConfigDAL = kmsRootConfigDALFactory(db); const permissionService = permissionServiceFactory({ @@ -299,7 +304,16 @@ export const registerRoutes = async ( const kmsService = kmsServiceFactory({ kmsRootConfigDAL, keyStore, - kmsDAL + kmsDAL, + internalKmsDAL, + orgDAL, + projectDAL + }); + const externalKmsService = externalKmsServiceFactory({ + kmsDAL, + kmsService, + permissionService, + externalKmsDAL }); const trustedIpService = trustedIpServiceFactory({ @@ -1012,7 +1026,8 @@ export const registerRoutes = async ( projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService, identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService, secretSharing: secretSharingService, - userEngagement: userEngagementService + userEngagement: userEngagementService, + externalKms: externalKmsService }); const cronJobs: CronJob[] = []; From 08f0bf9c67879dc9b4f47efc7acd086b6964a665 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 10 Jul 2024 12:45:02 +0530 Subject: [PATCH 5/6] feat: fixed migration down missing orgid --- .../migrations/20240708100026_external-kms.ts | 248 +++++++++++------- backend/src/db/schemas/kms-keys.ts | 2 + backend/src/db/schemas/models.ts | 5 +- 3 files changed, 163 insertions(+), 92 deletions(-) diff --git a/backend/src/db/migrations/20240708100026_external-kms.ts b/backend/src/db/migrations/20240708100026_external-kms.ts index 3c4bceb93..63390a635 100644 --- a/backend/src/db/migrations/20240708100026_external-kms.ts +++ b/backend/src/db/migrations/20240708100026_external-kms.ts @@ -5,13 +5,11 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TableName } from "../schemas"; -export async function up(knex: Knex): Promise { - // rename old kms key table to internal kms table - // the kms key table would be a container to hold external and internal respectively +const createInternalKmsTableAndBackfillData = async (knex: Knex) => { const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); - const doesOldKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.KmsKeyVersion); const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); + // building the internal kms table by filling from old kms table if (doesOldKmsKeyTableExist && !doesInternalKmsTableExist) { await knex.schema.createTable(TableName.InternalKms, (tb) => { tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); @@ -21,7 +19,8 @@ export async function up(knex: Knex): Promise { tb.uuid("kmsKeyId").unique().notNullable(); tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE"); }); - // copy the old kms and build the data + + // copy the old kms and backfill const oldKmsKey = await knex(TableName.KmsKey).select("version", "encryptedKey", "encryptionAlgorithm", "id"); if (oldKmsKey.length) { await knex(TableName.InternalKms).insert( @@ -33,51 +32,30 @@ export async function up(knex: Knex): Promise { })) ); } + } +}; - if (doesOldKmsKeyVersionTableExist) { - // because we haven't started using versioning for kms thus no data exist - await knex.schema.renameTable(TableName.KmsKeyVersion, TableName.InternalKmsKeyVersion); - await knex.schema.alterTable(TableName.InternalKmsKeyVersion, (tb) => { - tb.dropColumn("kmsKeyId"); +const renameKmsKeyVersionTableAsInternalKmsKeyVersion = async (knex: Knex) => { + const doesOldKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.KmsKeyVersion); + const doesNewKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.InternalKmsKeyVersion); + + if (doesOldKmsKeyVersionTableExist && !doesNewKmsKeyVersionTableExist) { + // because we haven't started using versioning for kms thus no data exist + await knex.schema.renameTable(TableName.KmsKeyVersion, TableName.InternalKmsKeyVersion); + const hasKmsKeyIdColumn = await knex.schema.hasColumn(TableName.InternalKmsKeyVersion, "kmsKeyId"); + const hasInternalKmsIdColumn = await knex.schema.hasColumn(TableName.InternalKmsKeyVersion, "internalKmsId"); + + await knex.schema.alterTable(TableName.InternalKmsKeyVersion, (tb) => { + if (hasKmsKeyIdColumn) tb.dropColumn("kmsKeyId"); + if (!hasInternalKmsIdColumn) { tb.uuid("internalKmsId").notNullable(); tb.foreign("internalKmsId").references("id").inTable(TableName.InternalKms).onDelete("CASCADE"); - }); - } - - await knex.schema.alterTable(TableName.KmsKey, (tb) => { - tb.string("slug", 32); - tb.dropColumn("encryptedKey"); - tb.dropColumn("encryptionAlgorithm"); - tb.dropColumn("version"); - }); - // backfill all org id in kms key - await knex(TableName.KmsKey) - .whereNull("orgId") - .update({ - // eslint-disable-next-line - // @ts-ignore because generate schema happens after this - orgId: knex(TableName.Project) - .select("orgId") - .where("id", knex.raw("??", [`${TableName.KmsKey}.projectId`])) - }); - // backfill slugs in kms - const missingSlugs = await knex(TableName.KmsKey).whereNull("slug").select("id"); - if (missingSlugs.length) { - await knex(TableName.KmsKey) - // eslint-disable-next-line - // @ts-ignore because generate schema happens after this - .insert(missingSlugs.map(({ id }) => ({ id, slug: slugify(alphaNumericNanoId(32)) }))) - .onConflict("id") - .merge(); - } - - await knex.schema.alterTable(TableName.KmsKey, (tb) => { - tb.uuid("orgId").notNullable().alter(); - tb.string("slug", 32).notNullable().alter(); - tb.dropColumn("projectId"); + } }); } +}; +const createExternalKmsKeyTable = async (knex: Knex) => { const doesExternalKmsServiceExist = await knex.schema.hasTable(TableName.ExternalKms); if (!doesExternalKmsServiceExist) { await knex.schema.createTable(TableName.ExternalKms, (tb) => { @@ -90,6 +68,72 @@ export async function up(knex: Knex): Promise { tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE"); }); } +}; + +const removeNonRequiredFieldsFromKmsKeyTableAndBackfillRequiredData = async (knex: Knex) => { + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + + // building the internal kms table by filling from old kms table + if (doesOldKmsKeyTableExist) { + const hasSlugColumn = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + const hasEncryptedKeyColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptedKey"); + const hasEncryptionAlgorithmColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptionAlgorithm"); + const hasVersionColumn = await knex.schema.hasColumn(TableName.KmsKey, "version"); + const hasTimestamps = await knex.schema.hasColumn(TableName.KmsKey, "createdAt"); + const hasProjectId = await knex.schema.hasColumn(TableName.KmsKey, "projectId"); + const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId"); + + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + if (!hasSlugColumn) tb.string("slug", 32); + if (hasEncryptedKeyColumn) tb.dropColumn("encryptedKey"); + if (hasEncryptionAlgorithmColumn) tb.dropColumn("encryptionAlgorithm"); + if (hasVersionColumn) tb.dropColumn("version"); + if (!hasTimestamps) tb.timestamps(true, true, true); + }); + + // backfill all org id in kms key because its gonna be changed to non nullable + if (hasProjectId && hasOrgId) { + await knex(TableName.KmsKey) + .whereNull("orgId") + .update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + orgId: knex(TableName.Project) + .select("orgId") + .where("id", knex.raw("??", [`${TableName.KmsKey}.projectId`])) + }); + } + + // backfill slugs in kms + const missingSlugs = await knex(TableName.KmsKey).whereNull("slug").select("id"); + if (missingSlugs.length) { + await knex(TableName.KmsKey) + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + .insert(missingSlugs.map(({ id }) => ({ id, slug: slugify(alphaNumericNanoId(8).toLowerCase()) }))) + .onConflict("id") + .merge(); + } + + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + if (hasOrgId) tb.uuid("orgId").notNullable().alter(); + tb.string("slug", 32).notNullable().alter(); + if (hasProjectId) tb.dropColumn("projectId"); + if (hasOrgId) tb.unique(["orgId", "slug"]); + }); + } +}; + +/* + * The goal for this migration is split the existing kms key into three table + * the kms-key table would be a container table that contains + * the internal kms key table and external kms table + */ +export async function up(knex: Knex): Promise { + await createInternalKmsTableAndBackfillData(knex); + await renameKmsKeyVersionTableAsInternalKmsKeyVersion(knex); + await removeNonRequiredFieldsFromKmsKeyTableAndBackfillRequiredData(knex); + await createExternalKmsKeyTable(knex); const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId"); if (!doesOrgKmsKeyExist) { @@ -108,48 +152,54 @@ export async function up(knex: Knex): Promise { } } -export async function down(knex: Knex): Promise { - const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId"); - if (doesOrgKmsKeyExist) { - await knex.schema.alterTable(TableName.Organization, (tb) => { - tb.dropColumn("kmsDefaultKeyId"); - }); - } - - const doesProjectKmsSecretManagerKeyExist = await knex.schema.hasColumn(TableName.Project, "kmsSecretManagerKeyId"); - if (doesProjectKmsSecretManagerKeyExist) { - await knex.schema.alterTable(TableName.Project, (tb) => { - tb.dropColumn("kmsSecretManagerKeyId"); - }); - } - +const renameInternalKmsKeyVersionBackToKmsKeyVersion = async (knex: Knex) => { const doesInternalKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.InternalKmsKeyVersion); - const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); - if (doesInternalKmsKeyVersionTableExist) { + const doesKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.KmsKeyVersion); + if (doesInternalKmsKeyVersionTableExist && !doesKmsKeyVersionTableExist) { // because we haven't started using versioning for kms thus no data exist await knex.schema.renameTable(TableName.InternalKmsKeyVersion, TableName.KmsKeyVersion); - await knex.schema.alterTable(TableName.KmsKeyVersion, (tb) => { - tb.dropColumn("internalKmsId"); - tb.uuid("kmsKeyId").notNullable(); - tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE"); - }); - } + const hasInternalKmsIdColumn = await knex.schema.hasColumn(TableName.KmsKeyVersion, "internalKmsId"); + const hasKmsKeyIdColumn = await knex.schema.hasColumn(TableName.KmsKeyVersion, "kmsKeyId"); - const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); - const doesKmsSlugExist = await knex.schema.hasColumn(TableName.KmsKey, "slug"); - if (doesInternalKmsTableExist && doesOldKmsKeyTableExist) { - // converting kms key to old one - // backfill so not setting it as not nullable - await knex.schema.alterTable(TableName.KmsKey, (tb) => { - tb.binary("encryptedKey"); - tb.string("encryptionAlgorithm"); - tb.integer("version").defaultTo(1); - tb.string("projectId"); - tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); - if (doesKmsSlugExist) { - tb.dropColumn("slug"); + await knex.schema.alterTable(TableName.KmsKeyVersion, (tb) => { + if (hasInternalKmsIdColumn) tb.dropColumn("internalKmsId"); + if (!hasKmsKeyIdColumn) { + tb.uuid("kmsKeyId").notNullable(); + tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE"); } }); + } +}; + +const bringBackKmsKeyFields = async (knex: Knex) => { + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); + if (doesOldKmsKeyTableExist && doesInternalKmsTableExist) { + const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + const hasEncryptedKeyColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptedKey"); + const hasEncryptionAlgorithmColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptionAlgorithm"); + const hasVersionColumn = await knex.schema.hasColumn(TableName.KmsKey, "version"); + const hasNullableOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId"); + const hasProjectIdColumn = await knex.schema.hasColumn(TableName.KmsKey, "projectId"); + + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + if (!hasEncryptedKeyColumn) tb.binary("encryptedKey"); + if (!hasEncryptionAlgorithmColumn) tb.string("encryptionAlgorithm"); + if (!hasVersionColumn) tb.integer("version").defaultTo(1); + if (hasNullableOrgId) tb.uuid("orgId").nullable().alter(); + if (!hasProjectIdColumn) { + tb.string("projectId"); + tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + } + if (hasSlug) tb.dropColumn("slug"); + }); + } +}; + +const backfillKmsKeyFromInternalKmsTable = async (knex: Knex) => { + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); + if (doesInternalKmsTableExist && doesOldKmsKeyTableExist) { // backfill kms key with internal kms data await knex(TableName.KmsKey).update({ // eslint-disable-next-line @@ -168,21 +218,39 @@ export async function down(knex: Knex): Promise { .select("id") .where("kmsCertificateKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])) }); + } +}; + +export async function down(knex: Knex): Promise { + const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId"); + if (doesOrgKmsKeyExist) { + await knex.schema.alterTable(TableName.Organization, (tb) => { + tb.dropColumn("kmsDefaultKeyId"); + }); + } + + const doesProjectKmsSecretManagerKeyExist = await knex.schema.hasColumn(TableName.Project, "kmsSecretManagerKeyId"); + if (doesProjectKmsSecretManagerKeyExist) { + await knex.schema.alterTable(TableName.Project, (tb) => { + tb.dropColumn("kmsSecretManagerKeyId"); + }); + } + + await renameInternalKmsKeyVersionBackToKmsKeyVersion(knex); + await bringBackKmsKeyFields(knex); + await backfillKmsKeyFromInternalKmsTable(knex); + + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + if (doesOldKmsKeyTableExist) { await knex.schema.alterTable(TableName.KmsKey, (tb) => { tb.binary("encryptedKey").notNullable().alter(); tb.string("encryptionAlgorithm").notNullable().alter(); }); - await knex.schema.alterTable(TableName.InternalKms, (tb) => { - tb.dropForeign("kmsKeyId"); - }); - await knex.schema.dropTable(TableName.InternalKms); } + const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); + if (doesInternalKmsTableExist) await knex.schema.dropTable(TableName.InternalKms); + const doesExternalKmsServiceExist = await knex.schema.hasTable(TableName.ExternalKms); - if (doesExternalKmsServiceExist) { - await knex.schema.alterTable(TableName.ExternalKms, (tb) => { - tb.dropForeign("kmsKeyId"); - }); - await knex.schema.dropTable(TableName.ExternalKms); - } + if (doesExternalKmsServiceExist) await knex.schema.dropTable(TableName.ExternalKms); } diff --git a/backend/src/db/schemas/kms-keys.ts b/backend/src/db/schemas/kms-keys.ts index 5e8dcf166..99df71f8d 100644 --- a/backend/src/db/schemas/kms-keys.ts +++ b/backend/src/db/schemas/kms-keys.ts @@ -13,6 +13,8 @@ export const KmsKeysSchema = z.object({ isDisabled: z.boolean().default(false).nullable().optional(), isReserved: z.boolean().default(true).nullable().optional(), orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), slug: z.string() }); diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 646e0455e..a7e91a6b8 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -95,10 +95,11 @@ export enum TableName { // KMS Service KmsServerRootConfig = "kms_root_config", KmsKey = "kms_keys", - KmsKeyVersion = "kms_key_versions", ExternalKms = "external_kms", InternalKms = "internal_kms", - InternalKmsKeyVersion = "internal_kms_key_version" + InternalKmsKeyVersion = "internal_kms_key_version", + // @depreciated + KmsKeyVersion = "kms_key_versions" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; From 5d4c7c2cbf9965de45badee9fd682db9cb6308e4 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 10 Jul 2024 15:23:02 +0530 Subject: [PATCH 6/6] feat: added encrypt/decrypt with key for kms service and changed kms encrytion to hoc to avoid back to back db calls --- .../certificate-authority-crl-service.ts | 8 +- .../external-kms/external-kms-service.ts | 33 ++++--- .../certificate-authority-fns.ts | 29 +++--- .../certificate-authority-queue.ts | 14 +-- .../certificate-authority-service.ts | 52 ++++++----- .../certificate-authority-types.ts | 6 +- .../certificate/certificate-service.ts | 10 ++- backend/src/services/kms/kms-service.ts | 89 +++++++++---------- backend/src/services/kms/kms-types.ts | 19 +--- 9 files changed, 139 insertions(+), 121 deletions(-) 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 c8b56561e..917c55a0f 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 @@ -17,7 +17,7 @@ type TCertificateAuthorityCrlServiceFactoryDep = { certificateAuthorityDAL: Pick; certificateAuthorityCrlDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; permissionService: Pick; licenseService: Pick; }; @@ -68,11 +68,11 @@ export const certificateAuthorityCrlServiceFactory = ({ kmsService }); - const decryptedCrl = await kmsService.decrypt({ - kmsId: keyId, - cipherTextBlob: caCrl.encryptedCrl + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId }); + const decryptedCrl = kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl }); const crl = new x509.X509Crl(decryptedCrl); const base64crl = crl.toString("base64"); diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index 4b3e693f9..168f6a1a7 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -22,7 +22,7 @@ import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model"; type TExternalKmsServiceFactoryDep = { externalKmsDAL: TExternalKmsDALFactory; - kmsService: Pick; + kmsService: Pick; kmsDAL: Pick; permissionService: Pick; }; @@ -70,8 +70,10 @@ export const externalKmsServiceFactory = ({ } const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId); - const { cipherTextBlob: encryptedProviderInputs } = await kmsService.encrypt({ - kmsId: orgKmsKeyId, + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: orgKmsKeyId + }); + const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({ plainText: Buffer.from(sanitizedProviderInput, "utf8") }); @@ -126,8 +128,10 @@ export const externalKmsServiceFactory = ({ const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); let sanitizedProviderInput = ""; if (provider) { - const decryptedProviderInputBlob = await kmsService.decrypt({ - kmsId: orgDefaultKmsId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: orgDefaultKmsId + }); + const decryptedProviderInputBlob = kmsDecryptor({ cipherTextBlob: externalKmsDoc.encryptedProviderInputs }); @@ -150,8 +154,10 @@ export const externalKmsServiceFactory = ({ let encryptedProviderInputs: Buffer | undefined; if (sanitizedProviderInput) { - const { cipherTextBlob } = await kmsService.encrypt({ - kmsId: orgDefaultKmsId, + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: orgDefaultKmsId + }); + const { cipherTextBlob } = kmsEncryptor({ plainText: Buffer.from(sanitizedProviderInput, "utf8") }); encryptedProviderInputs = cipherTextBlob; @@ -234,8 +240,10 @@ export const externalKmsServiceFactory = ({ if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); - const decryptedProviderInputBlob = await kmsService.decrypt({ - kmsId: orgDefaultKmsId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: orgDefaultKmsId + }); + const decryptedProviderInputBlob = kmsDecryptor({ cipherTextBlob: externalKmsDoc.encryptedProviderInputs }); switch (externalKmsDoc.provider) { @@ -271,10 +279,13 @@ export const externalKmsServiceFactory = ({ if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); - const decryptedProviderInputBlob = await kmsService.decrypt({ - kmsId: orgDefaultKmsId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: orgDefaultKmsId + }); + const decryptedProviderInputBlob = kmsDecryptor({ cipherTextBlob: externalKmsDoc.encryptedProviderInputs }); + switch (externalKmsDoc.provider) { case KmsProviders.Aws: { const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index cf42a058e..9f98dcb83 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -75,8 +75,10 @@ export const getCaCredentials = async ({ kmsService }); - const decryptedPrivateKey = await kmsService.decrypt({ - kmsId: keyId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + const decryptedPrivateKey = kmsDecryptor({ cipherTextBlob: caSecret.encryptedPrivateKey }); @@ -123,15 +125,17 @@ export const getCaCertChain = async ({ kmsService }); - const decryptedCaCert = await kmsService.decrypt({ - kmsId: keyId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + + const decryptedCaCert = kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificate }); const caCertObj = new x509.X509Certificate(decryptedCaCert); - const decryptedChain = await kmsService.decrypt({ - kmsId: keyId, + const decryptedChain = kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificateChain }); @@ -168,8 +172,11 @@ export const rebuildCaCrl = async ({ kmsService }); - const privateKey = await kmsService.decrypt({ - kmsId: keyId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + + const privateKey = kmsDecryptor({ cipherTextBlob: caSecret.encryptedPrivateKey }); @@ -200,8 +207,10 @@ export const rebuildCaCrl = async ({ signingKey: sk }); - const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({ - kmsId: keyId, + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: keyId + }); + const { cipherTextBlob: encryptedCrl } = 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 384f45c09..30da119d0 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -25,7 +25,7 @@ type TCertificateAuthorityQueueFactoryDep = { certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory; certificateDAL: TCertificateDALFactory; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; queueService: TQueueServiceFactory; }; export type TCertificateAuthorityQueueFactory = ReturnType; @@ -88,8 +88,10 @@ export const certificateAuthorityQueueFactory = ({ kmsService }); - const privateKey = await kmsService.decrypt({ - kmsId: keyId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + const privateKey = kmsDecryptor({ cipherTextBlob: caSecret.encryptedPrivateKey }); @@ -120,8 +122,10 @@ export const certificateAuthorityQueueFactory = ({ signingKey: sk }); - const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({ - kmsId: keyId, + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: keyId + }); + const { cipherTextBlob: encryptedCrl } = 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 7d87545e2..afc8d7efb 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -53,7 +53,7 @@ type TCertificateAuthorityServiceFactoryDep = { certificateDAL: Pick; certificateBodyDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; permissionService: Pick; }; @@ -154,11 +154,14 @@ export const certificateAuthorityServiceFactory = ({ tx ); - const keyId = await getProjectKmsCertificateKeyId({ + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: project.id, projectDAL, kmsService }); + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); if (type === CaType.ROOT) { // note: create self-signed cert only applicable for root CA @@ -178,13 +181,11 @@ export const certificateAuthorityServiceFactory = ({ ] }); - const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ plainText: Buffer.from(new Uint8Array(cert.rawData)) }); - const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({ plainText: Buffer.alloc(0) }); @@ -208,8 +209,7 @@ export const certificateAuthorityServiceFactory = ({ signingKey: keys.privateKey }); - const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ plainText: Buffer.from(new Uint8Array(crl.rawData)) }); @@ -224,8 +224,7 @@ export const certificateAuthorityServiceFactory = ({ // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey const skObj = KeyObject.from(keys.privateKey); - const { cipherTextBlob: encryptedPrivateKey } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedPrivateKey } = kmsEncryptor({ plainText: skObj.export({ type: "pkcs8", format: "der" @@ -449,15 +448,17 @@ export const certificateAuthorityServiceFactory = ({ const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - const keyId = await getProjectKmsCertificateKeyId({ + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, kmsService }); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); - const decryptedCaCert = await kmsService.decrypt({ - kmsId: keyId, + const decryptedCaCert = kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificate }); @@ -605,19 +606,20 @@ export const certificateAuthorityServiceFactory = ({ dn: parentCertSubject }); - const keyId = await getProjectKmsCertificateKeyId({ + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, kmsService }); + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); - const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ plainText: Buffer.from(new Uint8Array(certObj.rawData)) }); - const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({ plainText: Buffer.from(certificateChain) }); @@ -682,14 +684,16 @@ export const certificateAuthorityServiceFactory = ({ const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const keyId = await getProjectKmsCertificateKeyId({ + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, kmsService }); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); - const decryptedCaCert = await kmsService.decrypt({ - kmsId: keyId, + const decryptedCaCert = kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificate }); @@ -796,8 +800,10 @@ export const certificateAuthorityServiceFactory = ({ const skLeafObj = KeyObject.from(leafKeys.privateKey); const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; - const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({ - kmsId: keyId, + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }); diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 8af8b679c..7818c3a3e 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -95,7 +95,7 @@ export type TGetCaCredentialsDTO = { certificateAuthorityDAL: Pick; certificateAuthoritySecretDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; }; export type TGetCaCertChainDTO = { @@ -103,7 +103,7 @@ export type TGetCaCertChainDTO = { certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; }; export type TRebuildCaCrlDTO = { @@ -113,7 +113,7 @@ export type TRebuildCaCrlDTO = { certificateAuthoritySecretDAL: Pick; projectDAL: Pick; certificateDAL: Pick; - kmsService: Pick; + kmsService: Pick; }; export type TRotateCaCrlTriggerDTO = { diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index ba865caa1..401a55cc9 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -25,7 +25,7 @@ type TCertificateServiceFactoryDep = { certificateAuthorityCrlDAL: Pick; certificateAuthoritySecretDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; permissionService: Pick; }; @@ -164,14 +164,16 @@ export const certificateServiceFactory = ({ const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); - const keyId = await getProjectKmsCertificateKeyId({ + const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, kmsService }); - const decryptedCert = await kmsService.decrypt({ - kmsId: keyId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKeyId + }); + const decryptedCert = kmsDecryptor({ cipherTextBlob: certBody.encryptedCertificate }); diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 0c0f48f97..468666221 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -14,7 +14,13 @@ 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 { EncryptionMode, TGenerateKMSDTO, TKmsServiceDecryptionDTO, TKmsServiceEncryptionDTO } from "./kms-types"; +import { + TDecryptWithKeyDTO, + TDecryptWithKmsDTO, + TEncryptionWithKeyDTO, + TEncryptWithKmsDTO, + TGenerateKMSDTO +} from "./kms-types"; type TKmsServiceFactoryDep = { kmsDAL: TKmsKeyDALFactory; @@ -74,64 +80,55 @@ export const kmsServiceFactory = ({ return doc; }; - /* - * 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) => { + 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); - // instead of using kms key encrypt with the provided key - if (encryptionDetails.type === EncryptionMode.EncryptionKey) { - const { plainText, encryptionKey } = encryptionDetails; + return ({ plainText }: Pick) => { + const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey); - 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 - const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 - const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]); - return { cipherTextBlob }; + }; }; - /* - * 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) => { + 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); - 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; - } + return ({ plainText }: Pick) => { + const encryptedPlainTextBlob = cipher.encrypt(plainText, key); + // 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 { cipherTextBlob: versionedCipherTextBlob, kmsId } = encryptionDetails; + 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); - const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); - const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey); - return decryptedBlob; + 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); + + return ({ cipherTextBlob: versionedCipherTextBlob }: Pick) => { + const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); + const decryptedBlob = cipher.decrypt(cipherTextBlob, key); + return decryptedBlob; + }; }; const getOrgKmsKeyId = async (orgId: string) => { @@ -246,8 +243,10 @@ export const kmsServiceFactory = ({ return { startService, generateKmsKey, - encrypt, - decrypt, + encryptWithKmsKey, + encryptWithInputKey, + decryptWithKmsKey, + decryptWithInputKey, getOrgKmsKeyId, getProjectSecretManagerKmsKeyId }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index e1a152f06..5ba6c1343 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -7,35 +7,22 @@ export type TGenerateKMSDTO = { 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; + key: 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; +export type TDecryptWithKeyDTO = { + key: Buffer; cipherTextBlob: Buffer; }; - -export type TKmsServiceDecryptionDTO = TDecryptWithKmsDTO | TDecryptWithEncryptionKeyDTO;