diff --git a/README.md b/README.md index 74c9a40369..7e3f40d528 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,11 @@ We're on a mission to make security tooling more accessible to everyone, not jus - **[Infisical PKI Issuer for Kubernetes](https://infisical.com/docs/documentation/platform/pki/pki-issuer)**: Deliver TLS certificates to your Kubernetes workloads with automatic renewal. - **[Enrollment over Secure Transport](https://infisical.com/docs/documentation/platform/pki/est)**: Enroll and manage certificates via EST protocol. +### Key Management (KMS): + +- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API. +- **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data. + ### General Platform: - **Authentication Methods**: Authenticate machine identities with Infisical using a cloud-native or platform agnostic authentication method ([Kubernetes Auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth), [GCP Auth](https://infisical.com/docs/documentation/platform/identities/gcp-auth), [Azure Auth](https://infisical.com/docs/documentation/platform/identities/azure-auth), [AWS Auth](https://infisical.com/docs/documentation/platform/identities/aws-auth), [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general), [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth)). - **[Access Controls](https://infisical.com/docs/documentation/platform/access-controls/overview)**: Define advanced authorization controls for users and machine identities with [RBAC](https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls), [additional privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges), [temporary access](https://infisical.com/docs/documentation/platform/access-controls/temporary-access), [access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests), [approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows), and more. diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 10be1c4c95..65da730724 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -38,6 +38,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; +import { TCmekServiceFactory } from "@app/services/cmek/cmek-service"; import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; @@ -182,6 +183,7 @@ declare module "fastify" { orgAdmin: TOrgAdminServiceFactory; slack: TSlackServiceFactory; workflowIntegration: TWorkflowIntegrationServiceFactory; + cmek: TCmekServiceFactory; migration: TExternalMigrationServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data diff --git a/backend/src/db/migrations/20241003220151_kms-key-cmek-alterations.ts b/backend/src/db/migrations/20241003220151_kms-key-cmek-alterations.ts new file mode 100644 index 0000000000..6b701eea4a --- /dev/null +++ b/backend/src/db/migrations/20241003220151_kms-key-cmek-alterations.ts @@ -0,0 +1,46 @@ +import { Knex } from "knex"; + +import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists"; +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.KmsKey)) { + const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId"); + const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + + // drop constraint if exists (won't exist if rolled back, see below) + await dropConstraintIfExists(TableName.KmsKey, "kms_keys_orgid_slug_unique", knex); + + // projectId for CMEK functionality + await knex.schema.alterTable(TableName.KmsKey, (table) => { + table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE"); + + if (hasOrgId) { + table.unique(["orgId", "projectId", "slug"]); + } + + if (hasSlug) { + table.renameColumn("slug", "name"); + } + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.KmsKey)) { + const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId"); + const hasName = await knex.schema.hasColumn(TableName.KmsKey, "name"); + + // remove projectId for CMEK functionality + await knex.schema.alterTable(TableName.KmsKey, (table) => { + if (hasName) { + table.renameColumn("name", "slug"); + } + + if (hasOrgId) { + table.dropUnique(["orgId", "projectId", "slug"]); + } + table.dropColumn("projectId"); + }); + } +} diff --git a/backend/src/db/migrations/utils/dropConstraintIfExists.ts b/backend/src/db/migrations/utils/dropConstraintIfExists.ts new file mode 100644 index 0000000000..bfe487d496 --- /dev/null +++ b/backend/src/db/migrations/utils/dropConstraintIfExists.ts @@ -0,0 +1,6 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export const dropConstraintIfExists = (tableName: TableName, constraintName: string, knex: Knex) => + knex.raw(`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${constraintName};`); diff --git a/backend/src/db/migrations/utils/kms.ts b/backend/src/db/migrations/utils/kms.ts index b4698a42d3..9ed0909783 100644 --- a/backend/src/db/migrations/utils/kms.ts +++ b/backend/src/db/migrations/utils/kms.ts @@ -54,7 +54,7 @@ export const getSecretManagerDataKey = async (knex: Knex, projectId: string) => } else { const [kmsDoc] = await knex(TableName.KmsKey) .insert({ - slug: slugify(alphaNumericNanoId(8).toLowerCase()), + name: slugify(alphaNumericNanoId(8).toLowerCase()), orgId: project.orgId, isReserved: false }) diff --git a/backend/src/db/schemas/kms-keys.ts b/backend/src/db/schemas/kms-keys.ts index 60cbb2e3b7..b56fab7bf6 100644 --- a/backend/src/db/schemas/kms-keys.ts +++ b/backend/src/db/schemas/kms-keys.ts @@ -13,9 +13,10 @@ export const KmsKeysSchema = z.object({ isDisabled: z.boolean().default(false).nullable().optional(), isReserved: z.boolean().default(true).nullable().optional(), orgId: z.string().uuid(), - slug: z.string(), + name: z.string(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + projectId: z.string().nullable().optional() }); export type TKmsKeys = z.infer; diff --git a/backend/src/ee/routes/v1/external-kms-router.ts b/backend/src/ee/routes/v1/external-kms-router.ts index a029e1cb8c..4e43d6ed91 100644 --- a/backend/src/ee/routes/v1/external-kms-router.ts +++ b/backend/src/ee/routes/v1/external-kms-router.ts @@ -26,7 +26,7 @@ const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({ isDisabled: true, createdAt: true, updatedAt: true, - slug: true + name: true }) .extend({ externalKms: ExternalKmsSchema.pick({ @@ -57,7 +57,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - slug: z.string().min(1).trim().toLowerCase(), + name: z.string().min(1).trim().toLowerCase(), description: z.string().trim().optional(), provider: ExternalKmsInputSchema }), @@ -74,7 +74,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - slug: req.body.slug, + name: req.body.name, provider: req.body.provider, description: req.body.description }); @@ -87,7 +87,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { metadata: { kmsId: externalKms.id, provider: req.body.provider.type, - slug: req.body.slug, + name: req.body.name, description: req.body.description } } @@ -108,7 +108,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { id: z.string().trim().min(1) }), body: z.object({ - slug: z.string().min(1).trim().toLowerCase().optional(), + name: z.string().min(1).trim().toLowerCase().optional(), description: z.string().trim().optional(), provider: ExternalKmsInputUpdateSchema }), @@ -125,7 +125,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - slug: req.body.slug, + name: req.body.name, provider: req.body.provider, description: req.body.description, id: req.params.id @@ -139,7 +139,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { metadata: { kmsId: externalKms.id, provider: req.body.provider.type, - slug: req.body.slug, + name: req.body.name, description: req.body.description } } @@ -182,7 +182,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { type: EventType.DELETE_KMS, metadata: { kmsId: externalKms.id, - slug: externalKms.slug + name: externalKms.name } } }); @@ -224,7 +224,7 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { type: EventType.GET_KMS, metadata: { kmsId: externalKms.id, - slug: externalKms.slug + name: externalKms.name } } }); @@ -260,13 +260,13 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/slug/:slug", + url: "/name/:name", config: { rateLimit: readLimit }, schema: { params: z.object({ - slug: z.string().trim().min(1) + name: z.string().trim().min(1) }), response: { 200: z.object({ @@ -276,12 +276,12 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const externalKms = await server.services.externalKms.findBySlug({ + const externalKms = await server.services.externalKms.findByName({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - slug: req.params.slug + name: req.params.name }); return { externalKms }; } diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts index fccfbd158d..e3956731eb 100644 --- a/backend/src/ee/routes/v1/project-router.ts +++ b/backend/src/ee/routes/v1/project-router.ts @@ -203,7 +203,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 200: z.object({ secretManagerKmsKey: z.object({ id: z.string(), - slug: z.string(), + name: z.string(), isExternal: z.boolean() }) }) @@ -243,7 +243,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 200: z.object({ secretManagerKmsKey: z.object({ id: z.string(), - slug: z.string(), + name: z.string(), isExternal: z.boolean() }) }) @@ -268,7 +268,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { metadata: { secretManagerKmsKey: { id: secretManagerKmsKey.id, - slug: secretManagerKmsKey.slug + name: secretManagerKmsKey.name } } } @@ -336,7 +336,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 200: z.object({ secretManagerKmsKey: z.object({ id: z.string(), - slug: z.string(), + name: z.string(), isExternal: z.boolean() }) }) diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 542471facd..9a2875f69f 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -1,3 +1,4 @@ +import { SymmetricEncryption } from "@app/lib/crypto/cipher"; import { TProjectPermission } from "@app/lib/types"; import { ActorType } from "@app/services/auth/auth-type"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; @@ -182,7 +183,13 @@ export enum EventType { DELETE_SLACK_INTEGRATION = "delete-slack-integration", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", - INTEGRATION_SYNCED = "integration-synced" + INTEGRATION_SYNCED = "integration-synced", + CREATE_CMEK = "create-cmek", + UPDATE_CMEK = "update-cmek", + DELETE_CMEK = "delete-cmek", + GET_CMEKS = "get-cmeks", + CMEK_ENCRYPT = "cmek-encrypt", + CMEK_DECRYPT = "cmek-decrypt" } interface UserActorMetadata { @@ -1350,7 +1357,7 @@ interface CreateKmsEvent { metadata: { kmsId: string; provider: string; - slug: string; + name: string; description?: string; }; } @@ -1359,7 +1366,7 @@ interface DeleteKmsEvent { type: EventType.DELETE_KMS; metadata: { kmsId: string; - slug: string; + name: string; }; } @@ -1368,7 +1375,7 @@ interface UpdateKmsEvent { metadata: { kmsId: string; provider: string; - slug?: string; + name?: string; description?: string; }; } @@ -1377,7 +1384,7 @@ interface GetKmsEvent { type: EventType.GET_KMS; metadata: { kmsId: string; - slug: string; + name: string; }; } @@ -1386,7 +1393,7 @@ interface UpdateProjectKmsEvent { metadata: { secretManagerKmsKey: { id: string; - slug: string; + name: string; }; }; } @@ -1541,6 +1548,53 @@ interface IntegrationSyncedEvent { }; } +interface CreateCmekEvent { + type: EventType.CREATE_CMEK; + metadata: { + keyId: string; + name: string; + description?: string; + encryptionAlgorithm: SymmetricEncryption; + }; +} + +interface DeleteCmekEvent { + type: EventType.DELETE_CMEK; + metadata: { + keyId: string; + }; +} + +interface UpdateCmekEvent { + type: EventType.UPDATE_CMEK; + metadata: { + keyId: string; + name?: string; + description?: string; + }; +} + +interface GetCmeksEvent { + type: EventType.GET_CMEKS; + metadata: { + keyIds: string[]; + }; +} + +interface CmekEncryptEvent { + type: EventType.CMEK_ENCRYPT; + metadata: { + keyId: string; + }; +} + +interface CmekDecryptEvent { + type: EventType.CMEK_DECRYPT; + metadata: { + keyId: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -1680,4 +1734,10 @@ export type Event = | GetSlackIntegration | UpdateProjectSlackConfig | GetProjectSlackConfig - | IntegrationSyncedEvent; + | IntegrationSyncedEvent + | CreateCmekEvent + | UpdateCmekEvent + | DeleteCmekEvent + | GetCmeksEvent + | CmekEncryptEvent + | CmekDecryptEvent; diff --git a/backend/src/ee/services/external-kms/external-kms-dal.ts b/backend/src/ee/services/external-kms/external-kms-dal.ts index 7077b4aa9d..595ccb8109 100644 --- a/backend/src/ee/services/external-kms/external-kms-dal.ts +++ b/backend/src/ee/services/external-kms/external-kms-dal.ts @@ -30,7 +30,7 @@ export const externalKmsDALFactory = (db: TDbClient) => { isDisabled: el.isDisabled, isReserved: el.isReserved, orgId: el.orgId, - slug: el.slug, + name: el.name, createdAt: el.createdAt, updatedAt: el.updatedAt, externalKms: { 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 49191b13a6..9300b42d8c 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -43,7 +43,7 @@ export const externalKmsServiceFactory = ({ provider, description, actor, - slug, + name, actorId, actorOrgId, actorAuthMethod @@ -64,7 +64,7 @@ export const externalKmsServiceFactory = ({ }); } - const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase()); + const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); let sanitizedProviderInput = ""; switch (provider.type) { @@ -96,7 +96,7 @@ export const externalKmsServiceFactory = ({ { isReserved: false, description, - slug: kmsSlug, + name: kmsName, orgId: actorOrgId }, tx @@ -120,7 +120,7 @@ export const externalKmsServiceFactory = ({ description, actor, id: kmsId, - slug, + name, actorId, actorOrgId, actorAuthMethod @@ -142,7 +142,7 @@ export const externalKmsServiceFactory = ({ }); } - const kmsSlug = slug ? slugify(slug) : undefined; + const kmsName = name ? slugify(name) : undefined; const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); if (!externalKmsDoc) throw new NotFoundError({ message: "External kms not found" }); @@ -188,7 +188,7 @@ export const externalKmsServiceFactory = ({ kmsDoc.id, { description, - slug: kmsSlug + name: kmsName }, tx ); @@ -280,14 +280,14 @@ export const externalKmsServiceFactory = ({ } }; - const findBySlug = async ({ + const findByName = async ({ actor, actorId, actorOrgId, actorAuthMethod, - slug: kmsSlug + name: kmsName }: TGetExternalKmsBySlugDTO) => { - const kmsDoc = await kmsDAL.findOne({ slug: kmsSlug, orgId: actorOrgId }); + const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId }); const { permission } = await permissionService.getOrgPermission( actor, actorId, @@ -327,6 +327,6 @@ export const externalKmsServiceFactory = ({ deleteById, list, findById, - findBySlug + findByName }; }; diff --git a/backend/src/ee/services/external-kms/external-kms-types.ts b/backend/src/ee/services/external-kms/external-kms-types.ts index 6254a80ef5..850108ac47 100644 --- a/backend/src/ee/services/external-kms/external-kms-types.ts +++ b/backend/src/ee/services/external-kms/external-kms-types.ts @@ -3,14 +3,14 @@ import { TOrgPermission } from "@app/lib/types"; import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model"; export type TCreateExternalKmsDTO = { - slug?: string; + name?: string; description?: string; provider: TExternalKmsInputSchema; } & Omit; export type TUpdateExternalKmsDTO = { id: string; - slug?: string; + name?: string; description?: string; provider?: TExternalKmsInputUpdateSchema; } & Omit; @@ -26,5 +26,5 @@ export type TGetExternalKmsByIdDTO = { } & Omit; export type TGetExternalKmsBySlugDTO = { - slug: string; + name: string; } & Omit; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 7c9e75dedf..b2b34e488d 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -14,6 +14,15 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionCmekActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + Encrypt = "encrypt", + Decrypt = "decrypt" +} + export enum ProjectPermissionSub { Role = "role", Member = "member", @@ -38,7 +47,8 @@ export enum ProjectPermissionSub { CertificateTemplates = "certificate-templates", PkiAlerts = "pki-alerts", PkiCollections = "pki-collections", - Kms = "kms" + Kms = "kms", + Cmek = "cmek" } export type SecretSubjectFields = { @@ -95,6 +105,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] + | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] @@ -282,6 +293,12 @@ export const ProjectPermissionSchema = z.discriminatedUnion("subject", [ action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe( + "Describe what action an entity can take." + ) }) ]); @@ -325,6 +342,17 @@ const buildAdminPermissionRules = () => { can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project); can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms); + can( + [ + ProjectPermissionCmekActions.Create, + ProjectPermissionCmekActions.Edit, + ProjectPermissionCmekActions.Delete, + ProjectPermissionCmekActions.Read, + ProjectPermissionCmekActions.Encrypt, + ProjectPermissionCmekActions.Decrypt + ], + ProjectPermissionSub.Cmek + ); return rules; }; @@ -444,6 +472,18 @@ const buildMemberPermissionRules = () => { can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); + can( + [ + ProjectPermissionCmekActions.Create, + ProjectPermissionCmekActions.Edit, + ProjectPermissionCmekActions.Delete, + ProjectPermissionCmekActions.Read, + ProjectPermissionCmekActions.Encrypt, + ProjectPermissionCmekActions.Decrypt + ], + ProjectPermissionSub.Cmek + ); + return rules; }; @@ -470,6 +510,7 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); return rules; }; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 75c3dc32ae..d15b4fd865 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1347,3 +1347,37 @@ export const PROJECT_ROLE = { projectSlug: "The slug of the project to list the roles of." } }; + +export const KMS = { + CREATE_KEY: { + projectId: "The ID of the project to create the key in.", + name: "The name of the key to be created. Must be slug-friendly.", + description: "An optional description of the key.", + encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key." + }, + UPDATE_KEY: { + keyId: "The ID of the key to be updated.", + name: "The updated name of this key. Must be slug-friendly.", + description: "The updated description of this key.", + isDisabled: "The flag to enable or disable this key." + }, + DELETE_KEY: { + keyId: "The ID of the key to be deleted." + }, + LIST_KEYS: { + projectId: "The ID of the project to list keys from.", + offset: "The offset to start from. If you enter 10, it will start from the 10th key.", + limit: "The number of keys to return.", + orderBy: "The column to order keys by.", + orderDirection: "The direction to order keys in.", + search: "The text string to filter key names by." + }, + ENCRYPT: { + keyId: "The ID of the key to encrypt the data with.", + plaintext: "The plaintext to be encrypted (base64 encoded)." + }, + DECRYPT: { + keyId: "The ID of the key to decrypt the data with.", + ciphertext: "The ciphertext to be decrypted (base64 encoded)." + } +}; diff --git a/backend/src/lib/base64/index.ts b/backend/src/lib/base64/index.ts new file mode 100644 index 0000000000..cfc0fde3f1 --- /dev/null +++ b/backend/src/lib/base64/index.ts @@ -0,0 +1,28 @@ +// Credit: https://github.com/miguelmota/is-base64 +export const isBase64 = ( + v: string, + opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false } +) => { + if (opts.allowEmpty === false && v === "") { + return false; + } + + let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?"; + const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)"; + + if (opts.mimeRequired === true) { + regex = mimeRegex + regex; + } else if (opts.allowMime === true) { + regex = `${mimeRegex}?${regex}`; + } + + if (opts.paddingRequired === false) { + regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?"; + } + + return new RegExp(`^${regex}$`, "gi").test(v); +}; + +export const getBase64SizeInBytes = (base64String: string) => { + return Buffer.from(base64String, "base64").length; +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index addb46b937..96823cda01 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -96,6 +96,7 @@ import { certificateAuthorityServiceFactory } from "@app/services/certificate-au import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; +import { cmekServiceFactory } from "@app/services/cmek/cmek-service"; import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; @@ -1192,6 +1193,12 @@ export const registerRoutes = async ( workflowIntegrationDAL }); + const cmekService = cmekServiceFactory({ + kmsDAL, + kmsService, + permissionService + }); + const migrationService = externalMigrationServiceFactory({ projectService, orgService, @@ -1281,6 +1288,7 @@ export const registerRoutes = async ( secretSharing: secretSharingService, userEngagement: userEngagementService, externalKms: externalKmsService, + cmek: cmekService, orgAdmin: orgAdminService, slack: slackService, workflowIntegration: workflowIntegrationService, diff --git a/backend/src/server/routes/v1/cmek-router.ts b/backend/src/server/routes/v1/cmek-router.ts new file mode 100644 index 0000000000..b07d7b4902 --- /dev/null +++ b/backend/src/server/routes/v1/cmek-router.ts @@ -0,0 +1,331 @@ +import slugify from "@sindresorhus/slugify"; +import { z } from "zod"; + +import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { KMS } from "@app/lib/api-docs"; +import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64"; +import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { OrderByDirection } from "@app/lib/types"; +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"; +import { CmekOrderBy } from "@app/services/cmek/cmek-types"; + +const keyNameSchema = z + .string() + .trim() + .min(1) + .max(32) + .toLowerCase() + .refine((v) => slugify(v) === v, { + message: "Name must be slug friendly" + }); +const keyDescriptionSchema = z.string().trim().max(500).optional(); + +const base64Schema = z.string().superRefine((val, ctx) => { + if (!isBase64(val)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "plaintext must be base64 encoded" + }); + } + + if (getBase64SizeInBytes(val) > 4096) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "data cannot exceed 4096 bytes" + }); + } +}); + +export const registerCmekRouter = async (server: FastifyZodProvider) => { + // create encryption key + server.route({ + method: "POST", + url: "/keys", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create KMS key", + body: z.object({ + projectId: z.string().describe(KMS.CREATE_KEY.projectId), + name: keyNameSchema.describe(KMS.CREATE_KEY.name), + description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description), + encryptionAlgorithm: z + .nativeEnum(SymmetricEncryption) + .optional() + .default(SymmetricEncryption.AES_GCM_256) + .describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others + }), + response: { + 200: z.object({ + key: KmsKeysSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + body: { projectId, name, description, encryptionAlgorithm }, + permission + } = req; + + const cmek = await server.services.cmek.createCmek( + { orgId: permission.orgId, projectId, name, description, encryptionAlgorithm }, + permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.CREATE_CMEK, + metadata: { + keyId: cmek.id, + name, + description, + encryptionAlgorithm + } + } + }); + + return { key: cmek }; + } + }); + + // update KMS key + server.route({ + method: "PATCH", + url: "/keys/:keyId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Update KMS key", + params: z.object({ + keyId: z.string().uuid().describe(KMS.UPDATE_KEY.keyId) + }), + body: z.object({ + name: keyNameSchema.optional().describe(KMS.UPDATE_KEY.name), + isDisabled: z.boolean().optional().describe(KMS.UPDATE_KEY.isDisabled), + description: keyDescriptionSchema.describe(KMS.UPDATE_KEY.description) + }), + response: { + 200: z.object({ + key: KmsKeysSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId }, + body, + permission + } = req; + + const cmek = await server.services.cmek.updateCmekById({ keyId, ...body }, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: permission.orgId, + event: { + type: EventType.UPDATE_CMEK, + metadata: { + keyId, + ...body + } + } + }); + + return { key: cmek }; + } + }); + + // delete KMS key + server.route({ + method: "DELETE", + url: "/keys/:keyId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Delete KMS key", + params: z.object({ + keyId: z.string().uuid().describe(KMS.DELETE_KEY.keyId) + }), + response: { + 200: z.object({ + key: KmsKeysSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId }, + permission + } = req; + + const cmek = await server.services.cmek.deleteCmekById(keyId, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: permission.orgId, + event: { + type: EventType.DELETE_CMEK, + metadata: { + keyId + } + } + }); + + return { key: cmek }; + } + }); + + // list KMS keys + server.route({ + method: "GET", + url: "/keys", + config: { + rateLimit: readLimit + }, + schema: { + description: "List KMS keys", + querystring: z.object({ + projectId: z.string().describe(KMS.LIST_KEYS.projectId), + offset: z.coerce.number().min(0).optional().default(0).describe(KMS.LIST_KEYS.offset), + limit: z.coerce.number().min(1).max(100).optional().default(100).describe(KMS.LIST_KEYS.limit), + orderBy: z.nativeEnum(CmekOrderBy).optional().default(CmekOrderBy.Name).describe(KMS.LIST_KEYS.orderBy), + orderDirection: z + .nativeEnum(OrderByDirection) + .optional() + .default(OrderByDirection.ASC) + .describe(KMS.LIST_KEYS.orderDirection), + search: z.string().trim().optional().describe(KMS.LIST_KEYS.search) + }), + response: { + 200: z.object({ + keys: KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).array(), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + query: { projectId, ...dto }, + permission + } = req; + + const { cmeks, totalCount } = await server.services.cmek.listCmeksByProjectId({ projectId, ...dto }, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_CMEKS, + metadata: { + keyIds: cmeks.map((key) => key.id) + } + } + }); + + return { keys: cmeks, totalCount }; + } + }); + + // encrypt data + server.route({ + method: "POST", + url: "/keys/:keyId/encrypt", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Encrypt data with KMS key", + params: z.object({ + keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId) + }), + body: z.object({ + plaintext: base64Schema.describe(KMS.ENCRYPT.plaintext) + }), + response: { + 200: z.object({ + ciphertext: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId }, + body: { plaintext }, + permission + } = req; + + const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: permission.orgId, + event: { + type: EventType.CMEK_ENCRYPT, + metadata: { + keyId + } + } + }); + + return { ciphertext }; + } + }); + + server.route({ + method: "POST", + url: "/keys/:keyId/decrypt", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Decrypt data with KMS key", + params: z.object({ + keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId) + }), + body: z.object({ + ciphertext: base64Schema.describe(KMS.ENCRYPT.plaintext) + }), + response: { + 200: z.object({ + plaintext: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + params: { keyId }, + body: { ciphertext }, + permission + } = req; + + const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: permission.orgId, + event: { + type: EventType.CMEK_DECRYPT, + metadata: { + keyId + } + } + }); + + return { plaintext }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index d46b361b3c..55b3236566 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -1,3 +1,4 @@ +import { registerCmekRouter } from "@app/server/routes/v1/cmek-router"; import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router"; import { registerAdminRouter } from "./admin-router"; @@ -103,6 +104,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerIdentityRouter, { prefix: "/identities" }); await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" }); await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" }); - await server.register(registerDashboardRouter, { prefix: "/dashboard" }); + await server.register(registerCmekRouter, { prefix: "/kms" }); }; diff --git a/backend/src/services/cmek/cmek-service.ts b/backend/src/services/cmek/cmek-service.ts new file mode 100644 index 0000000000..1f9c19dc93 --- /dev/null +++ b/backend/src/services/cmek/cmek-service.ts @@ -0,0 +1,169 @@ +import { ForbiddenError } from "@casl/ability"; +import { FastifyRequest } from "fastify"; + +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { + TCmekDecryptDTO, + TCmekEncryptDTO, + TCreateCmekDTO, + TListCmeksByProjectIdDTO, + TUpdabteCmekByIdDTO +} from "@app/services/cmek/cmek-types"; +import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +type TCmekServiceFactoryDep = { + kmsService: TKmsServiceFactory; + kmsDAL: TKmsKeyDALFactory; + permissionService: TPermissionServiceFactory; +}; + +export type TCmekServiceFactory = ReturnType; + +export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => { + const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: FastifyRequest["permission"]) => { + const { permission } = await permissionService.getProjectPermission( + actor.type, + actor.id, + projectId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek); + + const cmek = await kmsService.generateKmsKey({ + ...dto, + projectId, + isReserved: false + }); + + return cmek; + }; + + const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: FastifyRequest["permission"]) => { + const key = await kmsDAL.findById(keyId); + + if (!key) throw new NotFoundError({ message: "Key not found" }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + const { permission } = await permissionService.getProjectPermission( + actor.type, + actor.id, + key.projectId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek); + + const cmek = await kmsDAL.updateById(keyId, data); + + return cmek; + }; + + const deleteCmekById = async (keyId: string, actor: FastifyRequest["permission"]) => { + const key = await kmsDAL.findById(keyId); + + if (!key) throw new NotFoundError({ message: "Key not found" }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + const { permission } = await permissionService.getProjectPermission( + actor.type, + actor.id, + key.projectId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek); + + const cmek = kmsDAL.deleteById(keyId); + + return cmek; + }; + + const listCmeksByProjectId = async ( + { projectId, ...filters }: TListCmeksByProjectIdDTO, + actor: FastifyRequest["permission"] + ) => { + const { permission } = await permissionService.getProjectPermission( + actor.type, + actor.id, + projectId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + + const { keys: cmeks, totalCount } = await kmsDAL.findKmsKeysByProjectId({ projectId, ...filters }); + + return { cmeks, totalCount }; + }; + + const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: FastifyRequest["permission"]) => { + const key = await kmsDAL.findById(keyId); + + if (!key) throw new NotFoundError({ message: "Key not found" }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); + + const { permission } = await permissionService.getProjectPermission( + actor.type, + actor.id, + key.projectId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek); + + const encrypt = await kmsService.encryptWithKmsKey({ kmsId: keyId }); + + const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") }); + + return cipherTextBlob.toString("base64"); + }; + + const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: FastifyRequest["permission"]) => { + const key = await kmsDAL.findById(keyId); + + if (!key) throw new NotFoundError({ message: "Key not found" }); + + if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); + + if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); + + const { permission } = await permissionService.getProjectPermission( + actor.type, + actor.id, + key.projectId, + actor.authMethod, + actor.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek); + + const decrypt = await kmsService.decryptWithKmsKey({ kmsId: keyId }); + + const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") }); + + return plaintextBlob.toString("base64"); + }; + + return { + createCmek, + updateCmekById, + deleteCmekById, + listCmeksByProjectId, + cmekEncrypt, + cmekDecrypt + }; +}; diff --git a/backend/src/services/cmek/cmek-types.ts b/backend/src/services/cmek/cmek-types.ts new file mode 100644 index 0000000000..b99ff1d6e3 --- /dev/null +++ b/backend/src/services/cmek/cmek-types.ts @@ -0,0 +1,40 @@ +import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +import { OrderByDirection } from "@app/lib/types"; + +export type TCreateCmekDTO = { + orgId: string; + projectId: string; + name: string; + description?: string; + encryptionAlgorithm: SymmetricEncryption; +}; + +export type TUpdabteCmekByIdDTO = { + keyId: string; + name?: string; + isDisabled?: boolean; + description?: string; +}; + +export type TListCmeksByProjectIdDTO = { + projectId: string; + offset?: number; + limit?: number; + orderBy?: CmekOrderBy; + orderDirection?: OrderByDirection; + search?: string; +}; + +export type TCmekEncryptDTO = { + keyId: string; + plaintext: string; +}; + +export type TCmekDecryptDTO = { + keyId: string; + ciphertext: string; +}; + +export enum CmekOrderBy { + Name = "name" +} diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 0422a4b5d4..1bea4c9f47 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -5,7 +5,7 @@ import { TableName, TIdentityOrgMemberships } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; -import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types"; +import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types"; export type TIdentityOrgDALFactory = ReturnType; @@ -33,7 +33,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { { limit, offset = 0, - orderBy, + orderBy = OrgIdentityOrderBy.Name, orderDirection = OrderByDirection.ASC, search, ...filter @@ -43,12 +43,16 @@ export const identityOrgDALFactory = (db: TDbClient) => { ) => { try { const paginatedFetchIdentity = (tx || db.replicaNode())(TableName.Identity) - .where((queryBuilder) => { - if (limit) { - void queryBuilder.offset(offset).limit(limit); - } - }) - .as(TableName.Identity); + .as(TableName.Identity) + .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection); + + if (search?.length) { + void paginatedFetchIdentity.whereILike(`${TableName.Identity}.name`, `%${search}%`); + } + + if (limit) { + void paginatedFetchIdentity.offset(offset).limit(limit); + } const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) .where(filter) @@ -78,24 +82,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") - ); - - if (orderBy) { - switch (orderBy) { - case "name": - void query.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection); - break; - case "role": - void query.orderBy(`${TableName.IdentityOrgMembership}.${orderBy}`, orderDirection); - break; - default: - // do nothing - } - } - - if (search?.length) { - void query.whereILike(`${TableName.Identity}.name`, `%${search}%`); - } + ) + .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection); const docs = await query; const formattedDocs = sqlNestRelationships({ diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts index 23110c3bdb..ceaf3ecfc3 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -41,6 +41,6 @@ export type TListOrgIdentitiesByOrgIdDTO = { } & TOrgPermission; export enum OrgIdentityOrderBy { - Name = "name", - Role = "role" + Name = "name" + // Role = "role" } diff --git a/backend/src/services/kms/kms-fns.ts b/backend/src/services/kms/kms-fns.ts new file mode 100644 index 0000000000..96196e1afc --- /dev/null +++ b/backend/src/services/kms/kms-fns.ts @@ -0,0 +1,11 @@ +import { SymmetricEncryption } from "@app/lib/crypto/cipher"; + +export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => { + switch (encryptionAlgorithm) { + case SymmetricEncryption.AES_GCM_128: + return 16; + case SymmetricEncryption.AES_GCM_256: + default: + return 32; + } +}; diff --git a/backend/src/services/kms/kms-key-dal.ts b/backend/src/services/kms/kms-key-dal.ts index 0043487228..e0246c0964 100644 --- a/backend/src/services/kms/kms-key-dal.ts +++ b/backend/src/services/kms/kms-key-dal.ts @@ -1,9 +1,11 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { KmsKeysSchema, TableName } from "@app/db/schemas"; +import { KmsKeysSchema, TableName, TInternalKms, TKmsKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { OrderByDirection } from "@app/lib/types"; +import { CmekOrderBy, TListCmeksByProjectIdDTO } from "@app/services/cmek/cmek-types"; export type TKmsKeyDALFactory = ReturnType; @@ -71,5 +73,50 @@ export const kmskeyDALFactory = (db: TDbClient) => { } }; - return { ...kmsOrm, findByIdWithAssociatedKms }; + const findKmsKeysByProjectId = async ( + { + projectId, + offset = 0, + limit, + orderBy = CmekOrderBy.Name, + orderDirection = OrderByDirection.ASC, + search + }: TListCmeksByProjectIdDTO, + tx?: Knex + ) => { + try { + const query = (tx || db.replicaNode())(TableName.KmsKey) + .where("projectId", projectId) + .where((qb) => { + if (search) { + void qb.whereILike("name", `%${search}%`); + } + }) + .join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`) + .select< + (TKmsKeys & + Pick & { + total_count: number; + })[] + >( + selectAllTableCols(TableName.KmsKey), + db.raw(`count(*) OVER() as total_count`), + db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms), + db.ref("version").withSchema(TableName.InternalKms) + ) + .orderBy(orderBy, orderDirection); + + if (limit) { + void query.limit(limit).offset(offset); + } + + const data = await query; + + return { keys: data, totalCount: Number(data?.[0]?.total_count ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "Find kms keys by project id" }); + } + }; + + return { ...kmsOrm, findByIdWithAssociatedKms, findKmsKeysByProjectId }; }; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index ad06906dfc..40ec3699a2 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -17,6 +17,7 @@ import { generateHash } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { getByteLengthForAlgorithm } from "@app/services/kms/kms-fns"; import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; @@ -71,17 +72,29 @@ export const kmsServiceFactory = ({ * This function is responsibile for generating the infisical internal KMS for various entities * Like for secret manager, cert manager or for organization */ - const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => { + const generateKmsKey = async ({ + orgId, + isReserved = true, + tx, + name, + projectId, + encryptionAlgorithm = SymmetricEncryption.AES_GCM_256, + description + }: TGenerateKMSDTO) => { const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const kmsKeyMaterial = randomSecureBytes(32); + + const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm)); + const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY); - const sanitizedSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase()); + const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); const dbQuery = async (db: Knex) => { const kmsDoc = await kmsDAL.create( { - slug: sanitizedSlug, + name: sanitizedName, orgId, - isReserved + isReserved, + projectId, + description }, db ); @@ -90,7 +103,7 @@ export const kmsServiceFactory = ({ { version: 1, encryptedKey: encryptedKeyMaterial, - encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, + encryptionAlgorithm, kmsKeyId: kmsDoc.id }, db @@ -286,12 +299,13 @@ export const kmsServiceFactory = ({ } // internal KMS - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption); + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); return ({ cipherTextBlob: versionedCipherTextBlob }: Pick) => { const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); - const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey); + const decryptedBlob = dataCipher.decrypt(cipherTextBlob, kmsKey); return Promise.resolve(decryptedBlob); }; }; @@ -347,11 +361,11 @@ export const kmsServiceFactory = ({ } // internal KMS - // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption); return ({ plainText }: Pick) => { - const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); - const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey); + const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey); // Buffer#1 encrypted text + Buffer#2 version number const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 @@ -767,8 +781,8 @@ export const kmsServiceFactory = ({ message: "KMS not found" }); } - const { id, slug, orgId, isExternal } = kms; - return { id, slug, orgId, isExternal }; + const { id, name, orgId, isExternal } = kms; + return { id, name, orgId, isExternal }; }; // akhilmhdh: a copy of this is made in migrations/utils/kms diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index dc7de10a42..5d5b77a093 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -1,5 +1,7 @@ import { Knex } from "knex"; +import { SymmetricEncryption } from "@app/lib/crypto/cipher"; + export enum KmsDataKey { Organization, SecretManager @@ -22,8 +24,11 @@ export type TEncryptWithKmsDataKeyDTO = export type TGenerateKMSDTO = { orgId: string; + projectId?: string; + encryptionAlgorithm?: SymmetricEncryption; isReserved?: boolean; - slug?: string; + name?: string; + description?: string; tx?: Knex; }; diff --git a/docs/api-reference/endpoints/kms/keys/create.mdx b/docs/api-reference/endpoints/kms/keys/create.mdx new file mode 100644 index 0000000000..194d466bf3 --- /dev/null +++ b/docs/api-reference/endpoints/kms/keys/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Key" +openapi: "POST /api/v1/kms/keys" +--- diff --git a/docs/api-reference/endpoints/kms/keys/decrypt.mdx b/docs/api-reference/endpoints/kms/keys/decrypt.mdx new file mode 100644 index 0000000000..2ab8ce4ab6 --- /dev/null +++ b/docs/api-reference/endpoints/kms/keys/decrypt.mdx @@ -0,0 +1,4 @@ +--- +title: "Decrypt Data" +openapi: "POST /api/v1/kms/keys/{keyId}/decrypt" +--- diff --git a/docs/api-reference/endpoints/kms/keys/delete.mdx b/docs/api-reference/endpoints/kms/keys/delete.mdx new file mode 100644 index 0000000000..91739d362b --- /dev/null +++ b/docs/api-reference/endpoints/kms/keys/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete Key" +openapi: "DELETE /api/v1/kms/keys/{keyId}" +--- diff --git a/docs/api-reference/endpoints/kms/keys/encrypt.mdx b/docs/api-reference/endpoints/kms/keys/encrypt.mdx new file mode 100644 index 0000000000..6d9db8006f --- /dev/null +++ b/docs/api-reference/endpoints/kms/keys/encrypt.mdx @@ -0,0 +1,4 @@ +--- +title: "Encrypt Data" +openapi: "POST /api/v1/kms/keys/{keyId}/encrypt" +--- diff --git a/docs/api-reference/endpoints/kms/keys/list.mdx b/docs/api-reference/endpoints/kms/keys/list.mdx new file mode 100644 index 0000000000..983c98ae3d --- /dev/null +++ b/docs/api-reference/endpoints/kms/keys/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List Keys" +openapi: "Get /api/v1/kms/keys" +--- diff --git a/docs/api-reference/endpoints/kms/keys/update.mdx b/docs/api-reference/endpoints/kms/keys/update.mdx new file mode 100644 index 0000000000..7e44420b1e --- /dev/null +++ b/docs/api-reference/endpoints/kms/keys/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update Key" +openapi: "PATCH /api/v1/kms/keys/{keyId}" +--- diff --git a/docs/documentation/platform/identities/aws-auth.mdx b/docs/documentation/platform/identities/aws-auth.mdx index 5c82a3807c..494606ccd7 100644 --- a/docs/documentation/platform/identities/aws-auth.mdx +++ b/docs/documentation/platform/identities/aws-auth.mdx @@ -7,7 +7,7 @@ description: "Learn how to authenticate with Infisical for EC2 instances, Lambda ## Diagram -The following sequence digram illustrates the AWS Auth workflow for authenticating AWS IAM principals with Infisical. +The following sequence diagram illustrates the AWS Auth workflow for authenticating AWS IAM principals with Infisical. ```mermaid sequenceDiagram diff --git a/docs/documentation/platform/identities/azure-auth.mdx b/docs/documentation/platform/identities/azure-auth.mdx index 910ca10145..03d997ffb8 100644 --- a/docs/documentation/platform/identities/azure-auth.mdx +++ b/docs/documentation/platform/identities/azure-auth.mdx @@ -7,7 +7,7 @@ description: "Learn how to authenticate with Infisical for services on Azure" ## Diagram -The following sequence digram illustrates the Azure Auth workflow for authenticating Azure [service principals](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) with Infisical. +The following sequence diagram illustrates the Azure Auth workflow for authenticating Azure [service principals](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) with Infisical. ```mermaid sequenceDiagram diff --git a/docs/documentation/platform/identities/gcp-auth.mdx b/docs/documentation/platform/identities/gcp-auth.mdx index 09b41852d0..6573544ded 100644 --- a/docs/documentation/platform/identities/gcp-auth.mdx +++ b/docs/documentation/platform/identities/gcp-auth.mdx @@ -13,7 +13,7 @@ description: "Learn how to authenticate with Infisical for services on Google Cl ## Diagram - The following sequence digram illustrates the GCP ID Token Auth workflow for authenticating GCP resources with Infisical. + The following sequence diagram illustrates the GCP ID Token Auth workflow for authenticating GCP resources with Infisical. ```mermaid sequenceDiagram @@ -182,7 +182,7 @@ access the Infisical API using the GCP ID Token authentication method. ## Diagram - The following sequence digram illustrates the GCP IAM Auth workflow for authenticating GCP IAM service accounts with Infisical. + The following sequence diagram illustrates the GCP IAM Auth workflow for authenticating GCP IAM service accounts with Infisical. ```mermaid sequenceDiagram diff --git a/docs/documentation/platform/identities/kubernetes-auth.mdx b/docs/documentation/platform/identities/kubernetes-auth.mdx index 153b0f4a35..b4d7cc1ac5 100644 --- a/docs/documentation/platform/identities/kubernetes-auth.mdx +++ b/docs/documentation/platform/identities/kubernetes-auth.mdx @@ -7,7 +7,7 @@ description: "Learn how to authenticate with Infisical in Kubernetes" ## Diagram - The following sequence digram illustrates the Kubernetes Auth workflow for authenticating applications running in pods with Infisical. + The following sequence diagram illustrates the Kubernetes Auth workflow for authenticating applications running in pods with Infisical. ```mermaid sequenceDiagram diff --git a/docs/documentation/platform/identities/token-auth.mdx b/docs/documentation/platform/identities/token-auth.mdx index 61906542fd..59c5f9abf6 100644 --- a/docs/documentation/platform/identities/token-auth.mdx +++ b/docs/documentation/platform/identities/token-auth.mdx @@ -7,7 +7,7 @@ description: "Learn how to authenticate to Infisical from any platform or enviro ## Diagram -The following sequence digram illustrates the Token Auth workflow for authenticating clients with Infisical. +The following sequence diagram illustrates the Token Auth workflow for authenticating clients with Infisical. ```mermaid sequenceDiagram diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index 32e916631c..5979780936 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -7,7 +7,7 @@ description: "Learn how to authenticate to Infisical from any platform or enviro ## Diagram -The following sequence digram illustrates the Universal Auth workflow for authenticating clients with Infisical. +The following sequence diagram illustrates the Universal Auth workflow for authenticating clients with Infisical. ```mermaid sequenceDiagram diff --git a/docs/documentation/platform/kms/aws-hsm.mdx b/docs/documentation/platform/kms-configuration/aws-hsm.mdx similarity index 100% rename from docs/documentation/platform/kms/aws-hsm.mdx rename to docs/documentation/platform/kms-configuration/aws-hsm.mdx diff --git a/docs/documentation/platform/kms/aws-kms.mdx b/docs/documentation/platform/kms-configuration/aws-kms.mdx similarity index 100% rename from docs/documentation/platform/kms/aws-kms.mdx rename to docs/documentation/platform/kms-configuration/aws-kms.mdx diff --git a/docs/documentation/platform/kms/overview.mdx b/docs/documentation/platform/kms-configuration/overview.mdx similarity index 95% rename from docs/documentation/platform/kms/overview.mdx rename to docs/documentation/platform/kms-configuration/overview.mdx index 99cfa134fe..327481bc40 100644 --- a/docs/documentation/platform/kms/overview.mdx +++ b/docs/documentation/platform/kms-configuration/overview.mdx @@ -1,5 +1,5 @@ --- -title: "Key Management Service (KMS)" +title: "Key Management Service (KMS) Configuration" sidebarTitle: "Overview" description: "Learn how to configure your project's encryption" --- @@ -25,4 +25,4 @@ For existing projects, you can configure the KMS from the Project Settings page. ## External KMS -Infisical supports the use of external KMS solutions to enhance security and compliance. You can configure your project to use services like [AWS Key Management Service](./aws-kms) for managing encryption. +Infisical supports the use of external KMS solutions to enhance security and compliance. You can configure your project to use services like [AWS Key Management Service](./aws-kms) for managing encryption. \ No newline at end of file diff --git a/docs/documentation/platform/kms.mdx b/docs/documentation/platform/kms.mdx new file mode 100644 index 0000000000..eb4479597d --- /dev/null +++ b/docs/documentation/platform/kms.mdx @@ -0,0 +1,208 @@ +--- +title: "Key Management Service (KMS)" +sidebarTitle: "Key Management (KMS)" +description: "Learn how to manage and use cryptographic keys with Infisical." +--- + +## Concept + +Infisical can be used as a Key Management System (KMS), referred to as Infisical KMS, to centralize management of keys to be used for cryptographic operations like encryption/decryption. + + + Keys managed in KMS are not extractable from the platform. Additionally, data + is never stored when performing cryptographic operations. + + +## Workflow + +The typical workflow for using Infisical KMS consists of the following steps: + +1. Creating a KMS key. As part of this step, you specify a name for the key and the encryption algorithm meant to be used for it (e.g. `AES-GCM-128`, `AES-GCM-256`). +2. Encryption: To encrypt data, you would make a request to the Infisical KMS API endpoint, specifying the base64-encoded plaintext and the intended key to use for encryption; the API would return the base64-encoded ciphertext. +3. Decryption: To decrypt data, you would make a request to the Infisical KMS API endpoint, specifying the base64-encoded ciphertext and the intended key to use for decryption; the API would return the base64-encoded plaintext. + + + Note that this workflow can be executed via the Infisical UI or manually such + as via API. + + +## Guide to Encrypting Data + +In the following steps, we explore how to generate a key and use it to encrypt data. + + + + + + Navigate to Project > Key Management and tap on the **Add Key** button. + ![kms add key button](/images/platform/kms/infisical-kms/kms-add-key.png) + + Specify your key details. Here's some guidance on each field: + + - Name: A slug-friendly name for the key. + - Type: The encryption algorithm associated with the key (e.g. `AES-GCM-256`). + - Description: An optional description of what the intended usage is for the key. + + ![kms add key modal](/images/platform/kms/infisical-kms/kms-add-key-modal.png) + + + Once your key is generated, open the options menu for the newly created key and select encrypt data. + ![kms key options](/images/platform/kms/infisical-kms/kms-key-options.png) + + Populate the text area with your data and tap on the Encrypt button. + ![kms encrypt data](/images/platform/kms/infisical-kms/kms-encrypt-data.png) + + + If your data is already Base64 encoded make sure to toggle the respective switch on to avoid + redundant encoding. + + + Copy and store the encrypted data. + ![kms encrypted data](/images/platform/kms/infisical-kms/kms-encrypted-data.png) + + + + + + + To create a cryptographic key, make an API request to the [Create KMS + Key](/api-reference/endpoints/kms/keys/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/kms/keys \ + --header 'Content-Type: application/json' \ + --data '{ + "projectId": "", + "name": "my-secret-key", + "description": "...", + "encryptionAlgorithm": "aes-256-gcm" + }' + ``` + + ### Sample response + + ```bash Response + { + "key": { + "id": "", + "description": "...", + "isDisabled": false, + "isReserved": false, + "orgId": "", + "name": "my-secret-key", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "projectId": "" + } + } + ``` + + + To encrypt data, make an API request to the [Encrypt + Data](/api-reference/endpoints/kms/keys/encrypt) API endpoint, + specifying the key to use. + + + Make sure your data is Base64 encoded + + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/kms/keys//encrypt \ + --header 'Content-Type: application/json' \ + --data '{ + "plaintext": "lUFHM5Ggwo6TOfpuN1S==" // base64 encoded plaintext + }' + ``` + + ### Sample response + + ```bash Response + { + "ciphertext": "HwFHwSFHwlMF6TOfp==" // base64 encoded ciphertext + } + ``` + + + + + + +## Guide to Decrypting Data + +In the following steps, we explore how to use decrypt data using an existing key in Infisical KMS. + + + + + + Navigate to Project > Key Management and open the options menu for the key used to encrypt the data + you want to decrypt. + ![kms key options](/images/platform/kms/infisical-kms/kms-decrypt-options.png) + + + + Paste your encrypted data into the text area and tap on the Decrypt button. Optionally, if your data was + originally plain text, enable the decode Base64 switch. + ![kms decrypt data](/images/platform/kms/infisical-kms/kms-decrypt-data.png) + + Your decrypted data will be displayed and can be copied for use. + ![kms decrypted data](/images/platform/kms/infisical-kms/kms-decrypted-data.png) + + + + + + + + To decrypt data, make an API request to the [Decrypt + Data](/api-reference/endpoints/kms/keys/decrypt) API endpoint, + specifying the key to use. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/kms/keys//decrypt \ + --header 'Content-Type: application/json' \ + --data '{ + "ciphertext": "HwFHwSFHwlMF6TOfp==" // base64 encoded ciphertext + }' + ``` + + ### Sample response + + ```bash Response + { + "plaintext": "lUFHM5Ggwo6TOfpuN1S==" // base64 encoded plaintext + } + ``` + + + + + + + +## FAQ + + + + No. Infisical's KMS only provides cryptographic services and does not store + any encrypted or decrypted data. + + + No. Infisical's KMS will never expose your keys, encrypted or decrypted, to + external sources. + + + Currently, Infisical only supports `AES-128-GCM` and `AES-256-GCM` for + encryption operations. We anticipate supporting more algorithms and + cryptographic operations in the coming months. + + diff --git a/docs/images/platform/kms/infisical-kms/kms-add-key-modal.png b/docs/images/platform/kms/infisical-kms/kms-add-key-modal.png new file mode 100644 index 0000000000..c1738eb437 Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/kms-add-key-modal.png differ diff --git a/docs/images/platform/kms/infisical-kms/kms-add-key.png b/docs/images/platform/kms/infisical-kms/kms-add-key.png new file mode 100644 index 0000000000..4fdc448986 Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/kms-add-key.png differ diff --git a/docs/images/platform/kms/infisical-kms/kms-decrypt-data.png b/docs/images/platform/kms/infisical-kms/kms-decrypt-data.png new file mode 100644 index 0000000000..63d4e2aee7 Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/kms-decrypt-data.png differ diff --git a/docs/images/platform/kms/infisical-kms/kms-decrypt-options.png b/docs/images/platform/kms/infisical-kms/kms-decrypt-options.png new file mode 100644 index 0000000000..e07a701962 Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/kms-decrypt-options.png differ diff --git a/docs/images/platform/kms/infisical-kms/kms-decrypted-data.png b/docs/images/platform/kms/infisical-kms/kms-decrypted-data.png new file mode 100644 index 0000000000..0d9ad82b8f Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/kms-decrypted-data.png differ diff --git a/docs/images/platform/kms/infisical-kms/kms-encrypt-data.png b/docs/images/platform/kms/infisical-kms/kms-encrypt-data.png new file mode 100644 index 0000000000..d73f1cc3b6 Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/kms-encrypt-data.png differ diff --git a/docs/images/platform/kms/infisical-kms/kms-encrypted-data.png b/docs/images/platform/kms/infisical-kms/kms-encrypted-data.png new file mode 100644 index 0000000000..33f896183f Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/kms-encrypted-data.png differ diff --git a/docs/images/platform/kms/infisical-kms/kms-key-options.png b/docs/images/platform/kms/infisical-kms/kms-key-options.png new file mode 100644 index 0000000000..b1e45e3569 Binary files /dev/null and b/docs/images/platform/kms/infisical-kms/kms-key-options.png differ diff --git a/docs/mint.json b/docs/mint.json index d2ca126056..6a1edb0bf8 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -113,6 +113,15 @@ "documentation/platform/pki/alerting" ] }, + "documentation/platform/kms", + { + "group": "KMS Configuration", + "pages": [ + "documentation/platform/kms-configuration/overview", + "documentation/platform/kms-configuration/aws-kms", + "documentation/platform/kms-configuration/aws-hsm" + ] + }, { "group": "Identities", "pages": [ @@ -171,14 +180,6 @@ "documentation/platform/dynamic-secrets/azure-entra-id" ] }, - { - "group": "Key Management (KMS)", - "pages": [ - "documentation/platform/kms/overview", - "documentation/platform/kms/aws-kms", - "documentation/platform/kms/aws-hsm" - ] - }, { "group": "Workflow Integrations", "pages": [ @@ -789,6 +790,22 @@ } ] }, + { + "group": "Infisical KMS", + "pages": [ + { + "group": "Keys", + "pages": [ + "api-reference/endpoints/kms/keys/list", + "api-reference/endpoints/kms/keys/create", + "api-reference/endpoints/kms/keys/update", + "api-reference/endpoints/kms/keys/delete", + "api-reference/endpoints/kms/keys/encrypt", + "api-reference/endpoints/kms/keys/decrypt" + ] + } + ] + }, { "group": "Internals", "pages": [ diff --git a/frontend/src/components/v2/Dropdown/Dropdown.tsx b/frontend/src/components/v2/Dropdown/Dropdown.tsx index 3870cc74e0..a2d5f0322f 100644 --- a/frontend/src/components/v2/Dropdown/Dropdown.tsx +++ b/frontend/src/components/v2/Dropdown/Dropdown.tsx @@ -86,13 +86,15 @@ export const DropdownMenuItem = ({ icon, as: Item = "button", iconPos = "left", + isDisabled = false, ...props -}: DropdownMenuItemProps & ComponentPropsWithRef) => ( +}: DropdownMenuItemProps & ComponentPropsWithRef & { isDisabled?: boolean }) => ( diff --git a/frontend/src/components/v2/Pagination/Pagination.tsx b/frontend/src/components/v2/Pagination/Pagination.tsx index cc1f7df953..51eed6396d 100644 --- a/frontend/src/components/v2/Pagination/Pagination.tsx +++ b/frontend/src/components/v2/Pagination/Pagination.tsx @@ -49,7 +49,7 @@ export const Pagination = ({ return (
diff --git a/frontend/src/components/v2/Switch/Switch.tsx b/frontend/src/components/v2/Switch/Switch.tsx index a54657fc61..ce955354ab 100644 --- a/frontend/src/components/v2/Switch/Switch.tsx +++ b/frontend/src/components/v2/Switch/Switch.tsx @@ -8,6 +8,7 @@ export type SwitchProps = Omit ( -
+
- + Role + {/*
Role { />
- + */} {isFetching ? : null} @@ -277,9 +286,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { })} - {!isLoading && data && data.totalCount > 0 && ( + {!isLoading && data && totalCount > 0 && ( setPage(newPage)} diff --git a/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx b/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx index 82fa428717..fd2d482253 100644 --- a/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx +++ b/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx @@ -2,7 +2,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { withProjectPermission } from "@app/hoc"; -import { CaTab, CertificatesTab,PkiAlertsTab } from "./components"; +import { CaTab, CertificatesTab, PkiAlertsTab } from "./components"; enum TabSections { Ca = "certificate-authorities", @@ -36,5 +36,5 @@ export const CertificatesPage = withProjectPermission(
); }, - { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs } + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Certificates } ); diff --git a/frontend/src/views/Project/KmsPage/components/CmekDecryptModal.tsx b/frontend/src/views/Project/KmsPage/components/CmekDecryptModal.tsx new file mode 100644 index 0000000000..aa0943f735 --- /dev/null +++ b/frontend/src/views/Project/KmsPage/components/CmekDecryptModal.tsx @@ -0,0 +1,169 @@ +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { faCheckCircle, faCopy, faInfoCircle, faLockOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { decodeBase64 } from "tweetnacl-util"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Modal, + ModalClose, + ModalContent, + Switch, + TextArea, + Tooltip +} from "@app/components/v2"; +import { useTimedReset } from "@app/hooks"; +import { TCmek, useCmekDecrypt } from "@app/hooks/api/cmeks"; + +const formSchema = z.object({ + ciphertext: z.string() +}); + +export type FormData = z.infer; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + cmek: TCmek; +}; + +type FormProps = Pick; + +const DecryptForm = ({ cmek }: FormProps) => { + const cmekDecrypt = useCmekDecrypt(); + const [shouldDecode, setShouldDecode] = useState(false); + const [plaintext, setPlaintext] = useState(""); + + const { + handleSubmit, + register, + formState: { isSubmitting, errors } + } = useForm({ + resolver: zodResolver(formSchema) + }); + + const [copyCiphertext, isCopyingCiphertext, setCopyCipherText] = useTimedReset({ + initialState: "Copy to Clipboard" + }); + + const handleDecryptData = async (formData: FormData) => { + try { + const data = await cmekDecrypt.mutateAsync({ ...formData, keyId: cmek.id }); + createNotification({ + text: "Successfully decrypted data", + type: "success" + }); + + setPlaintext( + shouldDecode ? Buffer.from(decodeBase64(data.plaintext)).toString("utf8") : data.plaintext + ); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to decrypt data", + type: "error" + }); + } + }; + + useEffect(() => { + const text = cmekDecrypt.data?.plaintext; + if (!text) return; + + setPlaintext(shouldDecode ? Buffer.from(decodeBase64(text)).toString("utf8") : text); + }, [shouldDecode]); + + const handleCopyToClipboard = () => { + navigator.clipboard.writeText(plaintext ?? ""); + + setCopyCipherText("Copied to Clipboard"); + }; + + return ( +
+ {plaintext ? ( + +