From 891a1ea2b9041ad19298c82b045b8f19eceefb15 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 31 Oct 2024 17:59:41 +0400 Subject: [PATCH] feat: HSM support --- backend/package-lock.json | 7 + backend/package.json | 1 + .../20241028134337_kms-root-cfg-hsm.ts | 4 + backend/src/db/schemas/kms-root-config.ts | 1 + backend/src/lib/config/env.ts | 7 +- backend/src/lib/crypto/index.ts | 1 + backend/src/lib/crypto/shamirs.ts | 38 ++++ backend/src/server/routes/index.ts | 1 + backend/src/server/routes/v1/admin-router.ts | 102 ++++++++++ backend/src/services/kms/kms-fns.ts | 2 + backend/src/services/kms/kms-service.ts | 111 ++++++----- .../super-admin/super-admin-service.ts | 102 +++++++++- cli/packages/api/api.go | 37 ++++ cli/packages/api/model.go | 12 +- cli/packages/cmd/kms.go | 128 ++++++++++++ frontend/src/hooks/api/admin/index.ts | 12 +- frontend/src/hooks/api/admin/mutation.ts | 34 ++++ frontend/src/hooks/api/admin/queries.ts | 28 ++- frontend/src/hooks/api/admin/types.ts | 14 ++ frontend/src/hooks/useFileDownload.tsx | 13 ++ .../admin/DashboardPage/DashboardPage.tsx | 15 +- .../admin/DashboardPage/EncryptionPanel.tsx | 182 ++++++++++++++++++ .../ExportRootKmsKeyModalContent.tsx | 53 +++++ .../RestoreRootKmsKeyModalContent.tsx | 102 ++++++++++ package-lock.json | 14 +- package.json | 3 +- 26 files changed, 967 insertions(+), 57 deletions(-) create mode 100644 backend/src/lib/crypto/shamirs.ts create mode 100644 cli/packages/cmd/kms.go create mode 100644 frontend/src/hooks/useFileDownload.tsx create mode 100644 frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx create mode 100644 frontend/src/views/admin/DashboardPage/components/ExportRootKmsKeyModalContent.tsx create mode 100644 frontend/src/views/admin/DashboardPage/components/RestoreRootKmsKeyModalContent.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index ef9f4e0d33..52fc81f832 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -90,6 +90,7 @@ "safe-regex": "^2.1.1", "scim-patch": "^0.8.3", "scim2-parse-filter": "^0.2.10", + "secrets.js-grempe": "^2.0.0", "sjcl": "^1.0.8", "smee-client": "^2.0.0", "snowflake-sdk": "^1.14.0", @@ -18365,6 +18366,12 @@ "resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz", "integrity": "sha512-k5TgGSuQEbR4jXRgw/GPAYVL9fMp1pWA2abLF5z3q9IGWSuZTqbrZBOSUezvc+rtViXr+czSZjg3eAN4QSTvxQ==" }, + "node_modules/secrets.js-grempe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/secrets.js-grempe/-/secrets.js-grempe-2.0.0.tgz", + "integrity": "sha512-4xkOIaDAg998dTFXZUJTOoVbdLHfB818SMeLJ69ABccgGEKokxsoRFupAFfAImloUSKv4QUGNMgKVbKMf6z0Ug==", + "license": "MIT" + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", diff --git a/backend/package.json b/backend/package.json index 0278689d33..d063550e0f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -195,6 +195,7 @@ "safe-regex": "^2.1.1", "scim-patch": "^0.8.3", "scim2-parse-filter": "^0.2.10", + "secrets.js-grempe": "^2.0.0", "sjcl": "^1.0.8", "smee-client": "^2.0.0", "snowflake-sdk": "^1.14.0", diff --git a/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts b/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts index a586f94b8b..ff76d8eea5 100644 --- a/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts +++ b/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts @@ -4,10 +4,12 @@ import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy"); + const hasExported = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "exported"); const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt"); await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => { if (!hasEncryptionStrategy) t.string("encryptionStrategy").defaultTo("BASIC"); + if (!hasExported) t.boolean("exported").defaultTo(false); if (!hasTimestampsCol) t.timestamps(true, true, true); }); } @@ -15,9 +17,11 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy"); const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt"); + const hasExported = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "exported"); await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => { if (hasEncryptionStrategy) t.dropColumn("encryptionStrategy"); if (hasTimestampsCol) t.dropTimestamps(true); + if (hasExported) t.dropColumn("exported"); }); } diff --git a/backend/src/db/schemas/kms-root-config.ts b/backend/src/db/schemas/kms-root-config.ts index d15e1dff89..950f818c44 100644 --- a/backend/src/db/schemas/kms-root-config.ts +++ b/backend/src/db/schemas/kms-root-config.ts @@ -13,6 +13,7 @@ export const KmsRootConfigSchema = z.object({ id: z.string().uuid(), encryptedRootKey: zodBuffer, encryptionStrategy: z.string(), + exported: z.boolean(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 0734cc24b7..54035bac9e 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -176,7 +176,7 @@ const envSchema = z .string() .optional() .transform((val) => { - if (process.env.NODE_ENV === "development") return "/usr/local/lib/softhsm/libsofthsm2.so"; + // if (process.env.NODE_ENV === "development") return "/usr/local/lib/softhsm/libsofthsm2.so"; return val; }) ), @@ -201,6 +201,11 @@ const envSchema = z HSM_SLOT: z.coerce.number().optional().default(0), HSM_MECHANISM: zpStr(z.string().optional().default("AES_GCM")) }) + // To ensure that basic encryption is always possible. + .refine( + (data) => data.ENCRYPTION_KEY != null || data.ROOT_ENCRYPTION_KEY != null, + "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." + ) .transform((data) => ({ ...data, diff --git a/backend/src/lib/crypto/index.ts b/backend/src/lib/crypto/index.ts index cc6acfb805..5ec76a6a2b 100644 --- a/backend/src/lib/crypto/index.ts +++ b/backend/src/lib/crypto/index.ts @@ -18,5 +18,6 @@ export { decryptSecrets, decryptSecretVersions } from "./secret-encryption"; +export { shamirsService } from "./shamirs"; export { verifyOfflineLicense } from "./signing"; export { generateSrpServerKey, srpCheckClientProof } from "./srp"; diff --git a/backend/src/lib/crypto/shamirs.ts b/backend/src/lib/crypto/shamirs.ts new file mode 100644 index 0000000000..121c146b24 --- /dev/null +++ b/backend/src/lib/crypto/shamirs.ts @@ -0,0 +1,38 @@ +import shamirs from "secrets.js-grempe"; + +import { getConfig } from "../config/env"; +import { symmetricCipherService, SymmetricEncryption } from "./cipher"; + +export const shamirsService = () => { + const $generateBasicEncryptionKey = () => { + const appCfg = getConfig(); + + const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; + const isBase64 = !appCfg.ENCRYPTION_KEY; + if (!encryptionKey) + throw new Error( + "Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?" + ); + + return Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); + }; + + const share = (secretBuffer: Buffer, partsCount: number, thresholdCount: number) => { + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const hexSecret = Buffer.from(cipher.encrypt(secretBuffer, $generateBasicEncryptionKey())).toString("hex"); + + const secretParts = shamirs.share(hexSecret, partsCount, thresholdCount); + return secretParts; + }; + + const combine = (parts: string[]) => { + const encryptedSecret = shamirs.combine(parts); + + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const decryptedSecret = cipher.decrypt(Buffer.from(encryptedSecret, "hex"), $generateBasicEncryptionKey()); + + return decryptedSecret; + }; + + return { share, combine }; +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 550277de1f..907ef66e71 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -572,6 +572,7 @@ export const registerRoutes = async ( userDAL, authService: loginService, serverCfgDAL: superAdminDAL, + kmsRootConfigDAL, orgService, keyStore, licenseService, diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index bc0c725f06..1136db3083 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { LoginMethod } from "@app/services/super-admin/super-admin-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; @@ -195,6 +196,107 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/kms-export", + config: { + rateLimit: writeLimit + }, + schema: { + response: { + 200: z.object({ + secretParts: z.array(z.string()) + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async () => { + const keyParts = await server.services.superAdmin.exportPlainKmsKey(); + + return { + secretParts: keyParts + }; + } + }); + + server.route({ + method: "POST", + url: "/kms-import", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + secretParts: z.array(z.string()) + }) + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + await server.services.superAdmin.importPlainKmsKey(req.body.secretParts); + } + }); + + server.route({ + method: "GET", + url: "/root-kms-config", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + strategies: z + .object({ + strategy: z.nativeEnum(RootKeyEncryptionStrategy), + name: z.string(), + enabled: z.boolean() + }) + .array(), + keyExported: z.boolean() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + + handler: async () => { + const encryptionDetails = await server.services.superAdmin.getConfiguredEncryptionStrategies(); + return encryptionDetails; + } + }); + + server.route({ + method: "POST", + url: "/encryption-strategies", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + strategy: z.nativeEnum(RootKeyEncryptionStrategy) + }) + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + await server.services.superAdmin.updateRootEncryptionStrategy(req.body.strategy); + } + }); + server.route({ method: "POST", url: "/signup", diff --git a/backend/src/services/kms/kms-fns.ts b/backend/src/services/kms/kms-fns.ts index 96196e1afc..06395272b2 100644 --- a/backend/src/services/kms/kms-fns.ts +++ b/backend/src/services/kms/kms-fns.ts @@ -1,5 +1,7 @@ import { SymmetricEncryption } from "@app/lib/crypto/cipher"; +export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; + export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => { switch (encryptionAlgorithm) { case SymmetricEncryption.AES_GCM_128: diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index def6413bb7..c7f20dd722 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -11,13 +11,13 @@ import { } from "@app/ee/services/external-kms/providers/model"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; -import { randomSecureBytes } from "@app/lib/crypto"; +import { randomSecureBytes, shamirsService } from "@app/lib/crypto"; import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; 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 { getByteLengthForAlgorithm, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns"; import { THsmServiceFactory } from "../hsm/hsm-service"; import { TOrgDALFactory } from "../org/org-dal"; @@ -50,8 +50,6 @@ type TKmsServiceFactoryDep = { export type TKmsServiceFactory = ReturnType; -const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; - const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key"; const KMS_ROOT_CREATION_WAIT_TIME = 10; @@ -614,7 +612,7 @@ export const kmsServiceFactory = ({ } }; - const $createBasicEncryptionKey = () => { + const $getBasicEncryptionKey = () => { const appCfg = getConfig(); const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; @@ -642,7 +640,7 @@ export const kmsServiceFactory = ({ // case 2: root key is encrypted with basic encryption if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Basic) { const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const encryptionKeyBuffer = $createBasicEncryptionKey(); + const encryptionKeyBuffer = $getBasicEncryptionKey(); return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); } @@ -660,7 +658,7 @@ export const kmsServiceFactory = ({ if (strategy === RootKeyEncryptionStrategy.Basic) { const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const encryptionKeyBuffer = $createBasicEncryptionKey(); + const encryptionKeyBuffer = $getBasicEncryptionKey(); return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer); } @@ -669,6 +667,31 @@ export const kmsServiceFactory = ({ throw new Error(`Invalid root key encryption strategy: ${strategy}`); }; + const exportRootEncryptionKeyParts = () => { + if (!ROOT_ENCRYPTION_KEY) { + throw new Error("Root encryption key not set"); + } + + const parts = shamirsService().share(ROOT_ENCRYPTION_KEY, 8, 4); + + return parts; + }; + + const importRootEncryptionKey = async (parts: string[]) => { + const decryptedRootKey = shamirsService().combine(parts); + + const encryptedRootKey = symmetricCipherService(SymmetricEncryption.AES_GCM_256).encrypt( + decryptedRootKey, + $getBasicEncryptionKey() + ); + + await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { + encryptedRootKey, + encryptionStrategy: RootKeyEncryptionStrategy.Basic + }); + ROOT_ENCRYPTION_KEY = decryptedRootKey; + }; + // by keeping the decrypted data key in inner scope // none of the entities outside can interact directly or expose the data key // NOTICE: If changing here update migrations/utils/kms @@ -853,9 +876,6 @@ export const kmsServiceFactory = ({ // akhilmhdh: a copy of this is made in migrations/utils/kms const startService = async () => { - const appCfg = getConfig(); - const hsmEnabled = hsmService.isActive(); - const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null); if (!lock) { await keyStore.waitTillReady({ @@ -875,34 +895,11 @@ export const kmsServiceFactory = ({ const decryptedRootKey = await $decryptRootKey(kmsRootConfig).catch((err) => { logger.error(err, `KMS: Failed to decrypt ROOT Key [strategy=${kmsRootConfig.encryptionStrategy}]`); - throw err; + // We do not want to throw on startup. If the HSM has issues, this will throw an error, causing the entire API to shut down. + // If the API shuts down, the user will have no way to do recovery by importing their backup decryption key and rolling back to basic encryption. + return Buffer.alloc(0); }); - const selectedEncryptionStrategy = appCfg.ROOT_KEY_ENCRYPTION_STRATEGY; - - // case: the users selected encryption key strategy does not match the one in the DB - // in this case we need to re-encrypt the key with the selected strategy, and update the strategy and key in the DB - if (selectedEncryptionStrategy !== kmsRootConfig.encryptionStrategy) { - logger.info( - { - newStrategy: selectedEncryptionStrategy, - configuredStrategy: kmsRootConfig.encryptionStrategy - }, - "KMS: Change in root encryption key strategy detected. Re-encrypting ROOT Key with selected strategy" - ); - const encryptedRootKey = await $encryptRootKey(decryptedRootKey, selectedEncryptionStrategy); - - if (!encryptedRootKey) { - logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy"); - throw new Error("Failed to re-encrypt ROOT Key with selected strategy"); - } - - await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { - encryptedRootKey, - encryptionStrategy: selectedEncryptionStrategy - }); - } - // set the flag so that other instance nodes can start await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true"); logger.info("KMS: Loading ROOT Key into Memory."); @@ -910,12 +907,11 @@ export const kmsServiceFactory = ({ return; } - logger.info("KMS: Generating ROOT Key"); - const selectedEncryptionStrategy = appCfg.ROOT_KEY_ENCRYPTION_STRATEGY; - + // case 2: no config is found, so we create a new root key with basic encryption + logger.info("KMS: Generating new ROOT Key"); const newRootKey = randomSecureBytes(32); - const encryptedRootKey = await $encryptRootKey(newRootKey, selectedEncryptionStrategy).catch((err) => { - logger.error({ hsmEnabled }, "KMS: Failed to encrypt ROOT Key"); + const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Basic).catch((err) => { + logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key"); throw err; }); @@ -923,7 +919,7 @@ export const kmsServiceFactory = ({ // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition id: KMS_ROOT_CONFIG_UUID, encryptedRootKey, - encryptionStrategy: selectedEncryptionStrategy + encryptionStrategy: RootKeyEncryptionStrategy.Basic }); // set the flag so that other instance nodes can start @@ -933,6 +929,32 @@ export const kmsServiceFactory = ({ ROOT_ENCRYPTION_KEY = newRootKey; }; + const updateEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => { + const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); + if (!kmsRootConfig) { + throw new NotFoundError({ message: "KMS root config not found" }); + } + + if (kmsRootConfig.encryptionStrategy === strategy) { + return; + } + + const decryptedRootKey = await $decryptRootKey(kmsRootConfig); + const encryptedRootKey = await $encryptRootKey(decryptedRootKey, strategy); + + if (!encryptedRootKey) { + logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy"); + throw new Error("Failed to re-encrypt ROOT Key with selected strategy"); + } + + await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { + encryptedRootKey, + encryptionStrategy: strategy + }); + + ROOT_ENCRYPTION_KEY = decryptedRootKey; + }; + return { startService, generateKmsKey, @@ -944,11 +966,14 @@ export const kmsServiceFactory = ({ encryptWithRootKey, decryptWithRootKey, getOrgKmsKeyId, + updateEncryptionStrategy, getProjectSecretManagerKmsKeyId, updateProjectSecretManagerKmsKey, getProjectKeyBackup, loadProjectKeyBackup, getKmsById, - createCipherPairWithDataKey + createCipherPairWithDataKey, + exportRootEncryptionKeyParts, + importRootEncryptionKey }; }; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 12c25de913..315b30b169 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -10,7 +10,10 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { AuthMethod } from "../auth/auth-type"; +import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns"; +import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; +import { RootKeyEncryptionStrategy } from "../kms/kms-types"; import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal"; @@ -20,7 +23,15 @@ type TSuperAdminServiceFactoryDep = { serverCfgDAL: TSuperAdminDALFactory; userDAL: TUserDALFactory; authService: Pick; - kmsService: Pick; + kmsService: Pick< + TKmsServiceFactory, + | "encryptWithRootKey" + | "decryptWithRootKey" + | "exportRootEncryptionKeyParts" + | "importRootEncryptionKey" + | "updateEncryptionStrategy" + >; + kmsRootConfigDAL: TKmsRootConfigDALFactory; orgService: Pick; keyStore: Pick; licenseService: Pick; @@ -47,6 +58,7 @@ export const superAdminServiceFactory = ({ authService, orgService, keyStore, + kmsRootConfigDAL, kmsService, licenseService }: TSuperAdminServiceFactoryDep) => { @@ -150,6 +162,35 @@ export const superAdminServiceFactory = ({ return updatedServerCfg; }; + const exportPlainKmsKey = async () => { + const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); + + if (!kmsRootConfig) { + throw new NotFoundError({ name: "KmsRootConfig", message: "KMS root configuration not found" }); + } + + if (kmsRootConfig.exported) { + throw new BadRequestError({ name: "KmsRootConfig", message: "KMS root configuration already exported" }); + } + + await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { exported: true }); + return kmsService.exportRootEncryptionKeyParts(); + }; + + const importPlainKmsKey = async (secretParts: string[]) => { + const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); + + if (!kmsRootConfig) { + throw new NotFoundError({ name: "KmsRootConfig", message: "KMS root configuration not found" }); + } + + if (!kmsRootConfig.exported) { + throw new BadRequestError({ name: "KmsRootConfig", message: "KMS root configuration was never exported" }); + } + + await kmsService.importRootEncryptionKey(secretParts); + }; + const adminSignUp = async ({ lastName, firstName, @@ -288,12 +329,69 @@ export const superAdminServiceFactory = ({ }; }; + const getConfiguredEncryptionStrategies = async () => { + const appCfg = getConfig(); + + const kmsRootCfg = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); + + if (!kmsRootCfg) { + throw new NotFoundError({ name: "KmsRootConfig", message: "KMS root configuration not found" }); + } + + const selectedStrategy = kmsRootCfg.encryptionStrategy; + const enabledStrategies: { enabled: boolean; strategy: RootKeyEncryptionStrategy; name: string }[] = []; + + if (appCfg.ROOT_ENCRYPTION_KEY || appCfg.ENCRYPTION_KEY) { + const basicStrategy = RootKeyEncryptionStrategy.Basic; + + enabledStrategies.push({ + name: "Regular Encryption", + enabled: selectedStrategy === basicStrategy, + strategy: basicStrategy + }); + } + if (appCfg.isHsmConfigured) { + const hsmStrategy = RootKeyEncryptionStrategy.Hsm; + + enabledStrategies.push({ + name: "Hardware Security Module (HSM)", + enabled: selectedStrategy === hsmStrategy, + strategy: hsmStrategy + }); + } + + return { + strategies: enabledStrategies, + keyExported: kmsRootCfg.exported + }; + }; + + const updateRootEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => { + const configuredStrategies = await getConfiguredEncryptionStrategies(); + + const foundStrategy = configuredStrategies.strategies.find((s) => s.strategy === strategy); + + if (!foundStrategy) { + throw new BadRequestError({ message: "Invalid encryption strategy" }); + } + + if (foundStrategy.enabled) { + throw new BadRequestError({ message: "The selected encryption strategy is already enabled" }); + } + + await kmsService.updateEncryptionStrategy(strategy); + }; + return { initServerCfg, updateServerCfg, adminSignUp, getUsers, deleteUser, - getAdminSlackConfig + getAdminSlackConfig, + updateRootEncryptionStrategy, + getConfiguredEncryptionStrategies, + exportPlainKmsKey, + importPlainKmsKey }; }; diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 35767cd3f1..d8d8849cbf 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -525,3 +525,40 @@ func CallUpdateRawSecretsV3(httpClient *resty.Client, request UpdateRawSecretByN return nil } + +func CallExportKmsRootEncryptionKey(httpClient *resty.Client) (ExportKmsRootKeyResponse, error) { + var exportKmsKeyResponse ExportKmsRootKeyResponse + response, err := httpClient. + R(). + SetResult(&exportKmsKeyResponse). + SetHeader("User-Agent", USER_AGENT). + Post(fmt.Sprintf("%v/v1/admin/kms-export", config.INFISICAL_URL)) + + if err != nil { + return ExportKmsRootKeyResponse{}, fmt.Errorf("CallSuperAdminExportKmsKey: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return ExportKmsRootKeyResponse{}, fmt.Errorf("CallSuperAdminExportKmsKey: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return exportKmsKeyResponse, nil +} + +func CallImportKmsRootEncryptionKey(httpClient *resty.Client, request ImportKmsRootKeyRequest) error { + response, err := httpClient. + R(). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v1/admin/kms-import", config.INFISICAL_URL)) + + if err != nil { + return fmt.Errorf("CallSuperAdminImportKmsKey: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return fmt.Errorf("CallSuperAdminImportKmsKey: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index f96c937099..faf0db5979 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -136,8 +136,8 @@ type GetOrganizationsResponse struct { } type SelectOrganizationResponse struct { - Token string `json:"token"` - MfaEnabled bool `json:"isMfaEnabled"` + Token string `json:"token"` + MfaEnabled bool `json:"isMfaEnabled"` } type SelectOrganizationRequest struct { @@ -617,3 +617,11 @@ type GetRawSecretV3ByNameResponse struct { } `json:"secret"` ETag string } + +type ExportKmsRootKeyResponse struct { + SecretParts []string `json:"secretParts"` +} + +type ImportKmsRootKeyRequest struct { + SecretParts []string `json:"secretParts"` +} diff --git a/cli/packages/cmd/kms.go b/cli/packages/cmd/kms.go new file mode 100644 index 0000000000..6808c35b9e --- /dev/null +++ b/cli/packages/cmd/kms.go @@ -0,0 +1,128 @@ +/* +Copyright (c) 2023 Infisical Inc. +*/ +package cmd + +import ( + "fmt" + "strings" + "time" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/fatih/color" + "github.com/go-resty/resty/v2" + "github.com/spf13/cobra" +) + +var kmsCmd = &cobra.Command{ + Use: "kms", + Short: "Manage your Infisical KMS encryption keys", + DisableFlagsInUseLine: true, + Example: "infisical kms", + Args: cobra.ExactArgs(0), + PreRun: func(cmd *cobra.Command, args []string) { + util.RequireLogin() + }, + Run: func(cmd *cobra.Command, args []string) { + }, +} + +// exportCmd represents the export command +var exportKeyCmd = &cobra.Command{ + Use: "export", + Short: "Used to export your Infisical root encryption key parts, to be used for recovery (infisical import-key [...parts])", + DisableFlagsInUseLine: true, + Example: "infisical kms export", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + + loggedInDetails, err := util.GetCurrentLoggedInUserDetails() + + if err != nil { + util.HandleError(err) + } + + if !loggedInDetails.IsUserLoggedIn || loggedInDetails.LoginExpired { + util.HandleError(fmt.Errorf("You must be logged in to run this command")) + } + + httpClient := resty.New() + httpClient.SetAuthToken(loggedInDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") + + res, err := api.CallExportKmsRootEncryptionKey(httpClient) + + if err != nil { + + if strings.Contains(err.Error(), "configuration already exported") { + util.HandleError(fmt.Errorf("This KMS encryption key has already been exported. You can only export the decryption key once.")) + } else { + util.HandleError(err) + } + } + + boldGreen := color.New(color.FgGreen).Add(color.Bold) + time.Sleep(time.Second * 1) + boldGreen.Printf(">>>> Successfully exported KMS encryption key\n\n") + + plainBold := color.New(color.Bold) + + for i, part := range res.SecretParts { + plainBold.Printf("Part %d: %v\n", i+1, part) + } + + boldYellow := color.New(color.FgYellow).Add(color.Bold) + boldYellow.Printf("\nPlease store these parts in a secure location. You will need them to recover your KMS encryption key.\nYou will not be able to export these credentials again in the future.\n\n") + }, +} + +var importKeyCmd = &cobra.Command{ + Use: "import", + Short: "Used to import your Infisical root encryption key parts, to be used for recovery (infisical import-key [...parts])", + DisableFlagsInUseLine: true, + Example: "infisical kms import", + Args: cobra.MinimumNArgs(6), + Run: func(cmd *cobra.Command, args []string) { + loggedInDetails, err := util.GetCurrentLoggedInUserDetails() + + if err != nil { + util.HandleError(err) + } + + if !loggedInDetails.IsUserLoggedIn || loggedInDetails.LoginExpired { + util.HandleError(fmt.Errorf("You must be logged in to run this command")) + } + + httpClient := resty.New() + httpClient.SetAuthToken(loggedInDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") + + err = api.CallImportKmsRootEncryptionKey(httpClient, api.ImportKmsRootKeyRequest{ + SecretParts: args, + }) + + if err != nil { + if strings.Contains(err.Error(), "configuration was never exported") { + util.HandleError(fmt.Errorf("This KMS encryption key has not been exported yet. You must export the key first before you can import it.")) + } else { + util.HandleError(err) + } + } + + boldGreen := color.New(color.FgGreen).Add(color.Bold) + time.Sleep(time.Second * 1) + boldGreen.Printf(">>>> Successfully imported KMS encryption key\n\n") + + boldYellow := color.New(color.FgYellow).Add(color.Bold) + boldYellow.Printf("Important: Make sure to set the `ROOT_KEY_ENCRYPTION_STRATEGY` environment variable to `BASIC` on your Infisical instance.\nNot doing this will likely result in having to re-import the key on the next instance restart.\n\n") + }, +} + +func init() { + kmsCmd.AddCommand(exportKeyCmd) + kmsCmd.AddCommand(importKeyCmd) + + rootCmd.AddCommand(kmsCmd) + +} diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index 658feeaa31..d3b3d17eb6 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -1,7 +1,15 @@ export { useAdminDeleteUser, useCreateAdminUser, + useExportServerDecryptionKey, + useImportServerDecryptionKey, useUpdateAdminSlackConfig, - useUpdateServerConfig + useUpdateServerConfig, + useUpdateServerEncryptionStrategy } from "./mutation"; -export { useAdminGetUsers, useGetAdminSlackConfig, useGetServerConfig } from "./queries"; +export { + useAdminGetUsers, + useGetAdminSlackConfig, + useGetServerConfig, + useGetServerRootKmsEncryptionDetails +} from "./queries"; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 9ab51d250b..b2e6b96071 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -7,6 +7,7 @@ import { User } from "../users/types"; import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { AdminSlackConfig, + RootKeyEncryptionStrategy, TCreateAdminUserDTO, TServerConfig, TUpdateAdminSlackConfigDTO @@ -85,3 +86,36 @@ export const useUpdateAdminSlackConfig = () => { } }); }; + +export const useUpdateServerEncryptionStrategy = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (strategy: RootKeyEncryptionStrategy) => { + await apiRequest.post("/api/v1/admin/encryption-strategies", { strategy }); + }, + onSuccess: () => { + queryClient.invalidateQueries(adminQueryKeys.getServerEncryptionStrategies()); + } + }); +}; + +export const useExportServerDecryptionKey = () => { + return useMutation({ + mutationFn: async () => { + const { data } = await apiRequest.post<{ secretParts: string[] }>("/api/v1/admin/kms-export"); + return data.secretParts; + } + }); +}; + +export const useImportServerDecryptionKey = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (secretParts: string[]) => { + await apiRequest.post("/api/v1/admin/kms-import", { secretParts }); + }, + onSuccess: () => { + queryClient.invalidateQueries(adminQueryKeys.serverConfig()); + } + }); +}; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 653b124d77..0ab3750e4f 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -3,7 +3,12 @@ import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-que import { apiRequest } from "@app/config/request"; import { User } from "../types"; -import { AdminGetUsersFilters, AdminSlackConfig, TServerConfig } from "./types"; +import { + AdminGetUsersFilters, + AdminSlackConfig, + TGetServerRootKmsEncryptionDetails, + TServerConfig +} from "./types"; export const adminStandaloneKeys = { getUsers: "get-users" @@ -12,7 +17,8 @@ export const adminStandaloneKeys = { export const adminQueryKeys = { serverConfig: () => ["server-config"] as const, getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const, - getAdminSlackConfig: () => ["admin-slack-config"] as const + getAdminSlackConfig: () => ["admin-slack-config"] as const, + getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const }; const fetchServerConfig = async () => { @@ -61,8 +67,8 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => { }); }; -export const useGetAdminSlackConfig = () => - useQuery({ +export const useGetAdminSlackConfig = () => { + return useQuery({ queryKey: adminQueryKeys.getAdminSlackConfig(), queryFn: async () => { const { data } = await apiRequest.get( @@ -72,3 +78,17 @@ export const useGetAdminSlackConfig = () => return data; } }); +}; + +export const useGetServerRootKmsEncryptionDetails = () => { + return useQuery({ + queryKey: adminQueryKeys.getServerEncryptionStrategies(), + queryFn: async () => { + const { data } = await apiRequest.get( + "/api/v1/admin/root-kms-config" + ); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index c95912b5e5..056391f85c 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -54,3 +54,17 @@ export type AdminSlackConfig = { clientId: string; clientSecret: string; }; + +export type TGetServerRootKmsEncryptionDetails = { + strategies: { + strategy: RootKeyEncryptionStrategy; + enabled: boolean; + name: string; + }[]; + keyExported: boolean; +}; + +export enum RootKeyEncryptionStrategy { + Basic = "BASIC", + Hsm = "HSM" +} diff --git a/frontend/src/hooks/useFileDownload.tsx b/frontend/src/hooks/useFileDownload.tsx new file mode 100644 index 0000000000..6cf833cde4 --- /dev/null +++ b/frontend/src/hooks/useFileDownload.tsx @@ -0,0 +1,13 @@ +import { useCallback } from "react"; + +export const useFileDownload = () => { + return useCallback((content: string, filename: string) => { + const downloadUrl = `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`; + const link = document.createElement("a"); + link.href = downloadUrl; + link.setAttribute("download", filename); + document.body.appendChild(link); + link.click(); + link.remove(); + }, []); +}; diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index d682abf2b0..67c2a84ca1 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -22,15 +22,21 @@ import { Tabs } from "@app/components/v2"; import { useOrganization, useServerConfig, useUser } from "@app/context"; -import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api"; +import { + useGetOrganizations, + useGetServerRootKmsEncryptionDetails, + useUpdateServerConfig +} from "@app/hooks/api"; import { AuthPanel } from "./AuthPanel"; +import { EncryptionPanel } from "./EncryptionPanel"; import { IntegrationPanel } from "./IntegrationPanel"; import { RateLimitPanel } from "./RateLimitPanel"; import { UserPanel } from "./UserPanel"; enum TabSections { Settings = "settings", + Encryption = "encryption", Auth = "auth", RateLimit = "rate-limit", Integrations = "integrations", @@ -55,6 +61,7 @@ type TDashboardForm = z.infer; export const AdminDashboardPage = () => { const router = useRouter(); const data = useServerConfig(); + const { data: serverRootKmsDetails } = useGetServerRootKmsEncryptionDetails(); const { config } = data; const { @@ -137,6 +144,7 @@ export const AdminDashboardPage = () => {
General + {!!serverRootKmsDetails && Encryption} Authentication Rate Limit Integrations @@ -321,6 +329,11 @@ export const AdminDashboardPage = () => { + {!!serverRootKmsDetails && ( + + + + )} diff --git a/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx b/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx new file mode 100644 index 0000000000..7a7794db74 --- /dev/null +++ b/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx @@ -0,0 +1,182 @@ +import { useCallback } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Modal, Select, SelectItem, Tooltip } from "@app/components/v2"; +import { usePopUp } from "@app/hooks"; +import { useUpdateServerEncryptionStrategy } from "@app/hooks/api"; +import { + RootKeyEncryptionStrategy, + TGetServerRootKmsEncryptionDetails +} from "@app/hooks/api/admin/types"; + +import { ExportRootKmsKeyModalContent } from "./components/ExportRootKmsKeyModalContent"; +import { RestoreRootKmsKeyModalContent } from "./components/RestoreRootKmsKeyModalContent"; + +const formSchema = z.object({ + encryptionStrategy: z.nativeEnum(RootKeyEncryptionStrategy) +}); + +type TForm = z.infer; + +type Props = { + rootKmsDetails: TGetServerRootKmsEncryptionDetails; +}; + +export const EncryptionPanel = ({ rootKmsDetails }: Props) => { + const { mutateAsync: updateEncryptionStrategy } = useUpdateServerEncryptionStrategy(); + const { handlePopUpToggle, handlePopUpOpen, popUp } = usePopUp([ + "exportKey", + "restoreKey" + ] as const); + + const { + control, + handleSubmit, + formState: { isSubmitting, isDirty } + } = useForm({ + resolver: zodResolver(formSchema), + values: { + encryptionStrategy: + rootKmsDetails?.strategies?.find((s) => s.enabled)?.strategy ?? + RootKeyEncryptionStrategy.Basic + } + }); + + const onSubmit = useCallback(async (formData: TForm) => { + try { + await updateEncryptionStrategy(formData.encryptionStrategy); + + if ( + !rootKmsDetails.keyExported && + formData.encryptionStrategy !== RootKeyEncryptionStrategy.Basic + ) { + handlePopUpOpen("exportKey"); + } + + createNotification({ + type: "success", + text: "Encryption strategy updated successfully" + }); + } catch { + createNotification({ + type: "error", + text: "Failed to update encryption strategy" + }); + } + }, []); + + return ( + <> +
+
+
+
+ KMS Encryption Strategy +
+ + {!rootKmsDetails.keyExported && ( +
+ + You have not exported the KMS root encryption key. Switch to HSM encryption or + run the{" "} + + + infisical kms export + + {" "} + CLI command to export the key parts. +
+ )} +
+ If you experience issues with accessing projects while not using Regular + Encryption (default), you can restore the KMS root encryption key by using your + exported key parts. +

+ If you do not have the exported key parts, you can export them by using the CLI + command +
+ + + infisical kms export + + + .
+
+ + Please keep in mind that you can only export the key parts once. + +
+ } + > + + +
+
+ Select which type of encryption strategy you want to use for your KMS root key. HSM is + supported on Enterprise plans. +
+ + ( + + + + )} + /> +
+ + + + + handlePopUpToggle("exportKey", state)} + > + + + + handlePopUpToggle("restoreKey", state)} + > + + + + ); +}; diff --git a/frontend/src/views/admin/DashboardPage/components/ExportRootKmsKeyModalContent.tsx b/frontend/src/views/admin/DashboardPage/components/ExportRootKmsKeyModalContent.tsx new file mode 100644 index 0000000000..f59a8ac416 --- /dev/null +++ b/frontend/src/views/admin/DashboardPage/components/ExportRootKmsKeyModalContent.tsx @@ -0,0 +1,53 @@ +import { useCallback, useState } from "react"; + +import { Button, ModalContent } from "@app/components/v2"; +import { useExportServerDecryptionKey } from "@app/hooks/api"; +import { useFileDownload } from "@app/hooks/useFileDownload"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + handlePopUpToggle: (popUpName: keyof UsePopUpState<["exportKey"]>, state?: boolean) => void; +}; + +export const ExportRootKmsKeyModalContent = ({ handlePopUpToggle }: Props) => { + const { mutateAsync: exportKey, isLoading } = useExportServerDecryptionKey(); + const downloadFile = useFileDownload(); + const [downloaded, setDownloaded] = useState(false); + + const onExport = useCallback(async () => { + const keyParts = await exportKey(); + downloadFile(keyParts.join("\n\n"), "infisical-encryption-key-parts.txt"); + setDownloaded(true); + }, []); + + return ( + +
+ {!downloaded ? ( + <> + + + + + ) : ( +
+ The key parts have been downloaded. Please store them in a safe place. You will need + these keys incase you need to recovery the KMS root encryption key. Please consult our + documentation for further instructions. +
+ )} +
+
+ ); +}; diff --git a/frontend/src/views/admin/DashboardPage/components/RestoreRootKmsKeyModalContent.tsx b/frontend/src/views/admin/DashboardPage/components/RestoreRootKmsKeyModalContent.tsx new file mode 100644 index 0000000000..8db6d0fbec --- /dev/null +++ b/frontend/src/views/admin/DashboardPage/components/RestoreRootKmsKeyModalContent.tsx @@ -0,0 +1,102 @@ +import { useMemo } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, ModalContent } from "@app/components/v2"; +import { useImportServerDecryptionKey } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + handlePopUpToggle: (popUpName: keyof UsePopUpState<["restoreKey"]>, state?: boolean) => void; +}; + +const formSchema = z.object({ + keyParts: z + .array(z.string()) + .refine((data) => data.length === 4 && data.every((part) => part.length > 0), { + message: "Enter at least 4 key parts in order to restore the KMS root decryption key." + }) +}); +type TForm = z.infer; + +export const RestoreRootKmsKeyModalContent = ({ handlePopUpToggle }: Props) => { + const { mutateAsync: importKmsRootKey } = useImportServerDecryptionKey(); + + const { + control, + handleSubmit, + watch, + formState: { isSubmitting, errors, isLoading, isValid } + } = useForm({ + resolver: zodResolver(formSchema), + values: { + keyParts: ["", "", "", ""] + } + }); + + const keyParts = useMemo(() => watch("keyParts"), []); + + console.log("lol", watch("keyParts")); + console.log("isVal", isValid); + console.log("errors", errors); + + return ( + + + + + } + > +
+
+ {keyParts.map((_, index) => ( + ( +
+ + + +
+ )} + /> + ))} + {errors.keyParts && ( +
{errors.keyParts.message}
+ )} +
+
+
+ ); +}; diff --git a/package-lock.json b/package-lock.json index 8e44e829eb..b57a8437f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,8 @@ "name": "infisical", "license": "ISC", "dependencies": { - "@radix-ui/react-radio-group": "^1.1.3" + "@radix-ui/react-radio-group": "^1.1.3", + "secrets.js-grempe": "^2.0.0" }, "devDependencies": { "@types/uuid": "^9.0.7", @@ -1392,6 +1393,12 @@ "loose-envify": "^1.1.0" } }, + "node_modules/secrets.js-grempe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/secrets.js-grempe/-/secrets.js-grempe-2.0.0.tgz", + "integrity": "sha512-4xkOIaDAg998dTFXZUJTOoVbdLHfB818SMeLJ69ABccgGEKokxsoRFupAFfAImloUSKv4QUGNMgKVbKMf6z0Ug==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2457,6 +2464,11 @@ "loose-envify": "^1.1.0" } }, + "secrets.js-grempe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/secrets.js-grempe/-/secrets.js-grempe-2.0.0.tgz", + "integrity": "sha512-4xkOIaDAg998dTFXZUJTOoVbdLHfB818SMeLJ69ABccgGEKokxsoRFupAFfAImloUSKv4QUGNMgKVbKMf6z0Ug==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 2ecb7217ee..10ec57eba4 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "husky": "^8.0.3" }, "dependencies": { - "@radix-ui/react-radio-group": "^1.1.3" + "@radix-ui/react-radio-group": "^1.1.3", + "secrets.js-grempe": "^2.0.0" } }