diff --git a/backend/src/ee/routes/v1/project-router.ts b/backend/src/ee/routes/v1/project-router.ts index 03db346e4..62ceace59 100644 --- a/backend/src/ee/routes/v1/project-router.ts +++ b/backend/src/ee/routes/v1/project-router.ts @@ -254,4 +254,36 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "GET", + url: "/:workspaceId/kms/backup", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.object({ + secretManager: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const backup = await server.services.project.getProjectKmsBackup({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId + }); + + // TODO: add audit log + + return backup; + } + }); }; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index b0c4a6801..404c9a4bd 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -585,6 +585,29 @@ export const kmsServiceFactory = ({ }); }; + const getProjectKeyBackup = async (projectId: string) => { + const project = await projectDAL.findById(projectId); + if (!project) { + throw new NotFoundError({ + message: "Project not found" + }); + } + + const secretManagerDataKey = await getProjectSecretManagerKmsDataKey(projectId); + const kmsKeyIdForEncrypt = await getOrgKmsKeyId(project.orgId); + const kmsEncryptor = await encryptWithKmsKey({ kmsId: kmsKeyIdForEncrypt }); + const { cipherTextBlob: encryptedSecretManagerDataKey } = await kmsEncryptor({ plainText: secretManagerDataKey }); + + // format: projectId.kmsFunction.kmsId.Base64(encryptedDataKey) + const secretManagerBackup = `${projectId}.secretManager.${kmsKeyIdForEncrypt}.${encryptedSecretManagerDataKey.toString( + "base64" + )}`; + + return { + secretManager: secretManagerBackup + }; + }; + const startService = async () => { const appCfg = getConfig(); // This will switch to a seal process and HMS flow in future @@ -642,6 +665,7 @@ export const kmsServiceFactory = ({ getOrgKmsDataKey, getProjectSecretManagerKmsDataKey, getProjectSecretManagerKmsKey, - updateProjectSecretManagerKmsKey + updateProjectSecretManagerKmsKey, + getProjectKeyBackup }; }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index e8027a7bc..1a935dc06 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -78,7 +78,7 @@ type TProjectServiceFactoryDep = { licenseService: Pick; orgDAL: Pick; keyStore: Pick; - kmsService: Pick; + kmsService: Pick; }; export type TProjectServiceFactory = ReturnType; @@ -693,6 +693,34 @@ export const projectServiceFactory = ({ }; }; + const getProjectKmsBackup = async ({ + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TProjectPermission) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms); + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.externalKms) { + throw new BadRequestError({ + message: "Failed to create KMS backup due to plan restriction. Upgrade to the enterprise plan." + }); + } + + const kmsBackup = await kmsService.getProjectKeyBackup(projectId); + return kmsBackup; + }; + return { createProject, deleteProject, @@ -707,6 +735,7 @@ export const projectServiceFactory = ({ listProjectCertificates, updateVersionLimit, updateAuditLogsRetention, - updateProjectKmsKey + updateProjectKmsKey, + getProjectKmsBackup }; }; diff --git a/frontend/src/hooks/api/kms/queries.tsx b/frontend/src/hooks/api/kms/queries.tsx index 4b84db74e..b4a0dc0eb 100644 --- a/frontend/src/hooks/api/kms/queries.tsx +++ b/frontend/src/hooks/api/kms/queries.tsx @@ -53,3 +53,11 @@ export const useGetActiveProjectKms = (projectId: string) => { } }); }; + +export const fetchProjectKmsBackup = async (projectId: string) => { + const { data } = await apiRequest.get<{ + secretManager: string; + }>(`/api/v1/workspace/${projectId}/kms/backup`); + + return data; +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx index f839a720c..2fa9da877 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/EncryptionTab/EncryptionTab.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import FileSaver from "file-saver"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -13,6 +14,7 @@ import { useWorkspace } from "@app/context"; import { useGetActiveProjectKms, useGetExternalKmsList, useUpdateProjectKms } from "@app/hooks/api"; +import { fetchProjectKmsBackup } from "@app/hooks/api/kms/queries"; const formSchema = z.object({ kmsKeyId: z.string() @@ -55,7 +57,7 @@ export const EncryptionTab = () => { } }, [kmsKeyId]); - const onFormSubmit = async (data: TForm) => { + const onUpdateProjectKms = async (data: TForm) => { try { await updateProjectKms({ secretManagerKmsKeyId: data.kmsKeyId @@ -70,12 +72,38 @@ export const EncryptionTab = () => { } }; + const downloadKmsBackup = async () => { + if (!currentWorkspace || !currentOrg) { + return; + } + + const { secretManager } = await fetchProjectKmsBackup(currentWorkspace.id); + + const [, kmsFunction] = secretManager.split("."); + const file = secretManager; + + const blob = new Blob([file], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs( + blob, + `kms-backup-${currentOrg.slug}-${currentWorkspace.slug}-${kmsFunction}.infisical.txt` + ); + }; + return (
-

Key Management

+
+

Key Management

+
+ + +
+
+

Select which Key Management System to use for encrypting your project data