From f096a567de2ea0df9cc8dab8750531184b8412da Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 29 Oct 2024 02:47:34 +0400 Subject: [PATCH 01/32] feat: Hardware security modules --- backend/Dockerfile.dev | 39 +++ backend/package-lock.json | 39 ++- backend/package.json | 1 + backend/src/@types/fastify.d.ts | 2 + .../migrations/20241028134337_kms-with-hsm.ts | 23 ++ backend/src/db/schemas/kms-root-config.ts | 5 +- backend/src/lib/config/env.ts | 41 ++- backend/src/main.ts | 8 +- backend/src/server/app.ts | 6 +- backend/src/server/routes/index.ts | 24 +- backend/src/services/hsm/hsm-fns.ts | 40 +++ backend/src/services/hsm/hsm-service.ts | 276 ++++++++++++++++++ backend/src/services/kms/kms-service.ts | 65 ++++- 13 files changed, 552 insertions(+), 17 deletions(-) create mode 100644 backend/src/db/migrations/20241028134337_kms-with-hsm.ts create mode 100644 backend/src/services/hsm/hsm-fns.ts create mode 100644 backend/src/services/hsm/hsm-service.ts diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index ec34f63d7b..97bc2c6a36 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -1,5 +1,44 @@ FROM node:20-alpine +# ? Setup a test SoftHSM module. In production a real HSM is used. + +ARG SOFTHSM2_VERSION=2.5.0 + +ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \ + SOFTHSM2_SOURCES=/tmp/softhsm2 + +# install build dependencies including python3 +RUN apk --update add \ + alpine-sdk \ + autoconf \ + automake \ + git \ + libtool \ + openssl-dev \ + python3 \ + make \ + g++ + +# build and install SoftHSM2 +RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES} +WORKDIR ${SOFTHSM2_SOURCES} + +RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \ + && sh autogen.sh \ + && ./configure --prefix=/usr/local --disable-gost \ + && make \ + && make install + +WORKDIR /root +RUN rm -fr ${SOFTHSM2_SOURCES} + +# install pkcs11-tool +RUN apk --update add opensc + +RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 + +# ? App setup + RUN apk add --no-cache bash curl && curl -1sLf \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ && apk add infisical=0.8.1 && apk add --no-cache git diff --git a/backend/package-lock.json b/backend/package-lock.json index 6829ebbc89..ef9f4e0d33 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -55,6 +55,7 @@ "fastify-plugin": "^4.5.1", "google-auth-library": "^9.9.0", "googleapis": "^137.1.0", + "graphene-pk11": "^2.3.6", "handlebars": "^4.7.8", "hdb": "^0.19.10", "ioredis": "^5.3.2", @@ -13556,6 +13557,23 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/graphene-pk11": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/graphene-pk11/-/graphene-pk11-2.3.6.tgz", + "integrity": "sha512-ol9Pf7XDv5UTjh1DPqtmQVZQqUheiXBzQVXQWRCLWq78+brKQB0Kum/s0NGEcsd/5NQQG8MFA2U/KNujEoC1fQ==", + "license": "MIT", + "dependencies": { + "pkcs11js": "^2.1.6", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/PeculiarVentures" + } + }, "node_modules/graphql": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", @@ -17066,6 +17084,20 @@ "node": ">= 6" } }, + "node_modules/pkcs11js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/pkcs11js/-/pkcs11js-2.1.6.tgz", + "integrity": "sha512-+t5jxzB749q8GaEd1yNx3l98xYuaVK6WW/Vjg1Mk1Iy5bMu/A5W4O/9wZGrpOknWF6lFQSb12FXX+eSNxdriwA==", + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/PeculiarVentures" + } + }, "node_modules/pkg-conf": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", @@ -19761,9 +19793,10 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "license": "0BSD" }, "node_modules/tsup": { "version": "8.0.1", diff --git a/backend/package.json b/backend/package.json index 1336478a1f..0278689d33 100644 --- a/backend/package.json +++ b/backend/package.json @@ -160,6 +160,7 @@ "fastify-plugin": "^4.5.1", "google-auth-library": "^9.9.0", "googleapis": "^137.1.0", + "graphene-pk11": "^2.3.6", "handlebars": "^4.7.8", "hdb": "^0.19.10", "ioredis": "^5.3.2", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index d79c224e5a..ebe3f4289d 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -44,6 +44,7 @@ import { TCmekServiceFactory } from "@app/services/cmek/cmek-service"; import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; +import { THsmServiceFactory } from "@app/services/hsm/hsm-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; @@ -184,6 +185,7 @@ declare module "fastify" { rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; externalKms: TExternalKmsServiceFactory; + hsm: THsmServiceFactory; orgAdmin: TOrgAdminServiceFactory; slack: TSlackServiceFactory; workflowIntegration: TWorkflowIntegrationServiceFactory; diff --git a/backend/src/db/migrations/20241028134337_kms-with-hsm.ts b/backend/src/db/migrations/20241028134337_kms-with-hsm.ts new file mode 100644 index 0000000000..86fc556d65 --- /dev/null +++ b/backend/src/db/migrations/20241028134337_kms-with-hsm.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasIsEncryptedByHsmCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "isEncryptedByHsm"); + const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt"); + + await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => { + if (!hasIsEncryptedByHsmCol) t.boolean("isEncryptedByHsm").defaultTo(false).notNullable(); + if (!hasTimestampsCol) t.timestamps(true, true); + }); +} + +export async function down(knex: Knex): Promise { + const hasIsEncryptedByHsmCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "isEncryptedByHsm"); + const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt"); + + await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => { + if (hasIsEncryptedByHsmCol) t.dropColumn("isEncryptedByHsm"); + if (hasTimestampsCol) t.dropTimestamps(true); + }); +} diff --git a/backend/src/db/schemas/kms-root-config.ts b/backend/src/db/schemas/kms-root-config.ts index d2c0edbc5e..978d108bd0 100644 --- a/backend/src/db/schemas/kms-root-config.ts +++ b/backend/src/db/schemas/kms-root-config.ts @@ -11,7 +11,10 @@ import { TImmutableDBKeys } from "./models"; export const KmsRootConfigSchema = z.object({ id: z.string().uuid(), - encryptedRootKey: zodBuffer + encryptedRootKey: zodBuffer, + isEncryptedByHsm: z.boolean().default(false), + createdAt: z.date(), + updatedAt: z.date() }); export type TKmsRootConfig = z.infer; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 638f21e5d7..b289968bfa 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -163,7 +163,38 @@ const envSchema = z SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"), WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()), WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()), - ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true") + ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"), + + // HSM + HSM_LIB_PATH: zpStr( + z + .string() + .optional() + .transform((val) => { + if (process.env.NODE_ENV === "development") return "/usr/local/lib/softhsm/libsofthsm2.so"; + return val; + }) + ), + HSM_PIN: zpStr( + z + .string() + .optional() + .transform((val) => { + if (process.env.NODE_ENV === "development") return "1234"; + return val; + }) + ), + HSM_KEY_LABEL: zpStr( + z + .string() + .optional() + .transform((val) => { + if (process.env.NODE_ENV === "development") return "auth-app"; + return val; + }) + ), + HSM_SLOT: z.coerce.number().optional().default(0), + HSM_MECHANISM: zpStr(z.string().optional().default("AES_GCM")) }) .transform((data) => ({ ...data, @@ -175,10 +206,18 @@ const envSchema = z isRedisConfigured: Boolean(data.REDIS_URL), isDevelopmentMode: data.NODE_ENV === "development", isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED, + isSecretScanningConfigured: Boolean(data.SECRET_SCANNING_GIT_APP_ID) && Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET), + isHsmConfigured: + Boolean(data.HSM_LIB_PATH) && + Boolean(data.HSM_PIN) && + Boolean(data.HSM_KEY_LABEL) && + Boolean(data.HSM_MECHANISM) && + data.HSM_SLOT !== undefined, + samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") })); diff --git a/backend/src/main.ts b/backend/src/main.ts index f71a1fe95a..f0ed3fddbd 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -9,6 +9,7 @@ import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; import { bootstrapCheck } from "./server/boot-strap-check"; +import { initializePkcs11Module } from "./services/hsm/hsm-fns"; import { smtpServiceFactory } from "./services/smtp/smtp-service"; dotenv.config(); @@ -53,13 +54,17 @@ const run = async () => { const queue = queueServiceFactory(appCfg.REDIS_URL); const keyStore = keyStoreFactory(appCfg.REDIS_URL); - const server = await main({ db, auditLogDb, smtp, logger, queue, keyStore }); + const pkcs11Module = initializePkcs11Module(); + pkcs11Module.initialize(); + + const server = await main({ db, auditLogDb, hsmModule: pkcs11Module.getModule(), smtp, logger, queue, keyStore }); const bootstrap = await bootstrapCheck({ db }); // eslint-disable-next-line process.on("SIGINT", async () => { await server.close(); await db.destroy(); + pkcs11Module.finalize(); process.exit(0); }); @@ -67,6 +72,7 @@ const run = async () => { process.on("SIGTERM", async () => { await server.close(); await db.destroy(); + pkcs11Module.finalize(); process.exit(0); }); diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index b768d0db5b..03f3f98f95 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -17,6 +17,7 @@ import { Logger } from "pino"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; +import { HsmModule } from "@app/services/hsm/hsm-fns"; import { TSmtpService } from "@app/services/smtp/smtp-service"; import { globalRateLimiterCfg } from "./config/rateLimiter"; @@ -36,10 +37,11 @@ type TMain = { logger?: Logger; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory; + hsmModule: HsmModule; }; // Run the server! -export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TMain) => { +export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore }: TMain) => { const appCfg = getConfig(); const server = fastify({ logger: appCfg.NODE_ENV === "test" ? false : logger, @@ -95,7 +97,7 @@ export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TM await server.register(maintenanceMode); - await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore }); + await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule }); if (appCfg.isProductionMode) { await server.register(registerExternalNextjs, { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b2d0970925..550277de1f 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1,5 +1,4 @@ import { CronJob } from "cron"; -// import { Redis } from "ioredis"; import { Knex } from "knex"; import { z } from "zod"; @@ -108,6 +107,8 @@ import { externalMigrationServiceFactory } from "@app/services/external-migratio import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service"; +import { HsmModule } from "@app/services/hsm/hsm-fns"; +import { hsmServiceFactory } from "@app/services/hsm/hsm-service"; import { identityDALFactory } from "@app/services/identity/identity-dal"; import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal"; @@ -223,10 +224,18 @@ export const registerRoutes = async ( { auditLogDb, db, + hsmModule, smtp: smtpService, queue: queueService, keyStore - }: { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory } + }: { + auditLogDb?: Knex; + db: Knex; + hsmModule: HsmModule; + smtp: TSmtpService; + queue: TQueueServiceFactory; + keyStore: TKeyStoreFactory; + } ) => { const appCfg = getConfig(); await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" }); @@ -352,14 +361,21 @@ export const registerRoutes = async ( projectDAL }); const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); + + const hsmService = hsmServiceFactory({ + pkcs11Module: hsmModule + }); + const kmsService = kmsServiceFactory({ kmsRootConfigDAL, keyStore, kmsDAL, internalKmsDAL, orgDAL, - projectDAL + projectDAL, + hsmService }); + const externalKmsService = externalKmsServiceFactory({ kmsDAL, kmsService, @@ -1265,6 +1281,7 @@ export const registerRoutes = async ( // setup the communication with license key server await licenseService.init(); + hsmService.startService(); await telemetryQueue.startTelemetryCheck(); await dailyResourceCleanUp.startCleanUp(); await dailyExpiringPkiItemAlert.startSendingAlerts(); @@ -1342,6 +1359,7 @@ export const registerRoutes = async ( secretSharing: secretSharingService, userEngagement: userEngagementService, externalKms: externalKmsService, + hsm: hsmService, cmek: cmekService, orgAdmin: orgAdminService, slack: slackService, diff --git a/backend/src/services/hsm/hsm-fns.ts b/backend/src/services/hsm/hsm-fns.ts new file mode 100644 index 0000000000..83b05568ea --- /dev/null +++ b/backend/src/services/hsm/hsm-fns.ts @@ -0,0 +1,40 @@ +import * as grapheneLib from "graphene-pk11"; + +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; + +export type HsmModule = { + module: grapheneLib.Module | null; + graphene: typeof grapheneLib; +}; + +export const initializePkcs11Module = () => { + const appCfg = getConfig(); + + let module: grapheneLib.Module | null = null; + + const initialize = () => { + if (!appCfg.isHsmConfigured) { + return; + } + + module = grapheneLib.Module.load(appCfg.HSM_LIB_PATH!, "SoftHSM"); + module.initialize(); + logger.info("PKCS#11 module initialized"); + }; + + const finalize = () => { + if (module) { + module.finalize(); + logger.info("PKCS#11 module finalized"); + } + }; + + const getModule = (): HsmModule => ({ module, graphene: grapheneLib }); + + return { + initialize, + finalize, + getModule + }; +}; diff --git a/backend/src/services/hsm/hsm-service.ts b/backend/src/services/hsm/hsm-service.ts new file mode 100644 index 0000000000..c9c4fb4f6d --- /dev/null +++ b/backend/src/services/hsm/hsm-service.ts @@ -0,0 +1,276 @@ +import grapheneLib from "graphene-pk11"; + +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; + +import { HsmModule } from "./hsm-fns"; + +type THsmServiceFactoryDep = { + pkcs11Module: HsmModule; +}; +const SESSION_TIMEOUT = 5 * 60 * 1000; // 5 minutes +const USER_ALREADY_LOGGED_IN_ERROR = "CKR_USER_ALREADY_LOGGED_IN"; + +export type THsmServiceFactory = ReturnType; + +class HsmSessionManager { + private session: grapheneLib.Session | null = null; + + private lastUsed: number = 0; + + private module: grapheneLib.Module; + + private graphene: typeof grapheneLib; + + private sessionCheckInterval: NodeJS.Timeout | null = null; + + private startSessionMonitoring() { + // Check session health every minute + this.sessionCheckInterval = setInterval(() => { + this.checkAndRefreshSession(); + }, 60 * 1000); // 1 minute + } + + private checkAndRefreshSession() { + if (!this.session) return; + + const now = Date.now(); + if (now - this.lastUsed > SESSION_TIMEOUT) { + logger.info("Session expired, cleaning up..."); + this.cleanup(); + } + } + + private cleanup() { + if (this.session) { + try { + this.session.logout(); + this.session.close(); + } catch (error) { + logger.error("Error during session cleanup:", error); + } + this.session = null; + } + + if (this.sessionCheckInterval) { + clearInterval(this.sessionCheckInterval); + this.sessionCheckInterval = null; + } + } + + getSession(): grapheneLib.Session { + const appCfg = getConfig(); + + // If we have a valid session, update its last used time and return it + if (this.session) { + try { + // Try a simple operation to verify session is still valid + this.session.generateRandom(16); + this.lastUsed = Date.now(); + return this.session; + } catch (error) { + logger.info("HSM Session validation failed, creating new session..."); + this.cleanup(); + } + } + + // Create new session + const slot = this.module.getSlots(appCfg.HSM_SLOT); + // eslint-disable-next-line no-bitwise + if (!(slot.flags & this.graphene.SlotFlag.TOKEN_PRESENT)) { + throw new Error("Slot is not initialized"); + } + + // eslint-disable-next-line no-bitwise + const session = slot.open(this.graphene.SessionFlag.RW_SESSION | this.graphene.SessionFlag.SERIAL_SESSION); + + try { + session.login(appCfg.HSM_PIN!); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- The error is of type `Pkcs11Error`, but this error is not exported by graphene. And we don't want to install another library just for an error assertion. + if (error.message !== USER_ALREADY_LOGGED_IN_ERROR) { + throw error; + } + } + + this.session = session; + this.lastUsed = Date.now(); + + return session; + } + + constructor(module: grapheneLib.Module, graphene: typeof grapheneLib) { + this.module = module; + this.graphene = graphene; + + this.startSessionMonitoring(); + } +} + +// eslint-disable-next-line no-empty-pattern +export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmServiceFactoryDep) => { + const appCfg = getConfig(); + + // Constants for buffer structure + const IV_LENGTH = 16; + const TAG_LENGTH = 16; + + let sessionManager: HsmSessionManager | null = null; + + const $findKey = (session: grapheneLib.Session) => { + // Find the existing AES key + const template = { + class: graphene.ObjectClass.SECRET_KEY, + keyType: graphene.KeyType.AES, + label: appCfg.HSM_KEY_LABEL + } as grapheneLib.ITemplate; + + const key = session.find(template).items(0); + + if (!key) { + throw new Error("Failed to encrypt data, AES key not found"); + } + + return key; + }; + + const $keyExists = (session: grapheneLib.Session): boolean => { + try { + const key = $findKey(session); + // items(0) will throw an error if no items are found + // Return true only if we got a valid object with handle + return key && typeof key.handle !== "undefined"; + } catch (error) { + // If items(0) throws, it means no key was found + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call + if ((error as any).message?.includes("CKR_OBJECT_HANDLE_INVALID")) { + return false; + } + logger.error(error, "Error checking for HSM key presence"); + return false; + } + }; + + const isActive = async () => { + if (!module || !appCfg.isHsmConfigured || !sessionManager) { + return false; + } + + return appCfg.isHsmConfigured && module !== null; + }; + + const startService = () => { + if (!appCfg.isHsmConfigured || !module) return; + + sessionManager = new HsmSessionManager(module, graphene); + const session = sessionManager.getSession(); + + try { + // Check if key already exists + if ($keyExists(session)) { + logger.info("Key already exists, skipping creation"); + } else { + // Generate 256-bit AES key with persistent storage + session.generateKey(graphene.KeyGenMechanism.AES, { + class: graphene.ObjectClass.SECRET_KEY, + token: true, // This ensures the key is stored persistently + valueLen: 256 / 8, + keyType: graphene.KeyType.AES, + label: appCfg.HSM_KEY_LABEL, + encrypt: true, + decrypt: true, + extractable: false, // Prevent key export + sensitive: true, // Mark as sensitive data + private: true // Require login to access + }); + logger.info(`Key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); + } + + const mechs = session.slot.getMechanisms(); + let gotAesGcmMechanism = false; + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < mechs.length; i++) { + const mech = mechs.items(i); + if (mech.name === "AES_GCM") { + gotAesGcmMechanism = true; + break; + } + } + + if (!gotAesGcmMechanism) { + throw new Error("Failed to initialize HSM. AES GCM encryption mechanism not supported by the HSM"); + } + } catch (error) { + logger.error(error, "Error creating HSM key"); + throw error; + } + }; + + function encrypt(data: Buffer): Buffer { + if (!module) { + throw new Error("PKCS#11 module is not initialized"); + } + + if (!sessionManager) { + throw new Error("HSM Session manager is not initialized"); + } + const session = sessionManager.getSession(); + const key = $findKey(session); + + // Generate IV + const iv = session.generateRandom(IV_LENGTH); + const alg = { + name: appCfg.HSM_MECHANISM, + params: new graphene.AesGcm240Params(iv) + } as grapheneLib.IAlgorithm; + + const cipher = session.createCipher(alg, new graphene.Key(key).toType()); + + // Calculate the output buffer size based on input length + // GCM adds a 16-byte auth tag, so we need input length + 16 + const outputBuffer = Buffer.alloc(data.length + TAG_LENGTH); + const encryptedData = cipher.once(data, outputBuffer); + + // Combine IV + encrypted data into a single buffer + // Format: [IV (16 bytes)][Encrypted Data][Auth Tag (16 bytes)] + return Buffer.concat([iv, encryptedData]); + } + + function decrypt(encryptedBlob: Buffer): Buffer { + if (!module) { + throw new Error("PKCS#11 module is not initialized"); + } + + if (!sessionManager) { + throw new Error("HSM Session manager is not initialized"); + } + + const session = sessionManager.getSession(); + const key = $findKey(session); + + // Extract IV, ciphertext, and tag from the blob + const iv = encryptedBlob.subarray(0, IV_LENGTH); + const ciphertext = encryptedBlob.subarray(IV_LENGTH, encryptedBlob.length); + + const algo = { + name: appCfg.HSM_MECHANISM, + params: new graphene.AesGcm240Params(iv) // Pass both IV and tag + }; + + const decipher = session.createDecipher(algo, new graphene.Key(key).toType()); + + // Allocate buffer for decrypted data + const outputBuffer = Buffer.alloc(ciphertext.length); + + const decrypted = decipher.once(ciphertext, outputBuffer); + return decrypted; + } + return { + encrypt, + startService, + isActive, + decrypt + }; +}; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 1b5c282a65..2a18d9ee15 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -19,6 +19,7 @@ import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { getByteLengthForAlgorithm } from "@app/services/kms/kms-fns"; +import { THsmServiceFactory } from "../hsm/hsm-service"; import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { TInternalKmsDALFactory } from "./internal-kms-dal"; @@ -40,9 +41,10 @@ type TKmsServiceFactoryDep = { kmsDAL: TKmsKeyDALFactory; projectDAL: Pick; orgDAL: Pick; - kmsRootConfigDAL: Pick; + kmsRootConfigDAL: Pick; keyStore: Pick; internalKmsDAL: Pick; + hsmService: THsmServiceFactory; }; export type TKmsServiceFactory = ReturnType; @@ -63,7 +65,8 @@ export const kmsServiceFactory = ({ keyStore, internalKmsDAL, orgDAL, - projectDAL + projectDAL, + hsmService }: TKmsServiceFactoryDep) => { let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); @@ -801,6 +804,7 @@ export const kmsServiceFactory = ({ const isBase64 = !appCfg.ENCRYPTION_KEY; if (!encryptionKey) throw new Error("Root encryption key not found for KMS service."); const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); + const hsmEnabled = await hsmService.isActive(); const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null); if (!lock) { @@ -817,8 +821,43 @@ export const kmsServiceFactory = ({ if (kmsRootConfig) { if (lock) await lock.release(); logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting."); - const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); - // set the flag so that other instancen nodes can start + + let decryptedRootKey: Buffer | null = null; + try { + decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); + logger.info("KMS: Decrypted ROOT Key with platform key."); + } catch (err) { + // First we attempt to decrypt with regular root key. If it fails, we check if HSM is enabled, and if it is we attempt to decrypt with HSM. + if (hsmEnabled && kmsRootConfig.isEncryptedByHsm) { + decryptedRootKey = hsmService.decrypt(kmsRootConfig.encryptedRootKey); + logger.info("KMS: Decrypted ROOT Key with HSM."); + } else { + // If HSM is not enabled we assume it's a general error, and throw. + throw err; + } + } + + if (!decryptedRootKey) { + logger.error( + { hsmEnabled, isEncryptedByHsm: kmsRootConfig.isEncryptedByHsm }, + "KMS: Failed to decrypt ROOT Key" + ); + throw new Error("Failed to decrypt ROOT Key"); + } + + // If the key is not encrypted with HSM, we re-encrypt it with HSM and update the key in the DB. + if (!kmsRootConfig.isEncryptedByHsm && hsmEnabled) { + const encryptedRootKey = hsmService.encrypt(decryptedRootKey); + + if (!encryptedRootKey) { + logger.error("KMS: Failed to encrypt ROOT Key with HSM"); + throw new Error("Failed to encrypt ROOT Key with HSM"); + } + + await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { encryptedRootKey, isEncryptedByHsm: true }); + } + + // 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."); ROOT_ENCRYPTION_KEY = decryptedRootKey; @@ -827,9 +866,23 @@ export const kmsServiceFactory = ({ logger.info("KMS: Generating ROOT Key"); const newRootKey = randomSecureBytes(32); - const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer); + + let encryptedRootKey: Buffer | null = null; + let isEncryptedByHsm = false; + + if (hsmEnabled) { + encryptedRootKey = hsmService.encrypt(newRootKey); + isEncryptedByHsm = true; + } else { + encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer); + } + + if (!encryptedRootKey) { + logger.error({ hsmEnabled }, "KMS: Failed to encrypt ROOT Key"); + } + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID }); + await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID, isEncryptedByHsm }); // set the flag so that other instancen nodes can start await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true"); From cfc0b2fb8ddde1160a82bc6cc7e15317ae215c1d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 29 Oct 2024 22:07:02 +0400 Subject: [PATCH 02/32] fix: renamed migration --- ...-with-hsm.ts => 20241028134337_kms-root-cfg-hsm.ts} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename backend/src/db/migrations/{20241028134337_kms-with-hsm.ts => 20241028134337_kms-root-cfg-hsm.ts} (57%) diff --git a/backend/src/db/migrations/20241028134337_kms-with-hsm.ts b/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts similarity index 57% rename from backend/src/db/migrations/20241028134337_kms-with-hsm.ts rename to backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts index 86fc556d65..a586f94b8b 100644 --- a/backend/src/db/migrations/20241028134337_kms-with-hsm.ts +++ b/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts @@ -3,21 +3,21 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { - const hasIsEncryptedByHsmCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "isEncryptedByHsm"); + const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy"); const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt"); await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => { - if (!hasIsEncryptedByHsmCol) t.boolean("isEncryptedByHsm").defaultTo(false).notNullable(); - if (!hasTimestampsCol) t.timestamps(true, true); + if (!hasEncryptionStrategy) t.string("encryptionStrategy").defaultTo("BASIC"); + if (!hasTimestampsCol) t.timestamps(true, true, true); }); } export async function down(knex: Knex): Promise { - const hasIsEncryptedByHsmCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "isEncryptedByHsm"); + const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy"); const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt"); await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => { - if (hasIsEncryptedByHsmCol) t.dropColumn("isEncryptedByHsm"); + if (hasEncryptionStrategy) t.dropColumn("encryptionStrategy"); if (hasTimestampsCol) t.dropTimestamps(true); }); } From a807f0cf6c31685a000125cf74a83eca9ff117cb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 29 Oct 2024 22:08:17 +0400 Subject: [PATCH 03/32] feat: added option for choosing encryption method --- backend/src/db/schemas/kms-root-config.ts | 2 +- backend/src/lib/config/env.ts | 8 ++ backend/src/services/hsm/hsm-service.ts | 2 +- backend/src/services/kms/kms-service.ts | 146 ++++++++++++++-------- backend/src/services/kms/kms-types.ts | 5 + 5 files changed, 109 insertions(+), 54 deletions(-) diff --git a/backend/src/db/schemas/kms-root-config.ts b/backend/src/db/schemas/kms-root-config.ts index 978d108bd0..d15e1dff89 100644 --- a/backend/src/db/schemas/kms-root-config.ts +++ b/backend/src/db/schemas/kms-root-config.ts @@ -12,7 +12,7 @@ import { TImmutableDBKeys } from "./models"; export const KmsRootConfigSchema = z.object({ id: z.string().uuid(), encryptedRootKey: zodBuffer, - isEncryptedByHsm: z.boolean().default(false), + encryptionStrategy: z.string(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index b289968bfa..0734cc24b7 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -1,6 +1,8 @@ import { Logger } from "pino"; import { z } from "zod"; +import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; + import { removeTrailingSlash } from "../fn"; import { zpStr } from "../zod"; @@ -165,6 +167,9 @@ const envSchema = z WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()), ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"), + // KMS ENCRYPTION + ROOT_KEY_ENCRYPTION_STRATEGY: z.nativeEnum(RootKeyEncryptionStrategy).default(RootKeyEncryptionStrategy.Basic), + // HSM HSM_LIB_PATH: zpStr( z @@ -198,6 +203,9 @@ const envSchema = z }) .transform((data) => ({ ...data, + + // ROOT_KEY_ENCRYPTION_STRATEGY: "HSM", + DB_READ_REPLICAS: data.DB_READ_REPLICAS ? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS)) : undefined, diff --git a/backend/src/services/hsm/hsm-service.ts b/backend/src/services/hsm/hsm-service.ts index c9c4fb4f6d..4aa5154e7f 100644 --- a/backend/src/services/hsm/hsm-service.ts +++ b/backend/src/services/hsm/hsm-service.ts @@ -152,7 +152,7 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe } }; - const isActive = async () => { + const isActive = () => { if (!module || !appCfg.isHsmConfigured || !sessionManager) { return false; } diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 2a18d9ee15..def6413bb7 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -2,7 +2,7 @@ import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; import { z } from "zod"; -import { KmsKeysSchema } from "@app/db/schemas"; +import { KmsKeysSchema, TKmsRootConfig } from "@app/db/schemas"; import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms"; import { ExternalKmsAwsSchema, @@ -28,6 +28,7 @@ import { TKmsRootConfigDALFactory } from "./kms-root-config-dal"; import { KmsDataKey, KmsType, + RootKeyEncryptionStrategy, TDecryptWithKeyDTO, TDecryptWithKmsDTO, TEncryptionWithKeyDTO, @@ -613,6 +614,61 @@ export const kmsServiceFactory = ({ } }; + const $createBasicEncryptionKey = () => { + 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?" + ); + + const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); + + return encryptionKeyBuffer; + }; + + const $decryptRootKey = async (kmsRootConfig: TKmsRootConfig) => { + // case 1: root key is encrypted with HSM + if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Hsm) { + if (!hsmService.isActive()) { + throw new Error("Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); + } + + return hsmService.decrypt(kmsRootConfig.encryptedRootKey); + } + + // case 2: root key is encrypted with basic encryption + if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Basic) { + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const encryptionKeyBuffer = $createBasicEncryptionKey(); + + return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); + } + + throw new Error(`Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}`); + }; + + const $encryptRootKey = async (plainKeyBuffer: Buffer, strategy: RootKeyEncryptionStrategy) => { + if (strategy === RootKeyEncryptionStrategy.Hsm) { + if (!hsmService.isActive()) { + throw new Error("Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); + } + return hsmService.encrypt(plainKeyBuffer); + } + + if (strategy === RootKeyEncryptionStrategy.Basic) { + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const encryptionKeyBuffer = $createBasicEncryptionKey(); + + return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer); + } + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Invalid root key encryption strategy: ${strategy}`); + }; + // 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 @@ -798,13 +854,7 @@ export const kmsServiceFactory = ({ // akhilmhdh: a copy of this is made in migrations/utils/kms const startService = async () => { const appCfg = getConfig(); - // This will switch to a seal process and HMS flow in future - const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; - // if root key its base64 encoded - const isBase64 = !appCfg.ENCRYPTION_KEY; - if (!encryptionKey) throw new Error("Root encryption key not found for KMS service."); - const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); - const hsmEnabled = await hsmService.isActive(); + const hsmEnabled = hsmService.isActive(); const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null); if (!lock) { @@ -817,44 +867,40 @@ export const kmsServiceFactory = ({ // check if KMS root key was already generated and saved in DB const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + + // case 1: a root key already exists in the DB if (kmsRootConfig) { if (lock) await lock.release(); logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting."); - let decryptedRootKey: Buffer | null = null; - try { - decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); - logger.info("KMS: Decrypted ROOT Key with platform key."); - } catch (err) { - // First we attempt to decrypt with regular root key. If it fails, we check if HSM is enabled, and if it is we attempt to decrypt with HSM. - if (hsmEnabled && kmsRootConfig.isEncryptedByHsm) { - decryptedRootKey = hsmService.decrypt(kmsRootConfig.encryptedRootKey); - logger.info("KMS: Decrypted ROOT Key with HSM."); - } else { - // If HSM is not enabled we assume it's a general error, and throw. - throw err; - } - } + const decryptedRootKey = await $decryptRootKey(kmsRootConfig).catch((err) => { + logger.error(err, `KMS: Failed to decrypt ROOT Key [strategy=${kmsRootConfig.encryptionStrategy}]`); + throw err; + }); - if (!decryptedRootKey) { - logger.error( - { hsmEnabled, isEncryptedByHsm: kmsRootConfig.isEncryptedByHsm }, - "KMS: Failed to decrypt ROOT Key" + 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" ); - throw new Error("Failed to decrypt ROOT Key"); - } - - // If the key is not encrypted with HSM, we re-encrypt it with HSM and update the key in the DB. - if (!kmsRootConfig.isEncryptedByHsm && hsmEnabled) { - const encryptedRootKey = hsmService.encrypt(decryptedRootKey); + const encryptedRootKey = await $encryptRootKey(decryptedRootKey, selectedEncryptionStrategy); if (!encryptedRootKey) { - logger.error("KMS: Failed to encrypt ROOT Key with HSM"); - throw new Error("Failed to encrypt ROOT Key with HSM"); + 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, isEncryptedByHsm: true }); + await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { + encryptedRootKey, + encryptionStrategy: selectedEncryptionStrategy + }); } // set the flag so that other instance nodes can start @@ -865,26 +911,22 @@ export const kmsServiceFactory = ({ } logger.info("KMS: Generating ROOT Key"); - const newRootKey = randomSecureBytes(32); - - let encryptedRootKey: Buffer | null = null; - let isEncryptedByHsm = false; + const selectedEncryptionStrategy = appCfg.ROOT_KEY_ENCRYPTION_STRATEGY; - if (hsmEnabled) { - encryptedRootKey = hsmService.encrypt(newRootKey); - isEncryptedByHsm = true; - } else { - encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer); - } - - if (!encryptedRootKey) { + const newRootKey = randomSecureBytes(32); + const encryptedRootKey = await $encryptRootKey(newRootKey, selectedEncryptionStrategy).catch((err) => { logger.error({ hsmEnabled }, "KMS: Failed to encrypt ROOT Key"); - } + throw err; + }); - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID, isEncryptedByHsm }); + await kmsRootConfigDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: KMS_ROOT_CONFIG_UUID, + encryptedRootKey, + encryptionStrategy: selectedEncryptionStrategy + }); - // set the flag so that other instancen nodes can start + // 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: Saved and loaded ROOT Key into memory"); if (lock) await lock.release(); diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index 5d5b77a093..e1aa7b5e0a 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -56,3 +56,8 @@ export type TUpdateProjectSecretManagerKmsKeyDTO = { projectId: string; kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string }; }; + +export enum RootKeyEncryptionStrategy { + Basic = "BASIC", + Hsm = "HSM" +} From 891a1ea2b9041ad19298c82b045b8f19eceefb15 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 31 Oct 2024 17:59:41 +0400 Subject: [PATCH 04/32] 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" } } From b7b3d07e9fa4ec32fb42fe61b736254b2c8e27cc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 31 Oct 2024 18:08:43 +0400 Subject: [PATCH 05/32] cleanup --- .../components/RestoreRootKmsKeyModalContent.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/views/admin/DashboardPage/components/RestoreRootKmsKeyModalContent.tsx b/frontend/src/views/admin/DashboardPage/components/RestoreRootKmsKeyModalContent.tsx index 8db6d0fbec..8122c44e33 100644 --- a/frontend/src/views/admin/DashboardPage/components/RestoreRootKmsKeyModalContent.tsx +++ b/frontend/src/views/admin/DashboardPage/components/RestoreRootKmsKeyModalContent.tsx @@ -38,10 +38,6 @@ export const RestoreRootKmsKeyModalContent = ({ handlePopUpToggle }: Props) => { const keyParts = useMemo(() => watch("keyParts"), []); - console.log("lol", watch("keyParts")); - console.log("isVal", isValid); - console.log("errors", errors); - return ( Date: Thu, 31 Oct 2024 18:12:01 +0400 Subject: [PATCH 06/32] Update vitest-environment-knex.ts --- backend/e2e-test/vitest-environment-knex.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 7be0b860f8..d44fc0729b 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -16,6 +16,7 @@ import { initDbConnection } from "@app/db"; import { queueServiceFactory } from "@app/queue"; import { keyStoreFactory } from "@app/keystore/keystore"; import { Redis } from "ioredis"; +import { initializePkcs11Module } from "@app/services/hsm/hsm-fns"; dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); export default { @@ -54,7 +55,12 @@ export default { const smtp = mockSmtpServer(); const queue = queueServiceFactory(cfg.REDIS_URL); const keyStore = keyStoreFactory(cfg.REDIS_URL); - const server = await main({ db, smtp, logger, queue, keyStore }); + + const pkcs11Module = initializePkcs11Module(); + pkcs11Module.initialize(); + + const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: pkcs11Module.getModule() }); + // @ts-expect-error type globalThis.testServer = server; // @ts-expect-error type From 472f5eb8b406f0db1203b20f2c312615550b4779 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 31 Oct 2024 18:32:01 +0400 Subject: [PATCH 07/32] Update env.ts --- backend/src/lib/config/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 54035bac9e..69a3d72ca4 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; }) ), From 3989646b80fc9b83905b7162b1639772856da28c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 31 Oct 2024 23:28:05 +0400 Subject: [PATCH 08/32] fix: dockerfile --- backend/Dockerfile | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 2153ba33a1..6948445472 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -3,6 +3,17 @@ FROM node:20-alpine AS build WORKDIR /app +RUN apk --update add \ + alpine-sdk \ + autoconf \ + automake \ + git \ + libtool \ + openssl-dev \ + python3 \ + make \ + g++ + COPY package*.json ./ RUN npm ci --only-production @@ -11,12 +22,23 @@ RUN npm run build # Production stage FROM node:20-alpine - WORKDIR /app ENV npm_config_cache /home/node/.npm COPY package*.json ./ + +RUN apk --update add \ + alpine-sdk \ + autoconf \ + automake \ + git \ + libtool \ + openssl-dev \ + python3 \ + make \ + g++ + RUN npm ci --only-production && npm cache clean --force COPY --from=build /app . From 472f02e8b129fb6cc67b2890a6429d3d2b24503b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 1 Nov 2024 15:47:43 +0400 Subject: [PATCH 09/32] feat: added key wrapping --- backend/src/services/hsm/hsm-service.ts | 119 ++++++++++++++++-------- 1 file changed, 80 insertions(+), 39 deletions(-) diff --git a/backend/src/services/hsm/hsm-service.ts b/backend/src/services/hsm/hsm-service.ts index 4aa5154e7f..5d870ce95c 100644 --- a/backend/src/services/hsm/hsm-service.ts +++ b/backend/src/services/hsm/hsm-service.ts @@ -118,8 +118,8 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe let sessionManager: HsmSessionManager | null = null; - const $findKey = (session: grapheneLib.Session) => { - // Find the existing AES key + const $findMasterKey = (session: grapheneLib.Session) => { + // Find the master key (root key) const template = { class: graphene.ObjectClass.SECRET_KEY, keyType: graphene.KeyType.AES, @@ -129,15 +129,58 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe const key = session.find(template).items(0); if (!key) { - throw new Error("Failed to encrypt data, AES key not found"); + throw new Error("Failed to find master key"); } return key; }; + const $generateAndWrapKey = (session: grapheneLib.Session) => { + const masterKey = $findMasterKey(session); + + // Generate a new session key for encryption + const sessionKey = session.generateKey(graphene.KeyGenMechanism.AES, { + class: graphene.ObjectClass.SECRET_KEY, + keyType: graphene.KeyType.AES, + token: false, // Session-only key + sensitive: true, + extractable: true, // Must be true to allow wrapping + encrypt: true, + decrypt: true, + valueLen: 32 // 256-bit key + } as grapheneLib.ITemplate); + + // Wrap the session key with master key + const wrappingMech = { name: "AES_KEY_WRAP", params: null }; + const wrappedKey = session.wrapKey( + wrappingMech, + new graphene.Key(masterKey).toType(), + new graphene.Key(sessionKey).toType() + ); + + return { wrappedKey, sessionKey }; + }; + + const $unwrapKey = (session: grapheneLib.Session, wrappedKey: Buffer) => { + const masterKey = $findMasterKey(session); + + // Absolute minimal template - let HSM set most attributes + const unwrapTemplate = { + class: graphene.ObjectClass.SECRET_KEY, + keyType: graphene.KeyType.AES + } as grapheneLib.ITemplate; + + const unwrappingMech = { + name: "AES_KEY_WRAP", + params: null + } as grapheneLib.MechanismType; + + return session.unwrapKey(unwrappingMech, new graphene.Key(masterKey).toType(), wrappedKey, unwrapTemplate); + }; + const $keyExists = (session: grapheneLib.Session): boolean => { try { - const key = $findKey(session); + const key = $findMasterKey(session); // items(0) will throw an error if no items are found // Return true only if we got a valid object with handle return key && typeof key.handle !== "undefined"; @@ -167,26 +210,24 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe const session = sessionManager.getSession(); try { - // Check if key already exists - if ($keyExists(session)) { - logger.info("Key already exists, skipping creation"); - } else { - // Generate 256-bit AES key with persistent storage + // Check if master key exists, create if not + if (!$keyExists(session)) { + // Generate 256-bit AES master key with persistent storage session.generateKey(graphene.KeyGenMechanism.AES, { class: graphene.ObjectClass.SECRET_KEY, - token: true, // This ensures the key is stored persistently + token: true, valueLen: 256 / 8, keyType: graphene.KeyType.AES, label: appCfg.HSM_KEY_LABEL, - encrypt: true, - decrypt: true, - extractable: false, // Prevent key export - sensitive: true, // Mark as sensitive data - private: true // Require login to access + derive: true, // Enable key derivation + extractable: false, + sensitive: true, + private: true }); - logger.info(`Key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); + logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); } + // Verify HSM supports required mechanisms const mechs = session.slot.getMechanisms(); let gotAesGcmMechanism = false; @@ -200,10 +241,10 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe } if (!gotAesGcmMechanism) { - throw new Error("Failed to initialize HSM. AES GCM encryption mechanism not supported by the HSM"); + throw new Error("HSM does not support AES_GCM mechanism"); } } catch (error) { - logger.error(error, "Error creating HSM key"); + logger.error(error, "Error initializing HSM service"); throw error; } }; @@ -217,55 +258,55 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe throw new Error("HSM Session manager is not initialized"); } const session = sessionManager.getSession(); - const key = $findKey(session); - // Generate IV + // Generate IV for encryption const iv = session.generateRandom(IV_LENGTH); + + // Generate and wrap a new session key + const { wrappedKey, sessionKey } = $generateAndWrapKey(session); + const alg = { name: appCfg.HSM_MECHANISM, params: new graphene.AesGcm240Params(iv) } as grapheneLib.IAlgorithm; - const cipher = session.createCipher(alg, new graphene.Key(key).toType()); + const cipher = session.createCipher(alg, new graphene.Key(sessionKey).toType()); // Calculate the output buffer size based on input length // GCM adds a 16-byte auth tag, so we need input length + 16 const outputBuffer = Buffer.alloc(data.length + TAG_LENGTH); const encryptedData = cipher.once(data, outputBuffer); - // Combine IV + encrypted data into a single buffer - // Format: [IV (16 bytes)][Encrypted Data][Auth Tag (16 bytes)] - return Buffer.concat([iv, encryptedData]); + // Format: [Wrapped Key (40)][IV (16)][Encrypted Data + Tag] + return Buffer.concat([wrappedKey, iv, encryptedData]); } function decrypt(encryptedBlob: Buffer): Buffer { - if (!module) { - throw new Error("PKCS#11 module is not initialized"); - } + const WRAPPED_KEY_LENGTH = 32 + 8; // AES-256 key + padding - if (!sessionManager) { - throw new Error("HSM Session manager is not initialized"); + if (!module || !sessionManager) { + throw new Error("HSM service not initialized"); } const session = sessionManager.getSession(); - const key = $findKey(session); - // Extract IV, ciphertext, and tag from the blob - const iv = encryptedBlob.subarray(0, IV_LENGTH); - const ciphertext = encryptedBlob.subarray(IV_LENGTH, encryptedBlob.length); + // Extract wrapped key, IV, and ciphertext + const wrappedKey = encryptedBlob.subarray(0, WRAPPED_KEY_LENGTH); + const iv = encryptedBlob.subarray(WRAPPED_KEY_LENGTH, WRAPPED_KEY_LENGTH + IV_LENGTH); + const ciphertext = encryptedBlob.subarray(WRAPPED_KEY_LENGTH + IV_LENGTH); + + // Unwrap the session key + const sessionKey = $unwrapKey(session, wrappedKey); const algo = { name: appCfg.HSM_MECHANISM, - params: new graphene.AesGcm240Params(iv) // Pass both IV and tag + params: new graphene.AesGcm240Params(iv) }; - const decipher = session.createDecipher(algo, new graphene.Key(key).toType()); - - // Allocate buffer for decrypted data + const decipher = session.createDecipher(algo, new graphene.Key(sessionKey).toType()); const outputBuffer = Buffer.alloc(ciphertext.length); - const decrypted = decipher.once(ciphertext, outputBuffer); - return decrypted; + return decipher.once(ciphertext, outputBuffer); } return { encrypt, From a3ec1a27dedc17b47b5fb9d2d87fe37158ff4442 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 3 Nov 2024 22:13:45 +0400 Subject: [PATCH 10/32] fix: removed recovery --- backend/package-lock.json | 7 - backend/package.json | 1 - .../src/ee/services/license/license-fns.ts | 1 + .../src/ee/services/license/license-types.ts | 1 + backend/src/lib/crypto/index.ts | 1 - backend/src/lib/crypto/shamirs.ts | 38 ------ backend/src/server/routes/v1/admin-router.ts | 51 +------ backend/src/services/kms/kms-service.ts | 31 +---- .../super-admin/super-admin-service.ts | 51 ++----- cli/packages/api/api.go | 37 ----- cli/packages/api/model.go | 8 -- cli/packages/cmd/kms.go | 128 ------------------ frontend/src/hooks/api/admin/index.ts | 2 - frontend/src/hooks/api/admin/mutation.ts | 21 --- frontend/src/hooks/api/admin/types.ts | 1 - frontend/src/hooks/api/subscriptions/types.ts | 1 + .../admin/DashboardPage/EncryptionPanel.tsx | 94 +++---------- .../ExportRootKmsKeyModalContent.tsx | 53 -------- .../RestoreRootKmsKeyModalContent.tsx | 98 -------------- 19 files changed, 34 insertions(+), 591 deletions(-) delete mode 100644 backend/src/lib/crypto/shamirs.ts delete mode 100644 cli/packages/cmd/kms.go delete mode 100644 frontend/src/views/admin/DashboardPage/components/ExportRootKmsKeyModalContent.tsx delete mode 100644 frontend/src/views/admin/DashboardPage/components/RestoreRootKmsKeyModalContent.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index 52fc81f832..ef9f4e0d33 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -90,7 +90,6 @@ "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", @@ -18366,12 +18365,6 @@ "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 d063550e0f..0278689d33 100644 --- a/backend/package.json +++ b/backend/package.json @@ -195,7 +195,6 @@ "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/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 0864ab33cb..8e86851a0f 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -29,6 +29,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ auditLogStreams: false, auditLogStreamLimit: 3, samlSSO: false, + hsm: true, oidcSSO: false, scim: false, ldap: false, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 0ba54afc31..622b0e06b0 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -46,6 +46,7 @@ export type TFeatureSet = { auditLogStreams: false; auditLogStreamLimit: 3; samlSSO: false; + hsm: false; oidcSSO: false; scim: false; ldap: false; diff --git a/backend/src/lib/crypto/index.ts b/backend/src/lib/crypto/index.ts index 5ec76a6a2b..cc6acfb805 100644 --- a/backend/src/lib/crypto/index.ts +++ b/backend/src/lib/crypto/index.ts @@ -18,6 +18,5 @@ 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 deleted file mode 100644 index 121c146b24..0000000000 --- a/backend/src/lib/crypto/shamirs.ts +++ /dev/null @@ -1,38 +0,0 @@ -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/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 1136db3083..f3c1212330 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -196,54 +196,6 @@ 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", @@ -259,8 +211,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { name: z.string(), enabled: z.boolean() }) - .array(), - keyExported: z.boolean() + .array() }) } }, diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index c7f20dd722..df6f4692b0 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -11,7 +11,7 @@ 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, shamirsService } from "@app/lib/crypto"; +import { randomSecureBytes } 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"; @@ -667,31 +667,6 @@ 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 @@ -972,8 +947,6 @@ export const kmsServiceFactory = ({ getProjectKeyBackup, loadProjectKeyBackup, getKmsById, - createCipherPairWithDataKey, - exportRootEncryptionKeyParts, - importRootEncryptionKey + createCipherPairWithDataKey }; }; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 315b30b169..97f314db80 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -23,14 +23,7 @@ type TSuperAdminServiceFactoryDep = { serverCfgDAL: TSuperAdminDALFactory; userDAL: TUserDALFactory; authService: Pick; - kmsService: Pick< - TKmsServiceFactory, - | "encryptWithRootKey" - | "decryptWithRootKey" - | "exportRootEncryptionKeyParts" - | "importRootEncryptionKey" - | "updateEncryptionStrategy" - >; + kmsService: Pick; kmsRootConfigDAL: TKmsRootConfigDALFactory; orgService: Pick; keyStore: Pick; @@ -162,35 +155,6 @@ 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, @@ -361,12 +325,17 @@ export const superAdminServiceFactory = ({ } return { - strategies: enabledStrategies, - keyExported: kmsRootCfg.exported + strategies: enabledStrategies }; }; const updateRootEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => { + if (!licenseService.onPremFeatures.hsm) { + throw new BadRequestError({ + message: "Failed to update encryption strategy due to plan restriction. Upgrade to Infisical's Enterprise plan." + }); + } + const configuredStrategies = await getConfiguredEncryptionStrategies(); const foundStrategy = configuredStrategies.strategies.find((s) => s.strategy === strategy); @@ -390,8 +359,6 @@ export const superAdminServiceFactory = ({ deleteUser, getAdminSlackConfig, updateRootEncryptionStrategy, - getConfiguredEncryptionStrategies, - exportPlainKmsKey, - importPlainKmsKey + getConfiguredEncryptionStrategies }; }; diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index d8d8849cbf..35767cd3f1 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -525,40 +525,3 @@ 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 faf0db5979..adf7a814d3 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -617,11 +617,3 @@ 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 deleted file mode 100644 index 6808c35b9e..0000000000 --- a/cli/packages/cmd/kms.go +++ /dev/null @@ -1,128 +0,0 @@ -/* -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 d3b3d17eb6..5405878c7c 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -1,8 +1,6 @@ export { useAdminDeleteUser, useCreateAdminUser, - useExportServerDecryptionKey, - useImportServerDecryptionKey, useUpdateAdminSlackConfig, useUpdateServerConfig, useUpdateServerEncryptionStrategy diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index b2e6b96071..30fffca3b2 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -98,24 +98,3 @@ export const useUpdateServerEncryptionStrategy = () => { } }); }; - -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/types.ts b/frontend/src/hooks/api/admin/types.ts index 056391f85c..0fa6d23ef1 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -61,7 +61,6 @@ export type TGetServerRootKmsEncryptionDetails = { enabled: boolean; name: string; }[]; - keyExported: boolean; }; export enum RootKeyEncryptionStrategy { diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 1ab01ef05b..b1c4e224d2 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -23,6 +23,7 @@ export type SubscriptionPlan = { workspacesUsed: number; environmentLimit: number; samlSSO: boolean; + hsm: boolean; oidcSSO: boolean; scim: boolean; ldap: boolean; diff --git a/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx b/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx index 7a7794db74..ad32df9982 100644 --- a/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx +++ b/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx @@ -1,12 +1,11 @@ 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 { Button, FormControl, Select, SelectItem, UpgradePlanModal } from "@app/components/v2"; +import { useSubscription } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useUpdateServerEncryptionStrategy } from "@app/hooks/api"; import { @@ -14,9 +13,6 @@ import { 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) }); @@ -29,10 +25,9 @@ type Props = { export const EncryptionPanel = ({ rootKmsDetails }: Props) => { const { mutateAsync: updateEncryptionStrategy } = useUpdateServerEncryptionStrategy(); - const { handlePopUpToggle, handlePopUpOpen, popUp } = usePopUp([ - "exportKey", - "restoreKey" - ] as const); + const { subscription } = useSubscription(); + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); const { control, @@ -48,16 +43,18 @@ export const EncryptionPanel = ({ rootKmsDetails }: Props) => { }); const onSubmit = useCallback(async (formData: TForm) => { + if (!subscription) return; + + if (!subscription.hsm) { + handlePopUpOpen("upgradePlan", { + description: "Hardware Security Module's (HSM's), are only available on Enterprise plans." + }); + return; + } + try { await updateEncryptionStrategy(formData.encryptionStrategy); - if ( - !rootKmsDetails.keyExported && - formData.encryptionStrategy !== RootKeyEncryptionStrategy.Basic - ) { - handlePopUpOpen("exportKey"); - } - createNotification({ type: "success", text: "Encryption strategy updated successfully" @@ -81,50 +78,6 @@ export const EncryptionPanel = ({ rootKmsDetails }: Props) => {
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 @@ -163,20 +116,11 @@ export const EncryptionPanel = ({ rootKmsDetails }: Props) => { Save - - handlePopUpToggle("exportKey", state)} - > - - - - handlePopUpToggle("restoreKey", state)} - > - - + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> ); }; diff --git a/frontend/src/views/admin/DashboardPage/components/ExportRootKmsKeyModalContent.tsx b/frontend/src/views/admin/DashboardPage/components/ExportRootKmsKeyModalContent.tsx deleted file mode 100644 index f59a8ac416..0000000000 --- a/frontend/src/views/admin/DashboardPage/components/ExportRootKmsKeyModalContent.tsx +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 8122c44e33..0000000000 --- a/frontend/src/views/admin/DashboardPage/components/RestoreRootKmsKeyModalContent.tsx +++ /dev/null @@ -1,98 +0,0 @@ -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"), []); - - return ( - - - -
- } - > -
-
- {keyParts.map((_, index) => ( - ( -
- - - -
- )} - /> - ))} - {errors.keyParts && ( -
{errors.keyParts.message}
- )} -
-
-
- ); -}; From 6dbe3c879305496dddffc746c2ca3bcdd094f695 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 3 Nov 2024 22:18:01 +0400 Subject: [PATCH 11/32] fix: removed exported field --- backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts | 4 ---- backend/src/db/schemas/kms-root-config.ts | 1 - backend/src/ee/services/license/license-fns.ts | 2 +- 3 files changed, 1 insertion(+), 6 deletions(-) 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 ff76d8eea5..a586f94b8b 100644 --- a/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts +++ b/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts @@ -4,12 +4,10 @@ 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); }); } @@ -17,11 +15,9 @@ 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 950f818c44..d15e1dff89 100644 --- a/backend/src/db/schemas/kms-root-config.ts +++ b/backend/src/db/schemas/kms-root-config.ts @@ -13,7 +13,6 @@ 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/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 8e86851a0f..70c2995641 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -29,7 +29,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ auditLogStreams: false, auditLogStreamLimit: 3, samlSSO: false, - hsm: true, + hsm: false, oidcSSO: false, scim: false, ldap: false, From 8d4a06e9e4ae96852d167e5e4fb2b29bd17afbd8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 3 Nov 2024 23:25:01 +0400 Subject: [PATCH 12/32] modified: src/lib/config/env.ts --- backend/src/lib/config/env.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 69a3d72ca4..7662bd8bec 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -1,8 +1,6 @@ import { Logger } from "pino"; import { z } from "zod"; -import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; - import { removeTrailingSlash } from "../fn"; import { zpStr } from "../zod"; @@ -167,9 +165,6 @@ const envSchema = z WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()), ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"), - // KMS ENCRYPTION - ROOT_KEY_ENCRYPTION_STRATEGY: z.nativeEnum(RootKeyEncryptionStrategy).default(RootKeyEncryptionStrategy.Basic), - // HSM HSM_LIB_PATH: zpStr( z @@ -209,8 +204,6 @@ const envSchema = z .transform((data) => ({ ...data, - // ROOT_KEY_ENCRYPTION_STRATEGY: "HSM", - DB_READ_REPLICAS: data.DB_READ_REPLICAS ? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS)) : undefined, From a1685af1196948bbf9c13faad2842a2fd71d80ec Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 4 Nov 2024 03:19:51 +0400 Subject: [PATCH 13/32] feat: hsm cryptographic tests --- backend/src/services/hsm/hsm-service.ts | 178 ++++++++++++++++-------- 1 file changed, 120 insertions(+), 58 deletions(-) diff --git a/backend/src/services/hsm/hsm-service.ts b/backend/src/services/hsm/hsm-service.ts index 5d870ce95c..8f9aa85568 100644 --- a/backend/src/services/hsm/hsm-service.ts +++ b/backend/src/services/hsm/hsm-service.ts @@ -13,6 +13,11 @@ const USER_ALREADY_LOGGED_IN_ERROR = "CKR_USER_ALREADY_LOGGED_IN"; export type THsmServiceFactory = ReturnType; +enum RequiredMechanisms { + AesGcm = "AES_GCM", + AesKeyWrap = "AES_KEY_WRAP" +} + class HsmSessionManager { private session: grapheneLib.Session | null = null; @@ -195,61 +200,7 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe } }; - const isActive = () => { - if (!module || !appCfg.isHsmConfigured || !sessionManager) { - return false; - } - - return appCfg.isHsmConfigured && module !== null; - }; - - const startService = () => { - if (!appCfg.isHsmConfigured || !module) return; - - sessionManager = new HsmSessionManager(module, graphene); - const session = sessionManager.getSession(); - - try { - // Check if master key exists, create if not - if (!$keyExists(session)) { - // Generate 256-bit AES master key with persistent storage - session.generateKey(graphene.KeyGenMechanism.AES, { - class: graphene.ObjectClass.SECRET_KEY, - token: true, - valueLen: 256 / 8, - keyType: graphene.KeyType.AES, - label: appCfg.HSM_KEY_LABEL, - derive: true, // Enable key derivation - extractable: false, - sensitive: true, - private: true - }); - logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); - } - - // Verify HSM supports required mechanisms - const mechs = session.slot.getMechanisms(); - let gotAesGcmMechanism = false; - - // eslint-disable-next-line no-plusplus - for (let i = 0; i < mechs.length; i++) { - const mech = mechs.items(i); - if (mech.name === "AES_GCM") { - gotAesGcmMechanism = true; - break; - } - } - - if (!gotAesGcmMechanism) { - throw new Error("HSM does not support AES_GCM mechanism"); - } - } catch (error) { - logger.error(error, "Error initializing HSM service"); - throw error; - } - }; - - function encrypt(data: Buffer): Buffer { + const encrypt = (data: Buffer) => { if (!module) { throw new Error("PKCS#11 module is not initialized"); } @@ -279,9 +230,9 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe // Format: [Wrapped Key (40)][IV (16)][Encrypted Data + Tag] return Buffer.concat([wrappedKey, iv, encryptedData]); - } + }; - function decrypt(encryptedBlob: Buffer): Buffer { + const decrypt = (encryptedBlob: Buffer) => { const WRAPPED_KEY_LENGTH = 32 + 8; // AES-256 key + padding if (!module || !sessionManager) { @@ -307,7 +258,118 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe const outputBuffer = Buffer.alloc(ciphertext.length); return decipher.once(ciphertext, outputBuffer); - } + }; + + // We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device. + const $testPkcs11Module = () => { + try { + if (!module || !sessionManager) { + throw new Error("HSM service not initialized"); + } + + const session = sessionManager.getSession(); + + let randomData: Buffer; + let encryptedData: Buffer; + let decryptedData: Buffer; + + try { + randomData = session.generateRandom(256); + } catch (error) { + throw new Error(`Error generating random bytes: ${(error as Error).message || "Unknown error"}`); + } + + try { + encryptedData = encrypt(Buffer.from(randomData)); + } catch (error) { + throw new Error(`Error encrypting data: ${(error as Error).message || "Unknown error"}`); + } + + try { + decryptedData = decrypt(encryptedData); + } catch (error) { + throw new Error(`Error decrypting data: ${(error as Error).message || "Unknown error"}`); + } + + if (Buffer.from(randomData).toString("hex") !== Buffer.from(decryptedData).toString("hex")) { + throw new Error("Decrypted data does not match original data"); + } + return true; + } catch (error) { + logger.error(error, "Error testing PKCS#11 module"); + return false; + } + }; + + const isActive = () => { + if (!module || !appCfg.isHsmConfigured || !sessionManager) { + return false; + } + + let pkcs11TestPassed = false; + + try { + pkcs11TestPassed = $testPkcs11Module(); + } catch (err) { + logger.error(err, "isActive: Error testing PKCS#11 module"); + } + + return appCfg.isHsmConfigured && module !== null && pkcs11TestPassed; + }; + + const startService = () => { + if (!appCfg.isHsmConfigured || !module) return; + + sessionManager = new HsmSessionManager(module, graphene); + const session = sessionManager.getSession(); + + try { + // Check if master key exists, create if not + if (!$keyExists(session)) { + // Generate 256-bit AES master key with persistent storage + session.generateKey(graphene.KeyGenMechanism.AES, { + class: graphene.ObjectClass.SECRET_KEY, + token: true, + valueLen: 256 / 8, + keyType: graphene.KeyType.AES, + label: appCfg.HSM_KEY_LABEL, + derive: true, // Enable key derivation + extractable: false, + sensitive: true, + private: true + }); + logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); + } + + // Verify HSM supports required mechanisms + const mechs = session.slot.getMechanisms(); + const mechNames: string[] = []; + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < mechs.length; i++) { + mechNames.push(mechs.items(i).name); + } + + const hasAesGcm = mechNames.includes(RequiredMechanisms.AesGcm); + const hasAesKeyWrap = mechNames.includes(RequiredMechanisms.AesKeyWrap); + + if (!hasAesGcm) { + throw new Error(`Required mechanism ${RequiredMechanisms.AesGcm} not supported by HSM`); + } + if (!hasAesKeyWrap) { + throw new Error(`Required mechanism ${RequiredMechanisms.AesKeyWrap} not supported by HSM`); + } + + // Run a test to verify module is working + if (!$testPkcs11Module()) { + throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured."); + } + } catch (error) { + logger.error(error, "Error initializing HSM service"); + throw error; + } + }; + return { encrypt, startService, From d0b3c6b66a36d16ed2bc37903a270ad0a20786a4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 4 Nov 2024 03:22:43 +0400 Subject: [PATCH 14/32] Create docker-compose.hsm.prod.yml --- docker-compose.hsm.prod.yml | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docker-compose.hsm.prod.yml diff --git a/docker-compose.hsm.prod.yml b/docker-compose.hsm.prod.yml new file mode 100644 index 0000000000..1c3a72ece8 --- /dev/null +++ b/docker-compose.hsm.prod.yml @@ -0,0 +1,74 @@ +version: "3" + +services: + db-migration: + container_name: infisical-db-migration + depends_on: + db: + condition: service_healthy + image: infisical/infisical:latest-postgres + env_file: .env + command: npm run migration:latest + pull_policy: always + networks: + - infisical + + backend: + container_name: infisical-backend + restart: unless-stopped + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + db-migration: + condition: service_completed_successfully + image: infisical/infisical:latest-postgres + pull_policy: always + env_file: .env + ports: + - 80:8080 + environment: + - NODE_ENV=production + volumes: + - /opt/infisical-hsm:/opt/infisical-hsm + networks: + - infisical + + redis: + image: redis + container_name: infisical-dev-redis + env_file: .env + restart: always + environment: + - ALLOW_EMPTY_PASSWORD=yes + ports: + - 6379:6379 + networks: + - infisical + volumes: + - redis_data:/data + + db: + container_name: infisical-db + image: postgres:14-alpine + restart: always + env_file: .env + volumes: + - pg_data:/var/lib/postgresql/data + networks: + - infisical + healthcheck: + test: "pg_isready --username=${POSTGRES_USER} && psql --username=${POSTGRES_USER} --list" + interval: 5s + timeout: 10s + retries: 10 + +volumes: + pg_data: + driver: local + redis_data: + driver: local + +networks: + infisical: From 8d4115925c1af621b563ab0fc422f9489a5846e6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 4 Nov 2024 19:00:24 +0400 Subject: [PATCH 15/32] requested changes --- backend/e2e-test/vitest-environment-knex.ts | 8 ++++---- .../20241028134337_kms-root-cfg-hsm.ts | 2 +- backend/src/lib/config/env.ts | 2 +- backend/src/main.ts | 12 ++++++------ backend/src/server/app.ts | 2 +- backend/src/server/routes/index.ts | 4 ++-- backend/src/services/hsm/hsm-fns.ts | 7 ++----- backend/src/services/hsm/hsm-service.ts | 11 +++-------- backend/src/services/hsm/hsm-types.ts | 11 +++++++++++ backend/src/services/kms/kms-service.ts | 18 ++++++++---------- backend/src/services/kms/kms-types.ts | 4 ++-- .../super-admin/super-admin-service.ts | 6 +++--- frontend/src/hooks/api/admin/types.ts | 4 ++-- frontend/src/hooks/useFileDownload.tsx | 13 ------------- .../admin/DashboardPage/EncryptionPanel.tsx | 2 +- 15 files changed, 47 insertions(+), 59 deletions(-) create mode 100644 backend/src/services/hsm/hsm-types.ts delete mode 100644 frontend/src/hooks/useFileDownload.tsx diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index d44fc0729b..ce83016907 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -16,7 +16,7 @@ import { initDbConnection } from "@app/db"; import { queueServiceFactory } from "@app/queue"; import { keyStoreFactory } from "@app/keystore/keystore"; import { Redis } from "ioredis"; -import { initializePkcs11Module } from "@app/services/hsm/hsm-fns"; +import { initializeHsmModule } from "@app/services/hsm/hsm-fns"; dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); export default { @@ -56,10 +56,10 @@ export default { const queue = queueServiceFactory(cfg.REDIS_URL); const keyStore = keyStoreFactory(cfg.REDIS_URL); - const pkcs11Module = initializePkcs11Module(); - pkcs11Module.initialize(); + const hsmModule = initializeHsmModule(); + hsmModule.initialize(); - const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: pkcs11Module.getModule() }); + const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: hsmModule.getModule() }); // @ts-expect-error type globalThis.testServer = server; 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..501eccb8b8 100644 --- a/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts +++ b/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts @@ -7,7 +7,7 @@ export async function up(knex: Knex): Promise { const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt"); await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => { - if (!hasEncryptionStrategy) t.string("encryptionStrategy").defaultTo("BASIC"); + if (!hasEncryptionStrategy) t.string("encryptionStrategy").defaultTo("SOFTWARE"); if (!hasTimestampsCol) t.timestamps(true, true, true); }); } diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 7662bd8bec..cb41ebd726 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -198,7 +198,7 @@ const envSchema = z }) // To ensure that basic encryption is always possible. .refine( - (data) => data.ENCRYPTION_KEY != null || data.ROOT_ENCRYPTION_KEY != null, + (data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY), "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." ) .transform((data) => ({ diff --git a/backend/src/main.ts b/backend/src/main.ts index f0ed3fddbd..db67e7a596 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -9,7 +9,7 @@ import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; import { bootstrapCheck } from "./server/boot-strap-check"; -import { initializePkcs11Module } from "./services/hsm/hsm-fns"; +import { initializeHsmModule } from "./services/hsm/hsm-fns"; import { smtpServiceFactory } from "./services/smtp/smtp-service"; dotenv.config(); @@ -54,17 +54,17 @@ const run = async () => { const queue = queueServiceFactory(appCfg.REDIS_URL); const keyStore = keyStoreFactory(appCfg.REDIS_URL); - const pkcs11Module = initializePkcs11Module(); - pkcs11Module.initialize(); + const hsmModule = initializeHsmModule(); + hsmModule.initialize(); - const server = await main({ db, auditLogDb, hsmModule: pkcs11Module.getModule(), smtp, logger, queue, keyStore }); + const server = await main({ db, auditLogDb, hsmModule: hsmModule.getModule(), smtp, logger, queue, keyStore }); const bootstrap = await bootstrapCheck({ db }); // eslint-disable-next-line process.on("SIGINT", async () => { await server.close(); await db.destroy(); - pkcs11Module.finalize(); + hsmModule.finalize(); process.exit(0); }); @@ -72,7 +72,7 @@ const run = async () => { process.on("SIGTERM", async () => { await server.close(); await db.destroy(); - pkcs11Module.finalize(); + hsmModule.finalize(); process.exit(0); }); diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 03f3f98f95..4d002f3f56 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -17,7 +17,7 @@ import { Logger } from "pino"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; -import { HsmModule } from "@app/services/hsm/hsm-fns"; +import { HsmModule } from "@app/services/hsm/hsm-types"; import { TSmtpService } from "@app/services/smtp/smtp-service"; import { globalRateLimiterCfg } from "./config/rateLimiter"; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 907ef66e71..4c7aee7ffe 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -107,8 +107,8 @@ import { externalMigrationServiceFactory } from "@app/services/external-migratio import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service"; -import { HsmModule } from "@app/services/hsm/hsm-fns"; import { hsmServiceFactory } from "@app/services/hsm/hsm-service"; +import { HsmModule } from "@app/services/hsm/hsm-types"; import { identityDALFactory } from "@app/services/identity/identity-dal"; import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal"; @@ -363,7 +363,7 @@ export const registerRoutes = async ( const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); const hsmService = hsmServiceFactory({ - pkcs11Module: hsmModule + hsmModule }); const kmsService = kmsServiceFactory({ diff --git a/backend/src/services/hsm/hsm-fns.ts b/backend/src/services/hsm/hsm-fns.ts index 83b05568ea..746948105b 100644 --- a/backend/src/services/hsm/hsm-fns.ts +++ b/backend/src/services/hsm/hsm-fns.ts @@ -3,12 +3,9 @@ import * as grapheneLib from "graphene-pk11"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; -export type HsmModule = { - module: grapheneLib.Module | null; - graphene: typeof grapheneLib; -}; +import { HsmModule } from "./hsm-types"; -export const initializePkcs11Module = () => { +export const initializeHsmModule = () => { const appCfg = getConfig(); let module: grapheneLib.Module | null = null; diff --git a/backend/src/services/hsm/hsm-service.ts b/backend/src/services/hsm/hsm-service.ts index 8f9aa85568..2208a72953 100644 --- a/backend/src/services/hsm/hsm-service.ts +++ b/backend/src/services/hsm/hsm-service.ts @@ -3,21 +3,16 @@ import grapheneLib from "graphene-pk11"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; -import { HsmModule } from "./hsm-fns"; +import { HsmModule, RequiredMechanisms } from "./hsm-types"; type THsmServiceFactoryDep = { - pkcs11Module: HsmModule; + hsmModule: HsmModule; }; const SESSION_TIMEOUT = 5 * 60 * 1000; // 5 minutes const USER_ALREADY_LOGGED_IN_ERROR = "CKR_USER_ALREADY_LOGGED_IN"; export type THsmServiceFactory = ReturnType; -enum RequiredMechanisms { - AesGcm = "AES_GCM", - AesKeyWrap = "AES_KEY_WRAP" -} - class HsmSessionManager { private session: grapheneLib.Session | null = null; @@ -114,7 +109,7 @@ class HsmSessionManager { } // eslint-disable-next-line no-empty-pattern -export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmServiceFactoryDep) => { +export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServiceFactoryDep) => { const appCfg = getConfig(); // Constants for buffer structure diff --git a/backend/src/services/hsm/hsm-types.ts b/backend/src/services/hsm/hsm-types.ts new file mode 100644 index 0000000000..e7c33ffdab --- /dev/null +++ b/backend/src/services/hsm/hsm-types.ts @@ -0,0 +1,11 @@ +import * as grapheneLib from "graphene-pk11"; + +export type HsmModule = { + module: grapheneLib.Module | null; + graphene: typeof grapheneLib; +}; + +export enum RequiredMechanisms { + AesGcm = "AES_GCM", + AesKeyWrap = "AES_KEY_WRAP" +} diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index df6f4692b0..102333038a 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -629,7 +629,7 @@ export const kmsServiceFactory = ({ const $decryptRootKey = async (kmsRootConfig: TKmsRootConfig) => { // case 1: root key is encrypted with HSM - if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Hsm) { + if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.HSM) { if (!hsmService.isActive()) { throw new Error("Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); } @@ -637,8 +637,8 @@ export const kmsServiceFactory = ({ return hsmService.decrypt(kmsRootConfig.encryptedRootKey); } - // case 2: root key is encrypted with basic encryption - if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Basic) { + // case 2: root key is encrypted with software encryption + if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) { const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const encryptionKeyBuffer = $getBasicEncryptionKey(); @@ -649,14 +649,14 @@ export const kmsServiceFactory = ({ }; const $encryptRootKey = async (plainKeyBuffer: Buffer, strategy: RootKeyEncryptionStrategy) => { - if (strategy === RootKeyEncryptionStrategy.Hsm) { + if (strategy === RootKeyEncryptionStrategy.HSM) { if (!hsmService.isActive()) { throw new Error("Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); } return hsmService.encrypt(plainKeyBuffer); } - if (strategy === RootKeyEncryptionStrategy.Basic) { + if (strategy === RootKeyEncryptionStrategy.Software) { const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const encryptionKeyBuffer = $getBasicEncryptionKey(); @@ -870,9 +870,7 @@ export const kmsServiceFactory = ({ const decryptedRootKey = await $decryptRootKey(kmsRootConfig).catch((err) => { logger.error(err, `KMS: Failed to decrypt ROOT Key [strategy=${kmsRootConfig.encryptionStrategy}]`); - // 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); + throw err; }); // set the flag so that other instance nodes can start @@ -885,7 +883,7 @@ export const kmsServiceFactory = ({ // 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, RootKeyEncryptionStrategy.Basic).catch((err) => { + const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => { logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key"); throw err; }); @@ -894,7 +892,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: RootKeyEncryptionStrategy.Basic + encryptionStrategy: RootKeyEncryptionStrategy.Software }); // set the flag so that other instance nodes can start diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index e1aa7b5e0a..f655d4b5d1 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -58,6 +58,6 @@ export type TUpdateProjectSecretManagerKmsKeyDTO = { }; export enum RootKeyEncryptionStrategy { - Basic = "BASIC", - Hsm = "HSM" + Software = "SOFTWARE", + HSM = "HSM" } diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 97f314db80..370890a09d 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -306,16 +306,16 @@ export const superAdminServiceFactory = ({ const enabledStrategies: { enabled: boolean; strategy: RootKeyEncryptionStrategy; name: string }[] = []; if (appCfg.ROOT_ENCRYPTION_KEY || appCfg.ENCRYPTION_KEY) { - const basicStrategy = RootKeyEncryptionStrategy.Basic; + const basicStrategy = RootKeyEncryptionStrategy.Software; enabledStrategies.push({ - name: "Regular Encryption", + name: "Software-based Encryption", enabled: selectedStrategy === basicStrategy, strategy: basicStrategy }); } if (appCfg.isHsmConfigured) { - const hsmStrategy = RootKeyEncryptionStrategy.Hsm; + const hsmStrategy = RootKeyEncryptionStrategy.HSM; enabledStrategies.push({ name: "Hardware Security Module (HSM)", diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 0fa6d23ef1..7d35f70bed 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -64,6 +64,6 @@ export type TGetServerRootKmsEncryptionDetails = { }; export enum RootKeyEncryptionStrategy { - Basic = "BASIC", - Hsm = "HSM" + Software = "SOFTWARE", + HSM = "HSM" } diff --git a/frontend/src/hooks/useFileDownload.tsx b/frontend/src/hooks/useFileDownload.tsx deleted file mode 100644 index 6cf833cde4..0000000000 --- a/frontend/src/hooks/useFileDownload.tsx +++ /dev/null @@ -1,13 +0,0 @@ -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/EncryptionPanel.tsx b/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx index ad32df9982..8be1982d83 100644 --- a/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx +++ b/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx @@ -38,7 +38,7 @@ export const EncryptionPanel = ({ rootKmsDetails }: Props) => { values: { encryptionStrategy: rootKmsDetails?.strategies?.find((s) => s.enabled)?.strategy ?? - RootKeyEncryptionStrategy.Basic + RootKeyEncryptionStrategy.Software } }); From 0fda6d6f4d19c774f43672a6b963e7d52f8cbdf8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 4 Nov 2024 19:38:05 +0400 Subject: [PATCH 16/32] requested changes --- backend/Dockerfile | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 6948445472..5822649461 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -3,13 +3,8 @@ FROM node:20-alpine AS build WORKDIR /app +# Required for pkcs11js RUN apk --update add \ - alpine-sdk \ - autoconf \ - automake \ - git \ - libtool \ - openssl-dev \ python3 \ make \ g++ @@ -29,12 +24,6 @@ ENV npm_config_cache /home/node/.npm COPY package*.json ./ RUN apk --update add \ - alpine-sdk \ - autoconf \ - automake \ - git \ - libtool \ - openssl-dev \ python3 \ make \ g++ From 1cf046f6b3ecdf6724b242942256f73251d886f1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 4 Nov 2024 19:42:08 +0400 Subject: [PATCH 17/32] Update super-admin-service.ts --- backend/src/services/super-admin/super-admin-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 370890a09d..32455a6fc5 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -292,7 +292,7 @@ export const superAdminServiceFactory = ({ clientSecret }; }; - + // const getConfiguredEncryptionStrategies = async () => { const appCfg = getConfig(); From abdf8f46a35b182b38ec7ced45654c62e3734d7c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 4 Nov 2024 19:42:32 +0400 Subject: [PATCH 18/32] Update super-admin-service.ts --- backend/src/services/super-admin/super-admin-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 32455a6fc5..370890a09d 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -292,7 +292,7 @@ export const superAdminServiceFactory = ({ clientSecret }; }; - // + const getConfiguredEncryptionStrategies = async () => { const appCfg = getConfig(); From 5e068cd8a09af1347eda089b9ab0942f74c0ac06 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 5 Nov 2024 01:04:45 +0400 Subject: [PATCH 19/32] feat: wait for session wrapper --- backend/src/server/routes/index.ts | 6 +- backend/src/services/hsm/hsm-service.ts | 347 +++++++++++------------- backend/src/services/kms/kms-service.ts | 20 +- 3 files changed, 175 insertions(+), 198 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4c7aee7ffe..0ef53e14e8 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1278,11 +1278,13 @@ export const registerRoutes = async ( }); await superAdminService.initServerCfg(); - // + // setup the communication with license key server await licenseService.init(); - hsmService.startService(); + // Start HSM service if it's configured/enabled. + await hsmService.startService(); + await telemetryQueue.startTelemetryCheck(); await dailyResourceCleanUp.startCleanUp(); await dailyExpiringPkiItemAlert.startSendingAlerts(); diff --git a/backend/src/services/hsm/hsm-service.ts b/backend/src/services/hsm/hsm-service.ts index 2208a72953..a2dd14bc74 100644 --- a/backend/src/services/hsm/hsm-service.ts +++ b/backend/src/services/hsm/hsm-service.ts @@ -8,105 +8,80 @@ import { HsmModule, RequiredMechanisms } from "./hsm-types"; type THsmServiceFactoryDep = { hsmModule: HsmModule; }; -const SESSION_TIMEOUT = 5 * 60 * 1000; // 5 minutes + const USER_ALREADY_LOGGED_IN_ERROR = "CKR_USER_ALREADY_LOGGED_IN"; +const WRAPPED_KEY_LENGTH = 32 + 8; // AES-256 key + padding export type THsmServiceFactory = ReturnType; -class HsmSessionManager { - private session: grapheneLib.Session | null = null; - - private lastUsed: number = 0; - - private module: grapheneLib.Module; - - private graphene: typeof grapheneLib; - - private sessionCheckInterval: NodeJS.Timeout | null = null; +type SyncOrAsync = T | Promise; +type SessionCallback = (session: grapheneLib.Session) => SyncOrAsync; - private startSessionMonitoring() { - // Check session health every minute - this.sessionCheckInterval = setInterval(() => { - this.checkAndRefreshSession(); - }, 60 * 1000); // 1 minute - } +export const withSession = async ( + { module, graphene }: HsmModule, + callbackWithSession: SessionCallback +): Promise => { + const appCfg = getConfig(); - private checkAndRefreshSession() { - if (!this.session) return; + let session: grapheneLib.Session | null = null; + try { + if (!module) { + throw new Error("PKCS#11 module is not initialized"); + } - const now = Date.now(); - if (now - this.lastUsed > SESSION_TIMEOUT) { - logger.info("Session expired, cleaning up..."); - this.cleanup(); + // Create new session + const slot = module.getSlots(appCfg.HSM_SLOT); + // eslint-disable-next-line no-bitwise + if (!(slot.flags & graphene.SlotFlag.TOKEN_PRESENT)) { + throw new Error("Slot is not initialized"); } - } - private cleanup() { - if (this.session) { + for (let i = 0; i < 10; i += 1) { try { - this.session.logout(); - this.session.close(); + // eslint-disable-next-line no-bitwise + session = slot.open(graphene.SessionFlag.RW_SESSION | graphene.SessionFlag.SERIAL_SESSION); + session.login(appCfg.HSM_PIN!); } catch (error) { - logger.error("Error during session cleanup:", error); + if ((error as Error)?.message !== USER_ALREADY_LOGGED_IN_ERROR) { + throw error; + } + logger.warn("HSM session already logged in"); + session = null; } - this.session = null; - } - if (this.sessionCheckInterval) { - clearInterval(this.sessionCheckInterval); - this.sessionCheckInterval = null; - } - } + if (session) { + break; + } - getSession(): grapheneLib.Session { - const appCfg = getConfig(); + logger.warn("Waiting for session to be available..."); + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + let sleepAmount = 1_500 * (i + 1); + if (sleepAmount > 5000) sleepAmount = 5000; - // If we have a valid session, update its last used time and return it - if (this.session) { - try { - // Try a simple operation to verify session is still valid - this.session.generateRandom(16); - this.lastUsed = Date.now(); - return this.session; - } catch (error) { - logger.info("HSM Session validation failed, creating new session..."); - this.cleanup(); - } + setTimeout(resolve, sleepAmount); + }); } - // Create new session - const slot = this.module.getSlots(appCfg.HSM_SLOT); - // eslint-disable-next-line no-bitwise - if (!(slot.flags & this.graphene.SlotFlag.TOKEN_PRESENT)) { - throw new Error("Slot is not initialized"); + if (!session) { + throw new Error("Failed to open session"); } - // eslint-disable-next-line no-bitwise - const session = slot.open(this.graphene.SessionFlag.RW_SESSION | this.graphene.SessionFlag.SERIAL_SESSION); - - try { - session.login(appCfg.HSM_PIN!); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- The error is of type `Pkcs11Error`, but this error is not exported by graphene. And we don't want to install another library just for an error assertion. - if (error.message !== USER_ALREADY_LOGGED_IN_ERROR) { - throw error; + // Execute the callback and await its result (works for both sync and async) + const result = await callbackWithSession(session); + return result; + } finally { + // Clean up session if it was created + if (session) { + try { + session.logout(); + session.close(); + } catch (error) { + logger.error("Error cleaning up HSM session:", error); } } - - this.session = session; - this.lastUsed = Date.now(); - - return session; - } - - constructor(module: grapheneLib.Module, graphene: typeof grapheneLib) { - this.module = module; - this.graphene = graphene; - - this.startSessionMonitoring(); } -} +}; // eslint-disable-next-line no-empty-pattern export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServiceFactoryDep) => { @@ -116,8 +91,6 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServi const IV_LENGTH = 16; const TAG_LENGTH = 16; - let sessionManager: HsmSessionManager | null = null; - const $findMasterKey = (session: grapheneLib.Session) => { // Find the master key (root key) const template = { @@ -195,100 +168,101 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServi } }; - const encrypt = (data: Buffer) => { + const encrypt: { + (data: Buffer, providedSession: grapheneLib.Session): Promise; + (data: Buffer): Promise; + } = async (data: Buffer, providedSession?: grapheneLib.Session) => { if (!module) { throw new Error("PKCS#11 module is not initialized"); } - if (!sessionManager) { - throw new Error("HSM Session manager is not initialized"); - } - const session = sessionManager.getSession(); + const $performEncryption = (s: grapheneLib.Session) => { + // Generate IV for encryption + const iv = s.generateRandom(IV_LENGTH); - // Generate IV for encryption - const iv = session.generateRandom(IV_LENGTH); + // Generate and wrap a new session key + const { wrappedKey, sessionKey } = $generateAndWrapKey(s); - // Generate and wrap a new session key - const { wrappedKey, sessionKey } = $generateAndWrapKey(session); + const alg = { + name: appCfg.HSM_MECHANISM, + params: new graphene.AesGcm240Params(iv) + } as grapheneLib.IAlgorithm; - const alg = { - name: appCfg.HSM_MECHANISM, - params: new graphene.AesGcm240Params(iv) - } as grapheneLib.IAlgorithm; + const cipher = s.createCipher(alg, new graphene.Key(sessionKey).toType()); - const cipher = session.createCipher(alg, new graphene.Key(sessionKey).toType()); + // Calculate the output buffer size based on input length + // GCM adds a 16-byte auth tag, so we need input length + 16 + const outputBuffer = Buffer.alloc(data.length + TAG_LENGTH); + const encryptedData = cipher.once(data, outputBuffer); - // Calculate the output buffer size based on input length - // GCM adds a 16-byte auth tag, so we need input length + 16 - const outputBuffer = Buffer.alloc(data.length + TAG_LENGTH); - const encryptedData = cipher.once(data, outputBuffer); + // Format: [Wrapped Key (40)][IV (16)][Encrypted Data + Tag] + return Buffer.concat([wrappedKey, iv, encryptedData]); + }; - // Format: [Wrapped Key (40)][IV (16)][Encrypted Data + Tag] - return Buffer.concat([wrappedKey, iv, encryptedData]); - }; + if (providedSession) { + return $performEncryption(providedSession); + } + + const encrypted = await withSession({ module, graphene }, $performEncryption); - const decrypt = (encryptedBlob: Buffer) => { - const WRAPPED_KEY_LENGTH = 32 + 8; // AES-256 key + padding + return encrypted; + }; - if (!module || !sessionManager) { + const decrypt: { + (encryptedBlob: Buffer, providedSession: grapheneLib.Session): Promise; + (encryptedBlob: Buffer): Promise; + } = async (encryptedBlob: Buffer, providedSession?: grapheneLib.Session) => { + if (!module) { throw new Error("HSM service not initialized"); } - const session = sessionManager.getSession(); + const $performDecryption = (s: grapheneLib.Session) => { + const wrappedKey = encryptedBlob.subarray(0, WRAPPED_KEY_LENGTH); + const iv = encryptedBlob.subarray(WRAPPED_KEY_LENGTH, WRAPPED_KEY_LENGTH + IV_LENGTH); + const ciphertext = encryptedBlob.subarray(WRAPPED_KEY_LENGTH + IV_LENGTH); - // Extract wrapped key, IV, and ciphertext - const wrappedKey = encryptedBlob.subarray(0, WRAPPED_KEY_LENGTH); - const iv = encryptedBlob.subarray(WRAPPED_KEY_LENGTH, WRAPPED_KEY_LENGTH + IV_LENGTH); - const ciphertext = encryptedBlob.subarray(WRAPPED_KEY_LENGTH + IV_LENGTH); + // Unwrap the session key + const sessionKey = $unwrapKey(s, wrappedKey); - // Unwrap the session key - const sessionKey = $unwrapKey(session, wrappedKey); + const algo = { + name: appCfg.HSM_MECHANISM, + params: new graphene.AesGcm240Params(iv) + }; - const algo = { - name: appCfg.HSM_MECHANISM, - params: new graphene.AesGcm240Params(iv) + const decipher = s.createDecipher(algo, new graphene.Key(sessionKey).toType()); + const outputBuffer = Buffer.alloc(ciphertext.length); + + // Extract wrapped key, IV, and ciphertext + return decipher.once(ciphertext, outputBuffer); }; - const decipher = session.createDecipher(algo, new graphene.Key(sessionKey).toType()); - const outputBuffer = Buffer.alloc(ciphertext.length); + if (providedSession) { + return $performDecryption(providedSession); + } + const decrypted = await withSession({ module, graphene }, (newSession) => $performDecryption(newSession)); - return decipher.once(ciphertext, outputBuffer); + return decrypted; }; // We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device. - const $testPkcs11Module = () => { + const $testPkcs11Module = async (session: grapheneLib.Session) => { try { - if (!module || !sessionManager) { + if (!module) { throw new Error("HSM service not initialized"); } - const session = sessionManager.getSession(); - - let randomData: Buffer; - let encryptedData: Buffer; - let decryptedData: Buffer; - - try { - randomData = session.generateRandom(256); - } catch (error) { - throw new Error(`Error generating random bytes: ${(error as Error).message || "Unknown error"}`); - } - - try { - encryptedData = encrypt(Buffer.from(randomData)); - } catch (error) { - throw new Error(`Error encrypting data: ${(error as Error).message || "Unknown error"}`); + if (!session) { + throw new Error("Session not initialized"); } - try { - decryptedData = decrypt(encryptedData); - } catch (error) { - throw new Error(`Error decrypting data: ${(error as Error).message || "Unknown error"}`); - } + const randomData = session.generateRandom(256); + const encryptedData = await encrypt(Buffer.from(randomData), session); + const decryptedData = await decrypt(encryptedData, session); if (Buffer.from(randomData).toString("hex") !== Buffer.from(decryptedData).toString("hex")) { throw new Error("Decrypted data does not match original data"); } + return true; } catch (error) { logger.error(error, "Error testing PKCS#11 module"); @@ -296,15 +270,15 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServi } }; - const isActive = () => { - if (!module || !appCfg.isHsmConfigured || !sessionManager) { + const isActive = async () => { + if (!module || !appCfg.isHsmConfigured) { return false; } let pkcs11TestPassed = false; try { - pkcs11TestPassed = $testPkcs11Module(); + pkcs11TestPassed = await withSession({ module, graphene }, $testPkcs11Module); } catch (err) { logger.error(err, "isActive: Error testing PKCS#11 module"); } @@ -312,53 +286,54 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServi return appCfg.isHsmConfigured && module !== null && pkcs11TestPassed; }; - const startService = () => { + const startService = async () => { if (!appCfg.isHsmConfigured || !module) return; - sessionManager = new HsmSessionManager(module, graphene); - const session = sessionManager.getSession(); - try { - // Check if master key exists, create if not - if (!$keyExists(session)) { - // Generate 256-bit AES master key with persistent storage - session.generateKey(graphene.KeyGenMechanism.AES, { - class: graphene.ObjectClass.SECRET_KEY, - token: true, - valueLen: 256 / 8, - keyType: graphene.KeyType.AES, - label: appCfg.HSM_KEY_LABEL, - derive: true, // Enable key derivation - extractable: false, - sensitive: true, - private: true - }); - logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); - } - - // Verify HSM supports required mechanisms - const mechs = session.slot.getMechanisms(); - const mechNames: string[] = []; - - // eslint-disable-next-line no-plusplus - for (let i = 0; i < mechs.length; i++) { - mechNames.push(mechs.items(i).name); - } - - const hasAesGcm = mechNames.includes(RequiredMechanisms.AesGcm); - const hasAesKeyWrap = mechNames.includes(RequiredMechanisms.AesKeyWrap); - - if (!hasAesGcm) { - throw new Error(`Required mechanism ${RequiredMechanisms.AesGcm} not supported by HSM`); - } - if (!hasAesKeyWrap) { - throw new Error(`Required mechanism ${RequiredMechanisms.AesKeyWrap} not supported by HSM`); - } - - // Run a test to verify module is working - if (!$testPkcs11Module()) { - throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured."); - } + await withSession({ module, graphene }, async (session) => { + // Check if master key exists, create if not + if (!$keyExists(session)) { + // Generate 256-bit AES master key with persistent storage + session.generateKey(graphene.KeyGenMechanism.AES, { + class: graphene.ObjectClass.SECRET_KEY, + token: true, + valueLen: 256 / 8, + keyType: graphene.KeyType.AES, + label: appCfg.HSM_KEY_LABEL, + derive: true, // Enable key derivation + extractable: false, + sensitive: true, + private: true + }); + logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); + } + + // Verify HSM supports required mechanisms + const mechs = session.slot.getMechanisms(); + const mechNames: string[] = []; + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < mechs.length; i++) { + mechNames.push(mechs.items(i).name); + } + + const hasAesGcm = mechNames.includes(RequiredMechanisms.AesGcm); + const hasAesKeyWrap = mechNames.includes(RequiredMechanisms.AesKeyWrap); + + if (!hasAesGcm) { + throw new Error(`Required mechanism ${RequiredMechanisms.AesGcm} not supported by HSM`); + } + if (!hasAesKeyWrap) { + throw new Error(`Required mechanism ${RequiredMechanisms.AesKeyWrap} not supported by HSM`); + } + + const testPassed = await $testPkcs11Module(session); + + // Run a test to verify module is working + if (!testPassed) { + throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured."); + } + }); } catch (error) { logger.error(error, "Error initializing HSM service"); throw error; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 102333038a..92b909870f 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -630,11 +630,13 @@ export const kmsServiceFactory = ({ const $decryptRootKey = async (kmsRootConfig: TKmsRootConfig) => { // case 1: root key is encrypted with HSM if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.HSM) { - if (!hsmService.isActive()) { + const hsmIsActive = await hsmService.isActive(); + if (!hsmIsActive) { throw new Error("Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); } - return hsmService.decrypt(kmsRootConfig.encryptedRootKey); + const decryptedKey = await hsmService.decrypt(kmsRootConfig.encryptedRootKey); + return decryptedKey; } // case 2: root key is encrypted with software encryption @@ -650,10 +652,12 @@ export const kmsServiceFactory = ({ const $encryptRootKey = async (plainKeyBuffer: Buffer, strategy: RootKeyEncryptionStrategy) => { if (strategy === RootKeyEncryptionStrategy.HSM) { - if (!hsmService.isActive()) { + const hsmIsActive = await hsmService.isActive(); + if (!hsmIsActive) { throw new Error("Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?"); } - return hsmService.encrypt(plainKeyBuffer); + const encrypted = await hsmService.encrypt(plainKeyBuffer); + return encrypted; } if (strategy === RootKeyEncryptionStrategy.Software) { @@ -828,7 +832,6 @@ export const kmsServiceFactory = ({ }, tx ); - return kmsDAL.findByIdWithAssociatedKms(key.id, tx); }); @@ -866,12 +869,9 @@ export const kmsServiceFactory = ({ // case 1: a root key already exists in the DB if (kmsRootConfig) { if (lock) await lock.release(); - logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting."); + logger.info(`KMS: Encrypted ROOT Key found from DB. Decrypting. [strategy=${kmsRootConfig.encryptionStrategy}]`); - const decryptedRootKey = await $decryptRootKey(kmsRootConfig).catch((err) => { - logger.error(err, `KMS: Failed to decrypt ROOT Key [strategy=${kmsRootConfig.encryptionStrategy}]`); - throw err; - }); + const decryptedRootKey = await $decryptRootKey(kmsRootConfig); // set the flag so that other instance nodes can start await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true"); From 00e68dc0bf0b914e1768417715b63c49cc09d0fb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 5 Nov 2024 06:17:49 +0400 Subject: [PATCH 20/32] Update hsm-fns.ts --- backend/src/services/hsm/hsm-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/hsm/hsm-fns.ts b/backend/src/services/hsm/hsm-fns.ts index 746948105b..59a50f51f7 100644 --- a/backend/src/services/hsm/hsm-fns.ts +++ b/backend/src/services/hsm/hsm-fns.ts @@ -15,7 +15,7 @@ export const initializeHsmModule = () => { return; } - module = grapheneLib.Module.load(appCfg.HSM_LIB_PATH!, "SoftHSM"); + module = grapheneLib.Module.load(appCfg.HSM_LIB_PATH!, "InfisicalHSM"); module.initialize(); logger.info("PKCS#11 module initialized"); }; From 21024b0d723d512bcb0bed5aad8a5c0e4fa6f169 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 5 Nov 2024 19:25:44 +0400 Subject: [PATCH 21/32] requested changes --- backend/e2e-test/vitest-environment-knex.ts | 2 +- backend/src/{ => ee}/services/hsm/hsm-fns.ts | 0 backend/src/{ => ee}/services/hsm/hsm-service.ts | 2 +- backend/src/{ => ee}/services/hsm/hsm-types.ts | 0 backend/src/main.ts | 3 ++- backend/src/server/app.ts | 2 +- backend/src/server/routes/index.ts | 4 ++-- backend/src/server/routes/v1/admin-router.ts | 4 ++-- backend/src/services/kms/kms-service.ts | 2 +- frontend/src/hooks/api/admin/mutation.ts | 2 +- frontend/src/hooks/api/admin/queries.ts | 2 +- 11 files changed, 12 insertions(+), 11 deletions(-) rename backend/src/{ => ee}/services/hsm/hsm-fns.ts (100%) rename backend/src/{ => ee}/services/hsm/hsm-service.ts (99%) rename backend/src/{ => ee}/services/hsm/hsm-types.ts (100%) diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index ce83016907..866b0f45ff 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -16,7 +16,7 @@ import { initDbConnection } from "@app/db"; import { queueServiceFactory } from "@app/queue"; import { keyStoreFactory } from "@app/keystore/keystore"; import { Redis } from "ioredis"; -import { initializeHsmModule } from "@app/services/hsm/hsm-fns"; +import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); export default { diff --git a/backend/src/services/hsm/hsm-fns.ts b/backend/src/ee/services/hsm/hsm-fns.ts similarity index 100% rename from backend/src/services/hsm/hsm-fns.ts rename to backend/src/ee/services/hsm/hsm-fns.ts diff --git a/backend/src/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts similarity index 99% rename from backend/src/services/hsm/hsm-service.ts rename to backend/src/ee/services/hsm/hsm-service.ts index a2dd14bc74..2530651381 100644 --- a/backend/src/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -88,7 +88,7 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServi const appCfg = getConfig(); // Constants for buffer structure - const IV_LENGTH = 16; + const IV_LENGTH = 12; const TAG_LENGTH = 16; const $findMasterKey = (session: grapheneLib.Session) => { diff --git a/backend/src/services/hsm/hsm-types.ts b/backend/src/ee/services/hsm/hsm-types.ts similarity index 100% rename from backend/src/services/hsm/hsm-types.ts rename to backend/src/ee/services/hsm/hsm-types.ts diff --git a/backend/src/main.ts b/backend/src/main.ts index db67e7a596..f8adea33f8 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,6 +1,8 @@ import dotenv from "dotenv"; import path from "path"; +import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; + import { initAuditLogDbConnection, initDbConnection } from "./db"; import { keyStoreFactory } from "./keystore/keystore"; import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env"; @@ -9,7 +11,6 @@ import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; import { bootstrapCheck } from "./server/boot-strap-check"; -import { initializeHsmModule } from "./services/hsm/hsm-fns"; import { smtpServiceFactory } from "./services/smtp/smtp-service"; dotenv.config(); diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 4d002f3f56..fa2c9910ac 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -14,10 +14,10 @@ import fastify from "fastify"; import { Knex } from "knex"; import { Logger } from "pino"; +import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; -import { HsmModule } from "@app/services/hsm/hsm-types"; import { TSmtpService } from "@app/services/smtp/smtp-service"; import { globalRateLimiterCfg } from "./config/rateLimiter"; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 0ef53e14e8..88bfaaaae0 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -30,6 +30,8 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; +import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; @@ -107,8 +109,6 @@ import { externalMigrationServiceFactory } from "@app/services/external-migratio import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service"; -import { hsmServiceFactory } from "@app/services/hsm/hsm-service"; -import { HsmModule } from "@app/services/hsm/hsm-types"; import { identityDALFactory } from "@app/services/identity/identity-dal"; import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal"; diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index f3c1212330..9891f58e50 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -198,7 +198,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/root-kms-config", + url: "/encryption-strategies", config: { rateLimit: readLimit }, @@ -228,7 +228,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); server.route({ - method: "POST", + method: "PATCH", url: "/encryption-strategies", config: { rateLimit: writeLimit diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 92b909870f..007d33e617 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -9,6 +9,7 @@ import { KmsProviders, TExternalKmsProviderFns } from "@app/ee/services/external-kms/providers/model"; +import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { randomSecureBytes } from "@app/lib/crypto"; @@ -19,7 +20,6 @@ import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { getByteLengthForAlgorithm, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns"; -import { THsmServiceFactory } from "../hsm/hsm-service"; import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { TInternalKmsDALFactory } from "./internal-kms-dal"; diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 30fffca3b2..6cd13050ee 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -91,7 +91,7 @@ export const useUpdateServerEncryptionStrategy = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (strategy: RootKeyEncryptionStrategy) => { - await apiRequest.post("/api/v1/admin/encryption-strategies", { strategy }); + await apiRequest.patch("/api/v1/admin/encryption-strategies", { strategy }); }, onSuccess: () => { queryClient.invalidateQueries(adminQueryKeys.getServerEncryptionStrategies()); diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 0ab3750e4f..a1d32bce35 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -85,7 +85,7 @@ export const useGetServerRootKmsEncryptionDetails = () => { queryKey: adminQueryKeys.getServerEncryptionStrategies(), queryFn: async () => { const { data } = await apiRequest.get( - "/api/v1/admin/root-kms-config" + "/api/v1/admin/encryption-strategies" ); return data; From 1041e136fba6a35a69b111c4326833d98f473608 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 6 Nov 2024 01:28:44 +0400 Subject: [PATCH 22/32] added keystore --- backend/src/ee/services/hsm/hsm-service.ts | 146 ++++++++++++--------- backend/src/server/app.ts | 3 +- backend/src/server/routes/index.ts | 3 +- 3 files changed, 89 insertions(+), 63 deletions(-) diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index 2530651381..fe273b3eea 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -1,14 +1,19 @@ import grapheneLib from "graphene-pk11"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; +import { Lock } from "@app/lib/red-lock"; import { HsmModule, RequiredMechanisms } from "./hsm-types"; type THsmServiceFactoryDep = { hsmModule: HsmModule; + keyStore: Pick; }; +const HSM_SESSION_WAIT_KEY = "wait_till_hsm_session_ready"; + const USER_ALREADY_LOGGED_IN_ERROR = "CKR_USER_ALREADY_LOGGED_IN"; const WRAPPED_KEY_LENGTH = 32 + 8; // AES-256 key + padding @@ -17,79 +22,98 @@ export type THsmServiceFactory = ReturnType; type SyncOrAsync = T | Promise; type SessionCallback = (session: grapheneLib.Session) => SyncOrAsync; -export const withSession = async ( - { module, graphene }: HsmModule, - callbackWithSession: SessionCallback -): Promise => { +// eslint-disable-next-line no-empty-pattern +export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: THsmServiceFactoryDep) => { const appCfg = getConfig(); - let session: grapheneLib.Session | null = null; - try { - if (!module) { - throw new Error("PKCS#11 module is not initialized"); - } + // Constants for buffer structure + const IV_LENGTH = 12; + const TAG_LENGTH = 16; - // Create new session - const slot = module.getSlots(appCfg.HSM_SLOT); - // eslint-disable-next-line no-bitwise - if (!(slot.flags & graphene.SlotFlag.TOKEN_PRESENT)) { - throw new Error("Slot is not initialized"); - } + const $withSession = async (callbackWithSession: SessionCallback): Promise => { + const RETRY_INTERVAL = 300; // 300ms between attempts + const MAX_TIMEOUT = 30_000; // 30 seconds maximum total time - for (let i = 0; i < 10; i += 1) { - try { - // eslint-disable-next-line no-bitwise - session = slot.open(graphene.SessionFlag.RW_SESSION | graphene.SessionFlag.SERIAL_SESSION); - session.login(appCfg.HSM_PIN!); - } catch (error) { - if ((error as Error)?.message !== USER_ALREADY_LOGGED_IN_ERROR) { - throw error; - } - logger.warn("HSM session already logged in"); + let session: grapheneLib.Session | null = null; + let lock: Lock | null = null; + + const removeSession = () => { + if (session) { + session.logout(); + session.close(); session = null; } + }; - if (session) { - break; + try { + if (!module) { + throw new Error("PKCS#11 module is not initialized"); } - logger.warn("Waiting for session to be available..."); - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { - let sleepAmount = 1_500 * (i + 1); - if (sleepAmount > 5000) sleepAmount = 5000; + // Create new session + const slot = module.getSlots(appCfg.HSM_SLOT); + // eslint-disable-next-line no-bitwise + if (!(slot.flags & graphene.SlotFlag.TOKEN_PRESENT)) { + throw new Error("Slot is not initialized"); + } - setTimeout(resolve, sleepAmount); - }); - } + lock = await keyStore.acquireLock(["HSM_SESSION_LOCK"], 10_000, { retryCount: 3 }).catch(() => null); - if (!session) { - throw new Error("Failed to open session"); - } + if (!lock) { + await keyStore.waitTillReady({ + key: HSM_SESSION_WAIT_KEY, + keyCheckCb: (val) => val === "true", + waitingCb: () => logger.info("HSM Lock: Waiting for session to be available...") + }); + } - // Execute the callback and await its result (works for both sync and async) - const result = await callbackWithSession(session); - return result; - } finally { - // Clean up session if it was created - if (session) { + const startTime = Date.now(); + while (Date.now() - startTime < MAX_TIMEOUT) { + try { + // eslint-disable-next-line no-bitwise + session = slot.open(graphene.SessionFlag.RW_SESSION | graphene.SessionFlag.SERIAL_SESSION); + session.login(appCfg.HSM_PIN!); + // session.login("4311"); + break; + } catch (error) { + if ((error as Error)?.message !== USER_ALREADY_LOGGED_IN_ERROR) { + throw error; + } + logger.warn("HSM session already logged in"); + } + + logger.warn(`HSM: No session available. Waiting for session to be available... [retry=${RETRY_INTERVAL}ms]`); + + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, RETRY_INTERVAL); + }); + } + + if (!session) { + throw new Error("Failed to open session"); + } + + // Execute the callback and await its result (works for both sync and async) + const result = await callbackWithSession(session); + + if (session) { + removeSession(); + await keyStore.setItemWithExpiry(HSM_SESSION_WAIT_KEY, 10, "true"); + } + + return result; + } finally { + // Clean up session if it was created try { - session.logout(); - session.close(); + removeSession(); } catch (error) { - logger.error("Error cleaning up HSM session:", error); + logger.error(error, "Error cleaning up HSM session:"); } - } - } -}; - -// eslint-disable-next-line no-empty-pattern -export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServiceFactoryDep) => { - const appCfg = getConfig(); - // Constants for buffer structure - const IV_LENGTH = 12; - const TAG_LENGTH = 16; + await lock?.release(); + } + }; const $findMasterKey = (session: grapheneLib.Session) => { // Find the master key (root key) @@ -203,7 +227,7 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServi return $performEncryption(providedSession); } - const encrypted = await withSession({ module, graphene }, $performEncryption); + const encrypted = await $withSession($performEncryption); return encrypted; }; @@ -239,7 +263,7 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServi if (providedSession) { return $performDecryption(providedSession); } - const decrypted = await withSession({ module, graphene }, (newSession) => $performDecryption(newSession)); + const decrypted = await $withSession($performDecryption); return decrypted; }; @@ -278,7 +302,7 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServi let pkcs11TestPassed = false; try { - pkcs11TestPassed = await withSession({ module, graphene }, $testPkcs11Module); + pkcs11TestPassed = await $withSession($testPkcs11Module); } catch (err) { logger.error(err, "isActive: Error testing PKCS#11 module"); } @@ -290,7 +314,7 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServi if (!appCfg.isHsmConfigured || !module) return; try { - await withSession({ module, graphene }, async (session) => { + await $withSession(async (session) => { // Check if master key exists, create if not if (!$keyExists(session)) { // Generate 256-bit AES master key with persistent storage diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index fa2c9910ac..3a22aafba0 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -47,7 +47,8 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key logger: appCfg.NODE_ENV === "test" ? false : logger, trustProxy: true, connectionTimeout: 30 * 1000, - ignoreTrailingSlash: true + ignoreTrailingSlash: true, + pluginTimeout: 40_000 }).withTypeProvider(); server.setValidatorCompiler(validatorCompiler); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 88bfaaaae0..065b3f6178 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -363,7 +363,8 @@ export const registerRoutes = async ( const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); const hsmService = hsmServiceFactory({ - hsmModule + hsmModule, + keyStore }); const kmsService = kmsServiceFactory({ From 395b3d9e05b90323b581c0242c7bf04461a1128e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 6 Nov 2024 19:49:04 +0400 Subject: [PATCH 23/32] requested changes requested changes temp: team debugging Revert "temp: team debugging" This reverts commit 6533d731f829d79f41bf2f7209e3a636553792b1. feat: hsm support Update hsm-service.ts feat: hsm support --- ...standalone-docker-img-postgres-offical.yml | 169 ++++-- .infisicalignore | 1 + Dockerfile.fips.standalone-infisical | 167 ++++++ backend/package-lock.json | 31 +- backend/package.json | 3 +- backend/src/ee/services/hsm/hsm-fns.ts | 39 +- backend/src/ee/services/hsm/hsm-service.ts | 510 +++++++++++------- backend/src/ee/services/hsm/hsm-types.ts | 12 +- backend/src/lib/config/env.ts | 39 +- backend/src/server/routes/index.ts | 3 +- 10 files changed, 640 insertions(+), 334 deletions(-) create mode 100644 Dockerfile.fips.standalone-infisical diff --git a/.github/workflows/release-standalone-docker-img-postgres-offical.yml b/.github/workflows/release-standalone-docker-img-postgres-offical.yml index f08e882aaa..7a73288cb1 100644 --- a/.github/workflows/release-standalone-docker-img-postgres-offical.yml +++ b/.github/workflows/release-standalone-docker-img-postgres-offical.yml @@ -1,62 +1,115 @@ name: Release standalone docker image on: - push: - tags: - - "infisical/v*.*.*-postgres" + push: + tags: + - "infisical/v*.*.*-postgres" jobs: - infisical-tests: - name: Run tests before deployment - # https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview - uses: ./.github/workflows/run-backend-tests.yml - infisical-standalone: - name: Build infisical standalone image postgres - runs-on: ubuntu-latest - needs: [infisical-tests] - steps: - - name: Extract version from tag - id: extract_version - run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" - - name: ☁️ Checkout source - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: 📦 Install dependencies to test all dependencies - run: npm ci --only-production - working-directory: backend - - name: version output - run: | - echo "Output Value: ${{ steps.version.outputs.major }}" - echo "Output Value: ${{ steps.version.outputs.minor }}" - echo "Output Value: ${{ steps.version.outputs.patch }}" - echo "Output Value: ${{ steps.version.outputs.version }}" - echo "Output Value: ${{ steps.version.outputs.version_type }}" - echo "Output Value: ${{ steps.version.outputs.increment }}" - - name: Save commit hashes for tag - id: commit - uses: pr-mpt/actions-commit-hash@v2 - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: 🐋 Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Depot CLI - uses: depot/setup-action@v1 - - name: 📦 Build backend and export to Docker - uses: depot/build-push-action@v1 - with: - project: 64mmf0n610 - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} - push: true - context: . - tags: | - infisical/infisical:latest-postgres - infisical/infisical:${{ steps.commit.outputs.short }} - infisical/infisical:${{ steps.extract_version.outputs.version }} - platforms: linux/amd64,linux/arm64 - file: Dockerfile.standalone-infisical - build-args: | - POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} + infisical-tests: + name: Run tests before deployment + # https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview + uses: ./.github/workflows/run-backend-tests.yml + + infisical-standalone: + name: Build infisical standalone image postgres + runs-on: ubuntu-latest + needs: [infisical-tests] + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" + - name: ☁️ Checkout source + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 📦 Install dependencies to test all dependencies + run: npm ci --only-production + working-directory: backend + - name: version output + run: | + echo "Output Value: ${{ steps.version.outputs.major }}" + echo "Output Value: ${{ steps.version.outputs.minor }}" + echo "Output Value: ${{ steps.version.outputs.patch }}" + echo "Output Value: ${{ steps.version.outputs.version }}" + echo "Output Value: ${{ steps.version.outputs.version_type }}" + echo "Output Value: ${{ steps.version.outputs.increment }}" + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: 📦 Build backend and export to Docker + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + push: true + context: . + tags: | + infisical/infisical:latest-postgres + infisical/infisical:${{ steps.commit.outputs.short }} + infisical/infisical:${{ steps.extract_version.outputs.version }} + platforms: linux/amd64,linux/arm64 + file: Dockerfile.standalone-infisical + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} + + infisical-fips-standalone: + name: Build infisical standalone image postgres + runs-on: ubuntu-latest + needs: [infisical-tests] + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" + - name: ☁️ Checkout source + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 📦 Install dependencies to test all dependencies + run: npm ci --only-production + working-directory: backend + - name: version output + run: | + echo "Output Value: ${{ steps.version.outputs.major }}" + echo "Output Value: ${{ steps.version.outputs.minor }}" + echo "Output Value: ${{ steps.version.outputs.patch }}" + echo "Output Value: ${{ steps.version.outputs.version }}" + echo "Output Value: ${{ steps.version.outputs.version_type }}" + echo "Output Value: ${{ steps.version.outputs.increment }}" + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: 📦 Build backend and export to Docker + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + push: true + context: . + tags: | + infisical/infisical-fips:latest-postgres + infisical/infisical-fips:${{ steps.commit.outputs.short }} + infisical/infisical-fips:${{ steps.extract_version.outputs.version }} + platforms: linux/amd64,linux/arm64 + file: Dockerfile.fips.standalone-infisical + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} diff --git a/.infisicalignore b/.infisicalignore index b7fc38b356..4c19af70c5 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -6,3 +6,4 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/S docs/self-hosting/configuration/envars.mdx:generic-api-key:106 frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451 docs/mint.json:generic-api-key:651 +backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134 diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical new file mode 100644 index 0000000000..53c24db5d5 --- /dev/null +++ b/Dockerfile.fips.standalone-infisical @@ -0,0 +1,167 @@ +ARG POSTHOG_HOST=https://app.posthog.com +ARG POSTHOG_API_KEY=posthog-api-key +ARG INTERCOM_ID=intercom-id +ARG CAPTCHA_SITE_KEY=captcha-site-key + +FROM --platform=linux/amd64 node:20-slim AS base + +FROM base AS frontend-dependencies +WORKDIR /app + +COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./ + +# Install dependencies +RUN npm ci --only-production --ignore-scripts + +# Rebuild the source code only when needed +FROM --platform=linux/amd64 base AS frontend-builder +WORKDIR /app + +# Copy dependencies +COPY --from=frontend-dependencies /app/node_modules ./node_modules +# Copy all files +COPY /frontend . + +ENV NODE_ENV production +ENV NEXT_PUBLIC_ENV production +ARG POSTHOG_HOST +ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST +ARG POSTHOG_API_KEY +ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY +ARG INTERCOM_ID +ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID +ARG INFISICAL_PLATFORM_VERSION +ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION +ARG CAPTCHA_SITE_KEY +ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY + +# Build +RUN npm run build + +# Production image +FROM --platform=linux/amd64 base AS frontend-runner +WORKDIR /app + +RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user + +RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images +VOLUME /app/.next/cache/images + +COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts +COPY --from=frontend-builder /app/public ./public +RUN chown non-root-user:nodejs ./public/data + +COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./ +COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static + +USER non-root-user + +ENV NEXT_TELEMETRY_DISABLED 1 + +## +## BACKEND +## +FROM --platform=linux/amd64 base AS backend-build + +ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ + +RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user + +WORKDIR /app + +# Required for pkcs11js +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +COPY backend/package*.json ./ +RUN npm ci --only-production + +COPY /backend . +COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh +RUN npm i -D tsconfig-paths +RUN npm run build + +# Production stage +FROM --platform=linux/amd64 base AS backend-runner + +ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ + +WORKDIR /app + +# Required for pkcs11js +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +COPY backend/package*.json ./ +RUN npm ci --only-production + +COPY --from=backend-build /app . + +RUN mkdir frontend-build + +# Production stage +FROM --platform=linux/amd64 base AS production + +# Install necessary packages +RUN apt-get update && apt-get install -y \ + ca-certificates \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Infisical CLI +RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \ + && apt-get update && apt-get install -y infisical=0.31.1 \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user + +# Give non-root-user permission to update SSL certs +RUN chown -R non-root-user /etc/ssl/certs +RUN chown non-root-user /etc/ssl/certs/ca-certificates.crt +RUN chmod -R u+rwx /etc/ssl/certs +RUN chmod u+rw /etc/ssl/certs/ca-certificates.crt +RUN chown non-root-user /usr/sbin/update-ca-certificates +RUN chmod u+rx /usr/sbin/update-ca-certificates + +## set pre baked keys +ARG POSTHOG_API_KEY +ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ + BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY +ARG INTERCOM_ID=intercom-id +ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ + BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID +ARG CAPTCHA_SITE_KEY +ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \ + BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY + +WORKDIR / + +COPY --from=backend-runner /app /backend + +COPY --from=frontend-runner /app ./backend/frontend-build + +ENV PORT 8080 +ENV HOST=0.0.0.0 +ENV HTTPS_ENABLED false +ENV NODE_ENV production +ENV STANDALONE_BUILD true +ENV STANDALONE_MODE true +ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ + +WORKDIR /backend + +ENV TELEMETRY_ENABLED true + +EXPOSE 8080 +EXPOSE 443 + +USER non-root-user + +CMD ["./standalone-entrypoint.sh"] \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index ef9f4e0d33..d55521f5bf 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -55,7 +55,6 @@ "fastify-plugin": "^4.5.1", "google-auth-library": "^9.9.0", "googleapis": "^137.1.0", - "graphene-pk11": "^2.3.6", "handlebars": "^4.7.8", "hdb": "^0.19.10", "ioredis": "^5.3.2", @@ -84,6 +83,7 @@ "pg-query-stream": "^4.5.3", "picomatch": "^3.0.1", "pino": "^8.16.2", + "pkcs11js": "^2.1.6", "pkijs": "^3.2.4", "posthog-node": "^3.6.2", "probot": "^13.3.8", @@ -121,6 +121,7 @@ "@types/passport-google-oauth20": "^2.0.14", "@types/pg": "^8.10.9", "@types/picomatch": "^2.3.3", + "@types/pkcs11js": "^1.0.4", "@types/prompt-sync": "^4.2.3", "@types/resolve": "^1.20.6", "@types/safe-regex": "^1.1.6", @@ -8831,6 +8832,17 @@ "integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==", "dev": true }, + "node_modules/@types/pkcs11js": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/pkcs11js/-/pkcs11js-1.0.4.tgz", + "integrity": "sha512-Pkq8VbwZZv7o/6ODFOhxw0s0M8J4ucg4/I4V1dSCn8tUwWgIKIYzuV4Pp2fYuir81DgQXAF5TpGyhBMjJ3FjFw==", + "deprecated": "This is a stub types definition for pkcs11js (https://github.com/PeculiarVentures/pkcs11js). pkcs11js provides its own type definitions, so you don't need @types/pkcs11js installed!", + "dev": true, + "license": "MIT", + "dependencies": { + "pkcs11js": "*" + } + }, "node_modules/@types/prompt-sync": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", @@ -13557,23 +13569,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/graphene-pk11": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/graphene-pk11/-/graphene-pk11-2.3.6.tgz", - "integrity": "sha512-ol9Pf7XDv5UTjh1DPqtmQVZQqUheiXBzQVXQWRCLWq78+brKQB0Kum/s0NGEcsd/5NQQG8MFA2U/KNujEoC1fQ==", - "license": "MIT", - "dependencies": { - "pkcs11js": "^2.1.6", - "tslib": "^2.7.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/PeculiarVentures" - } - }, "node_modules/graphql": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", diff --git a/backend/package.json b/backend/package.json index 0278689d33..240b7db2c2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -84,6 +84,7 @@ "@types/passport-google-oauth20": "^2.0.14", "@types/pg": "^8.10.9", "@types/picomatch": "^2.3.3", + "@types/pkcs11js": "^1.0.4", "@types/prompt-sync": "^4.2.3", "@types/resolve": "^1.20.6", "@types/safe-regex": "^1.1.6", @@ -160,7 +161,6 @@ "fastify-plugin": "^4.5.1", "google-auth-library": "^9.9.0", "googleapis": "^137.1.0", - "graphene-pk11": "^2.3.6", "handlebars": "^4.7.8", "hdb": "^0.19.10", "ioredis": "^5.3.2", @@ -189,6 +189,7 @@ "pg-query-stream": "^4.5.3", "picomatch": "^3.0.1", "pino": "^8.16.2", + "pkcs11js": "^2.1.6", "pkijs": "^3.2.4", "posthog-node": "^3.6.2", "probot": "^13.3.8", diff --git a/backend/src/ee/services/hsm/hsm-fns.ts b/backend/src/ee/services/hsm/hsm-fns.ts index 59a50f51f7..f91f9a0042 100644 --- a/backend/src/ee/services/hsm/hsm-fns.ts +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -1,4 +1,4 @@ -import * as grapheneLib from "graphene-pk11"; +import * as pkcs11js from "pkcs11js"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; @@ -8,26 +8,47 @@ import { HsmModule } from "./hsm-types"; export const initializeHsmModule = () => { const appCfg = getConfig(); - let module: grapheneLib.Module | null = null; + // Create a new instance of PKCS11 module + const pkcs11 = new pkcs11js.PKCS11(); + let isInitialized = false; const initialize = () => { if (!appCfg.isHsmConfigured) { return; } - module = grapheneLib.Module.load(appCfg.HSM_LIB_PATH!, "InfisicalHSM"); - module.initialize(); - logger.info("PKCS#11 module initialized"); + try { + // Load the PKCS#11 module + pkcs11.load(appCfg.HSM_LIB_PATH!); + + // Initialize the module + pkcs11.C_Initialize(); + isInitialized = true; + + logger.info("PKCS#11 module initialized"); + } catch (err) { + logger.error("Failed to initialize PKCS#11 module:", err); + throw err; + } }; const finalize = () => { - if (module) { - module.finalize(); - logger.info("PKCS#11 module finalized"); + if (isInitialized) { + try { + pkcs11.C_Finalize(); + isInitialized = false; + logger.info("PKCS#11 module finalized"); + } catch (err) { + logger.error("Failed to finalize PKCS#11 module:", err); + throw err; + } } }; - const getModule = (): HsmModule => ({ module, graphene: grapheneLib }); + const getModule = (): HsmModule => ({ + pkcs11, + isInitialized + }); return { initialize, diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index fe273b3eea..73c4b279cc 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -1,277 +1,339 @@ -import grapheneLib from "graphene-pk11"; +import pkcs11js from "pkcs11js"; -import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; -import { Lock } from "@app/lib/red-lock"; -import { HsmModule, RequiredMechanisms } from "./hsm-types"; +import { HsmKeyType, HsmModule } from "./hsm-types"; type THsmServiceFactoryDep = { hsmModule: HsmModule; - keyStore: Pick; }; -const HSM_SESSION_WAIT_KEY = "wait_till_hsm_session_ready"; - -const USER_ALREADY_LOGGED_IN_ERROR = "CKR_USER_ALREADY_LOGGED_IN"; -const WRAPPED_KEY_LENGTH = 32 + 8; // AES-256 key + padding - export type THsmServiceFactory = ReturnType; type SyncOrAsync = T | Promise; -type SessionCallback = (session: grapheneLib.Session) => SyncOrAsync; +type SessionCallback = (session: pkcs11js.Handle) => SyncOrAsync; // eslint-disable-next-line no-empty-pattern -export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: THsmServiceFactoryDep) => { +export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsmServiceFactoryDep) => { const appCfg = getConfig(); - // Constants for buffer structure - const IV_LENGTH = 12; - const TAG_LENGTH = 16; + // Constants for buffer structures + const IV_LENGTH = 16; // Luna HSM typically expects 16-byte IV for cbc + const BLOCK_SIZE = 16; + const HMAC_SIZE = 32; const $withSession = async (callbackWithSession: SessionCallback): Promise => { const RETRY_INTERVAL = 300; // 300ms between attempts const MAX_TIMEOUT = 30_000; // 30 seconds maximum total time - let session: grapheneLib.Session | null = null; - let lock: Lock | null = null; + let sessionHandle: pkcs11js.Handle | null = null; const removeSession = () => { - if (session) { - session.logout(); - session.close(); - session = null; + if (sessionHandle !== null) { + try { + pkcs11.C_Logout(sessionHandle); + pkcs11.C_CloseSession(sessionHandle); + logger.info("HSM: Terminated session successfully"); + } catch (error) { + logger.error("Error during session cleanup:", error); + } finally { + sessionHandle = null; + } } }; try { - if (!module) { + if (!pkcs11 || !isInitialized) { throw new Error("PKCS#11 module is not initialized"); } - // Create new session - const slot = module.getSlots(appCfg.HSM_SLOT); - // eslint-disable-next-line no-bitwise - if (!(slot.flags & graphene.SlotFlag.TOKEN_PRESENT)) { - throw new Error("Slot is not initialized"); + // Get slot list + let slots: pkcs11js.Handle[]; + try { + slots = pkcs11.C_GetSlotList(false); // false to get all slots + } catch (error) { + throw new Error(`Failed to get slot list: ${(error as Error)?.message}`); } - lock = await keyStore.acquireLock(["HSM_SESSION_LOCK"], 10_000, { retryCount: 3 }).catch(() => null); + if (slots.length === 0) { + throw new Error("No slots available"); + } - if (!lock) { - await keyStore.waitTillReady({ - key: HSM_SESSION_WAIT_KEY, - keyCheckCb: (val) => val === "true", - waitingCb: () => logger.info("HSM Lock: Waiting for session to be available...") - }); + if (appCfg.HSM_SLOT >= slots.length) { + throw new Error(`HSM slot ${appCfg.HSM_SLOT} not found or not initialized`); } + const slotId = slots[appCfg.HSM_SLOT]; + const startTime = Date.now(); while (Date.now() - startTime < MAX_TIMEOUT) { try { + // Open session // eslint-disable-next-line no-bitwise - session = slot.open(graphene.SessionFlag.RW_SESSION | graphene.SessionFlag.SERIAL_SESSION); - session.login(appCfg.HSM_PIN!); - // session.login("4311"); - break; + sessionHandle = pkcs11.C_OpenSession(slotId, pkcs11js.CKF_SERIAL_SESSION | pkcs11js.CKF_RW_SESSION); + + // Login + try { + pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, appCfg.HSM_PIN); + logger.info("HSM: Successfully authenticated"); + break; + } catch (error) { + if (error instanceof pkcs11js.Pkcs11Error) { + // Handle specific error cases + if (error.code === pkcs11js.CKR_PIN_INCORRECT) { + logger.error(error, `Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`); + throw new Error("Incorrect HSM Pin detected. Please check the HSM configuration."); + } + + if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) { + logger.warn("HSM session already logged in"); + } + } + throw error; // Re-throw other errors + } } catch (error) { - if ((error as Error)?.message !== USER_ALREADY_LOGGED_IN_ERROR) { - throw error; + logger.warn(`HSM: Session creation failed. Retrying... Error: ${(error as Error)?.message}`); + + if (sessionHandle !== null) { + try { + pkcs11.C_CloseSession(sessionHandle); + } catch (closeError) { + logger.error("Error closing failed session:", closeError); + } + sessionHandle = null; } - logger.warn("HSM session already logged in"); - } - - logger.warn(`HSM: No session available. Waiting for session to be available... [retry=${RETRY_INTERVAL}ms]`); - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { - setTimeout(resolve, RETRY_INTERVAL); - }); - } - - if (!session) { - throw new Error("Failed to open session"); + // Wait before retrying + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, RETRY_INTERVAL); + }); + } } - // Execute the callback and await its result (works for both sync and async) - const result = await callbackWithSession(session); - - if (session) { - removeSession(); - await keyStore.setItemWithExpiry(HSM_SESSION_WAIT_KEY, 10, "true"); + if (sessionHandle === null) { + throw new Error("Failed to open session after maximum retries"); } + // Execute callback with session handle + const result = await callbackWithSession(sessionHandle); + removeSession(); return result; + } catch (error) { + logger.error("Error in HSM session handling:", error); + throw error; } finally { - // Clean up session if it was created - try { - removeSession(); - } catch (error) { - logger.error(error, "Error cleaning up HSM session:"); - } - - await lock?.release(); + // Ensure cleanup + removeSession(); } }; - const $findMasterKey = (session: grapheneLib.Session) => { - // Find the master key (root key) - const template = { - class: graphene.ObjectClass.SECRET_KEY, - keyType: graphene.KeyType.AES, - label: appCfg.HSM_KEY_LABEL - } as grapheneLib.ITemplate; - - const key = session.find(template).items(0); + const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => { + const label = type === HsmKeyType.HMAC ? `${appCfg.HSM_KEY_LABEL}_HMAC` : appCfg.HSM_KEY_LABEL; + const keyType = type === HsmKeyType.HMAC ? pkcs11js.CKK_GENERIC_SECRET : pkcs11js.CKK_AES; - if (!key) { - throw new Error("Failed to find master key"); - } - - return key; - }; - - const $generateAndWrapKey = (session: grapheneLib.Session) => { - const masterKey = $findMasterKey(session); - - // Generate a new session key for encryption - const sessionKey = session.generateKey(graphene.KeyGenMechanism.AES, { - class: graphene.ObjectClass.SECRET_KEY, - keyType: graphene.KeyType.AES, - token: false, // Session-only key - sensitive: true, - extractable: true, // Must be true to allow wrapping - encrypt: true, - decrypt: true, - valueLen: 32 // 256-bit key - } as grapheneLib.ITemplate); - - // Wrap the session key with master key - const wrappingMech = { name: "AES_KEY_WRAP", params: null }; - const wrappedKey = session.wrapKey( - wrappingMech, - new graphene.Key(masterKey).toType(), - new graphene.Key(sessionKey).toType() - ); - - return { wrappedKey, sessionKey }; - }; + const template = [ + { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, + { type: pkcs11js.CKA_KEY_TYPE, value: keyType }, + { type: pkcs11js.CKA_LABEL, value: label } + ]; - const $unwrapKey = (session: grapheneLib.Session, wrappedKey: Buffer) => { - const masterKey = $findMasterKey(session); + try { + // Initialize search + pkcs11.C_FindObjectsInit(sessionHandle, template); - // Absolute minimal template - let HSM set most attributes - const unwrapTemplate = { - class: graphene.ObjectClass.SECRET_KEY, - keyType: graphene.KeyType.AES - } as grapheneLib.ITemplate; + try { + // Find first matching object + const handles = pkcs11.C_FindObjects(sessionHandle, 1); - const unwrappingMech = { - name: "AES_KEY_WRAP", - params: null - } as grapheneLib.MechanismType; + if (handles.length === 0) { + throw new Error("Failed to find master key"); + } - return session.unwrapKey(unwrappingMech, new graphene.Key(masterKey).toType(), wrappedKey, unwrapTemplate); + return handles[0]; // Return the key handle + } finally { + // Always finalize the search operation + pkcs11.C_FindObjectsFinal(sessionHandle); + } + } catch (error) { + logger.error("Error finding master key:", error); + return null; + } }; - const $keyExists = (session: grapheneLib.Session): boolean => { + const $keyExists = (session: pkcs11js.Handle, type: HsmKeyType): boolean => { try { - const key = $findMasterKey(session); + const key = $findKey(session, type); // items(0) will throw an error if no items are found // Return true only if we got a valid object with handle - return key && typeof key.handle !== "undefined"; + return !!key && key.length > 0; } catch (error) { // If items(0) throws, it means no key was found // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call - if ((error as any).message?.includes("CKR_OBJECT_HANDLE_INVALID")) { - return false; - } logger.error(error, "Error checking for HSM key presence"); + + if (error instanceof pkcs11js.Pkcs11Error) { + if (error.code === pkcs11js.CKR_OBJECT_HANDLE_INVALID) { + return false; + } + } + return false; } }; const encrypt: { - (data: Buffer, providedSession: grapheneLib.Session): Promise; + (data: Buffer, providedSession: pkcs11js.Handle): Promise; (data: Buffer): Promise; - } = async (data: Buffer, providedSession?: grapheneLib.Session) => { - if (!module) { + } = async (data: Buffer, providedSession?: pkcs11js.Handle) => { + if (!pkcs11 || !isInitialized) { throw new Error("PKCS#11 module is not initialized"); } - const $performEncryption = (s: grapheneLib.Session) => { - // Generate IV for encryption - const iv = s.generateRandom(IV_LENGTH); + const $performEncryption = (sessionHandle: pkcs11js.Handle) => { + try { + const aesKey = $findKey(sessionHandle, HsmKeyType.AES); + if (!aesKey) { + throw new Error("AES key not found"); + } + + const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC); + if (!hmacKey) { + throw new Error("HMAC key not found"); + } + + const iv = Buffer.alloc(IV_LENGTH); + pkcs11.C_GenerateRandom(sessionHandle, iv); + + const encryptMechanism = { + mechanism: pkcs11js.CKM_AES_CBC_PAD, + parameter: iv + }; + + pkcs11.C_EncryptInit(sessionHandle, encryptMechanism, aesKey); + + // Calculate max buffer size (input length + potential full block of padding) + const maxEncryptedLength = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE; + const tempBuffer = Buffer.alloc(maxEncryptedLength); - // Generate and wrap a new session key - const { wrappedKey, sessionKey } = $generateAndWrapKey(s); + // First call to get the actual length + const encryptedLength = pkcs11.C_Encrypt(sessionHandle, data, tempBuffer); - const alg = { - name: appCfg.HSM_MECHANISM, - params: new graphene.AesGcm240Params(iv) - } as grapheneLib.IAlgorithm; + // Create a copy of the encrypted data using the actual length + const encryptedData = Buffer.from(tempBuffer.slice(0, encryptedLength.length || 16)); - const cipher = s.createCipher(alg, new graphene.Key(sessionKey).toType()); + // Initialize HMAC + const hmacMechanism = { + mechanism: pkcs11js.CKM_SHA256_HMAC + }; - // Calculate the output buffer size based on input length - // GCM adds a 16-byte auth tag, so we need input length + 16 - const outputBuffer = Buffer.alloc(data.length + TAG_LENGTH); - const encryptedData = cipher.once(data, outputBuffer); + pkcs11.C_SignInit(sessionHandle, hmacMechanism, hmacKey); - // Format: [Wrapped Key (40)][IV (16)][Encrypted Data + Tag] - return Buffer.concat([wrappedKey, iv, encryptedData]); + // Sign the IV and encrypted data + pkcs11.C_SignUpdate(sessionHandle, iv); + pkcs11.C_SignUpdate(sessionHandle, encryptedData); + + // Get the HMAC + const hmac = Buffer.alloc(HMAC_SIZE); + pkcs11.C_SignFinal(sessionHandle, hmac); + + // Combine encrypted data and HMAC [Encrypted Data | HMAC] + const finalBuffer = Buffer.alloc(encryptedData.length + hmac.length); + encryptedData.copy(finalBuffer); + hmac.copy(finalBuffer, encryptedData.length); + + return Buffer.concat([iv, finalBuffer]); + } catch (error) { + logger.error("Encryption error:", error); + throw new Error(`Encryption failed: ${(error as Error)?.message}`); + } }; if (providedSession) { return $performEncryption(providedSession); } - const encrypted = await $withSession($performEncryption); - - return encrypted; + const result = await $withSession($performEncryption); + return result; }; const decrypt: { - (encryptedBlob: Buffer, providedSession: grapheneLib.Session): Promise; + (encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise; (encryptedBlob: Buffer): Promise; - } = async (encryptedBlob: Buffer, providedSession?: grapheneLib.Session) => { - if (!module) { + } = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => { + if (!isInitialized) { throw new Error("HSM service not initialized"); } - const $performDecryption = (s: grapheneLib.Session) => { - const wrappedKey = encryptedBlob.subarray(0, WRAPPED_KEY_LENGTH); - const iv = encryptedBlob.subarray(WRAPPED_KEY_LENGTH, WRAPPED_KEY_LENGTH + IV_LENGTH); - const ciphertext = encryptedBlob.subarray(WRAPPED_KEY_LENGTH + IV_LENGTH); + const $performDecryption = (sessionHandle: pkcs11js.Handle) => { + try { + // structure is: [IV (16 bytes) | Encrypted Data (N bytes) | HMAC (32 bytes)] + const iv = encryptedBlob.subarray(0, IV_LENGTH); + const encryptedDataWithHmac = encryptedBlob.subarray(IV_LENGTH); - // Unwrap the session key - const sessionKey = $unwrapKey(s, wrappedKey); + // Split encrypted data and HMAC + const hmac = encryptedDataWithHmac.subarray(-HMAC_SIZE); // Last 32 bytes are HMAC - const algo = { - name: appCfg.HSM_MECHANISM, - params: new graphene.AesGcm240Params(iv) - }; + const encryptedData = encryptedDataWithHmac.slice(0, -HMAC_SIZE); // Everything except last 32 bytes - const decipher = s.createDecipher(algo, new graphene.Key(sessionKey).toType()); - const outputBuffer = Buffer.alloc(ciphertext.length); + // Find the keys + const aesKey = $findKey(sessionHandle, HsmKeyType.AES); + if (!aesKey) { + throw new Error("AES key not found"); + } + + const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC); + if (!hmacKey) { + throw new Error("HMAC key not found"); + } - // Extract wrapped key, IV, and ciphertext - return decipher.once(ciphertext, outputBuffer); + // Verify HMAC first + const hmacMechanism = { + mechanism: pkcs11js.CKM_SHA256_HMAC + }; + + pkcs11.C_VerifyInit(sessionHandle, hmacMechanism, hmacKey); + pkcs11.C_VerifyUpdate(sessionHandle, iv); + pkcs11.C_VerifyUpdate(sessionHandle, encryptedData); + + try { + pkcs11.C_VerifyFinal(sessionHandle, hmac); + } catch (error) { + throw new Error("Decryption failed"); // Generic error for failed verification + } + + // Only decrypt if verification passed + const decryptMechanism = { + mechanism: pkcs11js.CKM_AES_CBC_PAD, + parameter: iv + }; + + pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey); + + const tempBuffer = Buffer.alloc(encryptedData.length); + const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer); + + // Create a new buffer from the decrypted data + return Buffer.from(decryptedData); + } catch (error) { + logger.error("Decryption error:", error); + throw new Error(`Decryption failed: ${(error as Error)?.message}`); + } }; if (providedSession) { return $performDecryption(providedSession); } - const decrypted = await $withSession($performDecryption); - return decrypted; + const result = await $withSession($performDecryption); + return result; }; // We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device. - const $testPkcs11Module = async (session: grapheneLib.Session) => { + const $testPkcs11Module = async (session: pkcs11js.Handle) => { try { - if (!module) { + if (!isInitialized) { throw new Error("HSM service not initialized"); } @@ -279,11 +341,15 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: throw new Error("Session not initialized"); } - const randomData = session.generateRandom(256); - const encryptedData = await encrypt(Buffer.from(randomData), session); + const randomData = pkcs11.C_GenerateRandom(session, Buffer.alloc(500)); + + const encryptedData = await encrypt(randomData, session); const decryptedData = await decrypt(encryptedData, session); - if (Buffer.from(randomData).toString("hex") !== Buffer.from(decryptedData).toString("hex")) { + const randomDataHex = randomData.toString("hex"); + const decryptedDataHex = decryptedData.toString("hex"); + + if (randomDataHex !== decryptedDataHex && Buffer.compare(randomData, decryptedData)) { throw new Error("Decrypted data does not match original data"); } @@ -295,7 +361,7 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: }; const isActive = async () => { - if (!module || !appCfg.isHsmConfigured) { + if (!isInitialized || !appCfg.isHsmConfigured) { return false; } @@ -304,62 +370,94 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: try { pkcs11TestPassed = await $withSession($testPkcs11Module); } catch (err) { - logger.error(err, "isActive: Error testing PKCS#11 module"); + logger.error(err, "HSM: Error testing PKCS#11 module"); } - return appCfg.isHsmConfigured && module !== null && pkcs11TestPassed; + return appCfg.isHsmConfigured && isInitialized && pkcs11TestPassed; }; const startService = async () => { - if (!appCfg.isHsmConfigured || !module) return; + if (!appCfg.isHsmConfigured || !pkcs11 || !isInitialized) return; try { - await $withSession(async (session) => { + await $withSession(async (sessionHandle) => { // Check if master key exists, create if not - if (!$keyExists(session)) { - // Generate 256-bit AES master key with persistent storage - session.generateKey(graphene.KeyGenMechanism.AES, { - class: graphene.ObjectClass.SECRET_KEY, - token: true, - valueLen: 256 / 8, - keyType: graphene.KeyType.AES, - label: appCfg.HSM_KEY_LABEL, - derive: true, // Enable key derivation - extractable: false, - sensitive: true, - private: true - }); + + const genericAttributes = [ + { type: pkcs11js.CKA_TOKEN, value: true }, // Persistent storage + { type: pkcs11js.CKA_EXTRACTABLE, value: false }, // Cannot be extracted + { type: pkcs11js.CKA_SENSITIVE, value: true }, // Sensitive value + { type: pkcs11js.CKA_PRIVATE, value: true } // Requires authentication + ]; + + if (!$keyExists(sessionHandle, HsmKeyType.AES)) { + // Template for generating 256-bit AES master key + const keyTemplate = [ + { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, + { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_AES }, + { type: pkcs11js.CKA_VALUE_LEN, value: 256 / 8 }, + { type: pkcs11js.CKA_LABEL, value: appCfg.HSM_KEY_LABEL! }, + { type: pkcs11js.CKA_ENCRYPT, value: true }, // Allow encryption + { type: pkcs11js.CKA_DECRYPT, value: true }, // Allow decryption + ...genericAttributes + ]; + + // Generate the key + pkcs11.C_GenerateKey( + sessionHandle, + { + mechanism: pkcs11js.CKM_AES_KEY_GEN + }, + keyTemplate + ); + logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); } - // Verify HSM supports required mechanisms - const mechs = session.slot.getMechanisms(); - const mechNames: string[] = []; - - // eslint-disable-next-line no-plusplus - for (let i = 0; i < mechs.length; i++) { - mechNames.push(mechs.items(i).name); + // Check if HMAC key exists, create if not + if (!$keyExists(sessionHandle, HsmKeyType.HMAC)) { + const hmacKeyTemplate = [ + { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, + { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_GENERIC_SECRET }, + { type: pkcs11js.CKA_VALUE_LEN, value: 256 / 8 }, // 256-bit key + { type: pkcs11js.CKA_LABEL, value: `${appCfg.HSM_KEY_LABEL!}_HMAC` }, + { type: pkcs11js.CKA_SIGN, value: true }, // Allow signing + { type: pkcs11js.CKA_VERIFY, value: true }, // Allow verification + ...genericAttributes + ]; + + // Generate the HMAC key + pkcs11.C_GenerateKey( + sessionHandle, + { + mechanism: pkcs11js.CKM_GENERIC_SECRET_KEY_GEN + }, + hmacKeyTemplate + ); + + logger.info(`HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`); } - const hasAesGcm = mechNames.includes(RequiredMechanisms.AesGcm); - const hasAesKeyWrap = mechNames.includes(RequiredMechanisms.AesKeyWrap); + // Get slot info to check supported mechanisms + const slotId = pkcs11.C_GetSessionInfo(sessionHandle).slotID; + const mechanisms = pkcs11.C_GetMechanismList(slotId); - if (!hasAesGcm) { - throw new Error(`Required mechanism ${RequiredMechanisms.AesGcm} not supported by HSM`); - } - if (!hasAesKeyWrap) { - throw new Error(`Required mechanism ${RequiredMechanisms.AesKeyWrap} not supported by HSM`); + // Check for AES CBC PAD support + const hasAesCbc = mechanisms.includes(pkcs11js.CKM_AES_CBC_PAD); + + if (!hasAesCbc) { + throw new Error(`Required mechanism CKM_AEC_CBC_PAD not supported by HSM`); } - const testPassed = await $testPkcs11Module(session); + // Run test encryption/decryption + const testPassed = await $testPkcs11Module(sessionHandle); - // Run a test to verify module is working if (!testPassed) { throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured."); } }); } catch (error) { - logger.error(error, "Error initializing HSM service"); + logger.error("Error initializing HSM service:", error); throw error; } }; diff --git a/backend/src/ee/services/hsm/hsm-types.ts b/backend/src/ee/services/hsm/hsm-types.ts index e7c33ffdab..b688147f58 100644 --- a/backend/src/ee/services/hsm/hsm-types.ts +++ b/backend/src/ee/services/hsm/hsm-types.ts @@ -1,11 +1,11 @@ -import * as grapheneLib from "graphene-pk11"; +import pkcs11js from "pkcs11js"; export type HsmModule = { - module: grapheneLib.Module | null; - graphene: typeof grapheneLib; + pkcs11: pkcs11js.PKCS11; + isInitialized: boolean; }; -export enum RequiredMechanisms { - AesGcm = "AES_GCM", - AesKeyWrap = "AES_KEY_WRAP" +export enum HsmKeyType { + AES = "AES", + HMAC = "hmac" } diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index cb41ebd726..72e0c0d058 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -166,35 +166,10 @@ const envSchema = z ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"), // HSM - HSM_LIB_PATH: zpStr( - z - .string() - .optional() - .transform((val) => { - if (process.env.NODE_ENV === "development") return "/usr/local/lib/softhsm/libsofthsm2.so"; - return val; - }) - ), - HSM_PIN: zpStr( - z - .string() - .optional() - .transform((val) => { - if (process.env.NODE_ENV === "development") return "1234"; - return val; - }) - ), - HSM_KEY_LABEL: zpStr( - z - .string() - .optional() - .transform((val) => { - if (process.env.NODE_ENV === "development") return "auth-app"; - return val; - }) - ), - HSM_SLOT: z.coerce.number().optional().default(0), - HSM_MECHANISM: zpStr(z.string().optional().default("AES_GCM")) + HSM_LIB_PATH: zpStr(z.string().optional()), + HSM_PIN: zpStr(z.string().optional()), + HSM_KEY_LABEL: zpStr(z.string().optional()), + HSM_SLOT: z.coerce.number().optional().default(0) }) // To ensure that basic encryption is always possible. .refine( @@ -218,11 +193,7 @@ const envSchema = z Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET), isHsmConfigured: - Boolean(data.HSM_LIB_PATH) && - Boolean(data.HSM_PIN) && - Boolean(data.HSM_KEY_LABEL) && - Boolean(data.HSM_MECHANISM) && - data.HSM_SLOT !== undefined, + Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 065b3f6178..88bfaaaae0 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -363,8 +363,7 @@ export const registerRoutes = async ( const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); const hsmService = hsmServiceFactory({ - hsmModule, - keyStore + hsmModule }); const kmsService = kmsServiceFactory({ From 22f8a3daa7ce565fbf0ce24968c71ef0323ea6a3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 7 Nov 2024 13:49:04 +0400 Subject: [PATCH 24/32] temp: team debugging --- backend/Dockerfile.dev | 44 +++++++++--------------------------------- docker-compose.dev.yml | 1 + 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 97bc2c6a36..552508e36c 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -1,48 +1,22 @@ -FROM node:20-alpine +FROM --platform=linux/amd64 node:20-alpine -# ? Setup a test SoftHSM module. In production a real HSM is used. - -ARG SOFTHSM2_VERSION=2.5.0 - -ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \ - SOFTHSM2_SOURCES=/tmp/softhsm2 +# ? App setup -# install build dependencies including python3 RUN apk --update add \ - alpine-sdk \ - autoconf \ - automake \ - git \ - libtool \ - openssl-dev \ python3 \ make \ - g++ - -# build and install SoftHSM2 -RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES} -WORKDIR ${SOFTHSM2_SOURCES} - -RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \ - && sh autogen.sh \ - && ./configure --prefix=/usr/local --disable-gost \ - && make \ - && make install - -WORKDIR /root -RUN rm -fr ${SOFTHSM2_SOURCES} - -# install pkcs11-tool -RUN apk --update add opensc - -RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 - -# ? App setup + g++ \ + gcompat \ + libstdc++ RUN apk add --no-cache bash curl && curl -1sLf \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ && apk add infisical=0.8.1 && apk add --no-cache git + +ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ + + WORKDIR /app COPY package.json package.json diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9e56ae5891..fc78f2562a 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -92,6 +92,7 @@ services: - TELEMETRY_ENABLED=false volumes: - ./backend/src:/app/src + - "/etc/luna-docker:/usr/safenet/lunaclient" extra_hosts: - "host.docker.internal:host-gateway" From 1d3dca11e7042a25b0412a74a600a0de609f9dca Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 8 Nov 2024 10:22:17 +0400 Subject: [PATCH 25/32] Revert "temp: team debugging" This reverts commit 6533d731f829d79f41bf2f7209e3a636553792b1. --- backend/Dockerfile.dev | 44 +++++++++++++++++++++++++++++++++--------- docker-compose.dev.yml | 1 - 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 552508e36c..97bc2c6a36 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -1,21 +1,47 @@ -FROM --platform=linux/amd64 node:20-alpine +FROM node:20-alpine -# ? App setup +# ? Setup a test SoftHSM module. In production a real HSM is used. + +ARG SOFTHSM2_VERSION=2.5.0 +ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \ + SOFTHSM2_SOURCES=/tmp/softhsm2 + +# install build dependencies including python3 RUN apk --update add \ + alpine-sdk \ + autoconf \ + automake \ + git \ + libtool \ + openssl-dev \ python3 \ make \ - g++ \ - gcompat \ - libstdc++ + g++ -RUN apk add --no-cache bash curl && curl -1sLf \ - 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ - && apk add infisical=0.8.1 && apk add --no-cache git +# build and install SoftHSM2 +RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES} +WORKDIR ${SOFTHSM2_SOURCES} + +RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \ + && sh autogen.sh \ + && ./configure --prefix=/usr/local --disable-gost \ + && make \ + && make install +WORKDIR /root +RUN rm -fr ${SOFTHSM2_SOURCES} -ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ +# install pkcs11-tool +RUN apk --update add opensc +RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000 + +# ? App setup + +RUN apk add --no-cache bash curl && curl -1sLf \ + 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ + && apk add infisical=0.8.1 && apk add --no-cache git WORKDIR /app diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index fc78f2562a..9e56ae5891 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -92,7 +92,6 @@ services: - TELEMETRY_ENABLED=false volumes: - ./backend/src:/app/src - - "/etc/luna-docker:/usr/safenet/lunaclient" extra_hosts: - "host.docker.internal:host-gateway" From 98fd146e859b98e8fee92e066b9db95c275113c1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 8 Nov 2024 10:58:26 +0400 Subject: [PATCH 26/32] cleanup --- Dockerfile.standalone-infisical | 14 +++++++ docker-compose.hsm.prod.yml | 74 --------------------------------- 2 files changed, 14 insertions(+), 74 deletions(-) delete mode 100644 docker-compose.hsm.prod.yml diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 269cbfcf9c..1a6775c98c 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -72,6 +72,13 @@ RUN addgroup --system --gid 1001 nodejs \ WORKDIR /app +# Required for pkcs11js +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + COPY backend/package*.json ./ RUN npm ci --only-production @@ -85,6 +92,13 @@ FROM base AS backend-runner WORKDIR /app +# Required for pkcs11js +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + COPY backend/package*.json ./ RUN npm ci --only-production diff --git a/docker-compose.hsm.prod.yml b/docker-compose.hsm.prod.yml deleted file mode 100644 index 1c3a72ece8..0000000000 --- a/docker-compose.hsm.prod.yml +++ /dev/null @@ -1,74 +0,0 @@ -version: "3" - -services: - db-migration: - container_name: infisical-db-migration - depends_on: - db: - condition: service_healthy - image: infisical/infisical:latest-postgres - env_file: .env - command: npm run migration:latest - pull_policy: always - networks: - - infisical - - backend: - container_name: infisical-backend - restart: unless-stopped - depends_on: - db: - condition: service_healthy - redis: - condition: service_started - db-migration: - condition: service_completed_successfully - image: infisical/infisical:latest-postgres - pull_policy: always - env_file: .env - ports: - - 80:8080 - environment: - - NODE_ENV=production - volumes: - - /opt/infisical-hsm:/opt/infisical-hsm - networks: - - infisical - - redis: - image: redis - container_name: infisical-dev-redis - env_file: .env - restart: always - environment: - - ALLOW_EMPTY_PASSWORD=yes - ports: - - 6379:6379 - networks: - - infisical - volumes: - - redis_data:/data - - db: - container_name: infisical-db - image: postgres:14-alpine - restart: always - env_file: .env - volumes: - - pg_data:/var/lib/postgresql/data - networks: - - infisical - healthcheck: - test: "pg_isready --username=${POSTGRES_USER} && psql --username=${POSTGRES_USER} --list" - interval: 5s - timeout: 10s - retries: 10 - -volumes: - pg_data: - driver: local - redis_data: - driver: local - -networks: - infisical: From 14884cd6b02454694ef89e5dee0b16a60fdb9482 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 8 Nov 2024 21:23:29 +0400 Subject: [PATCH 27/32] Update Dockerfile.standalone-infisical --- Dockerfile.standalone-infisical | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 1a6775c98c..41c898b793 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -73,11 +73,7 @@ RUN addgroup --system --gid 1001 nodejs \ WORKDIR /app # Required for pkcs11js -RUN apt-get update && apt-get install -y \ - python3 \ - make \ - g++ \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache python3 make g++ COPY backend/package*.json ./ RUN npm ci --only-production @@ -93,11 +89,7 @@ FROM base AS backend-runner WORKDIR /app # Required for pkcs11js -RUN apt-get update && apt-get install -y \ - python3 \ - make \ - g++ \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache python3 make g++ COPY backend/package*.json ./ RUN npm ci --only-production From 7c055f71f706d68064ef2d943c49be84b2ec84c3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 8 Nov 2024 22:44:44 +0400 Subject: [PATCH 28/32] Update hsm-service.ts --- backend/src/ee/services/hsm/hsm-service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index 73c4b279cc..243a452769 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -221,7 +221,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm const encryptedLength = pkcs11.C_Encrypt(sessionHandle, data, tempBuffer); // Create a copy of the encrypted data using the actual length - const encryptedData = Buffer.from(tempBuffer.slice(0, encryptedLength.length || 16)); + const encryptedData = Buffer.from(tempBuffer.subarray(0, encryptedLength.length || 16)); // Initialize HMAC const hmacMechanism = { @@ -275,7 +275,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm // Split encrypted data and HMAC const hmac = encryptedDataWithHmac.subarray(-HMAC_SIZE); // Last 32 bytes are HMAC - const encryptedData = encryptedDataWithHmac.slice(0, -HMAC_SIZE); // Everything except last 32 bytes + const encryptedData = encryptedDataWithHmac.subarray(0, -HMAC_SIZE); // Everything except last 32 bytes // Find the keys const aesKey = $findKey(sessionHandle, HsmKeyType.AES); @@ -300,6 +300,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm try { pkcs11.C_VerifyFinal(sessionHandle, hmac); } catch (error) { + logger.error(error, "HSM: HMAC verification failed"); throw new Error("Decryption failed"); // Generic error for failed verification } From a1474145aeff1b0871cb5fd03ccbc71554c4b170 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 8 Nov 2024 23:52:38 +0400 Subject: [PATCH 29/32] Update hsm-service.ts --- backend/src/ee/services/hsm/hsm-service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index 243a452769..495d961dd1 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -301,7 +301,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm pkcs11.C_VerifyFinal(sessionHandle, hmac); } catch (error) { logger.error(error, "HSM: HMAC verification failed"); - throw new Error("Decryption failed"); // Generic error for failed verification + throw new Error("HSM: Decryption failed"); // Generic error for failed verification } // Only decrypt if verification passed @@ -318,8 +318,8 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm // Create a new buffer from the decrypted data return Buffer.from(decryptedData); } catch (error) { - logger.error("Decryption error:", error); - throw new Error(`Decryption failed: ${(error as Error)?.message}`); + logger.error(error, "HSM: Failed to perform decryption"); + throw new Error("HSM: Decryption failed"); // Generic error for failed decryption, to avoid leaking details about why it failed (such as padding related errors) } }; From 603fcd8ab51b5ad832de5441f68a6a08938e9b57 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 11 Nov 2024 18:48:14 +0400 Subject: [PATCH 30/32] Update hsm-service.ts --- backend/src/ee/services/hsm/hsm-service.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index 495d961dd1..6ff5665e16 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -23,6 +23,9 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm const BLOCK_SIZE = 16; const HMAC_SIZE = 32; + const AES_KEY_SIZE = 256; + const HMAC_KEY_SIZE = 256; + const $withSession = async (callbackWithSession: SessionCallback): Promise => { const RETRY_INTERVAL = 300; // 300ms between attempts const MAX_TIMEOUT = 30_000; // 30 seconds maximum total time @@ -215,13 +218,9 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm // Calculate max buffer size (input length + potential full block of padding) const maxEncryptedLength = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE; - const tempBuffer = Buffer.alloc(maxEncryptedLength); - - // First call to get the actual length - const encryptedLength = pkcs11.C_Encrypt(sessionHandle, data, tempBuffer); - // Create a copy of the encrypted data using the actual length - const encryptedData = Buffer.from(tempBuffer.subarray(0, encryptedLength.length || 16)); + // Encrypt the data - this returns the encrypted data directly + const encryptedData = pkcs11.C_Encrypt(sessionHandle, data, Buffer.alloc(maxEncryptedLength)); // Initialize HMAC const hmacMechanism = { @@ -396,7 +395,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm const keyTemplate = [ { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_AES }, - { type: pkcs11js.CKA_VALUE_LEN, value: 256 / 8 }, + { type: pkcs11js.CKA_VALUE_LEN, value: AES_KEY_SIZE / 8 }, { type: pkcs11js.CKA_LABEL, value: appCfg.HSM_KEY_LABEL! }, { type: pkcs11js.CKA_ENCRYPT, value: true }, // Allow encryption { type: pkcs11js.CKA_DECRYPT, value: true }, // Allow decryption @@ -420,7 +419,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm const hmacKeyTemplate = [ { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_GENERIC_SECRET }, - { type: pkcs11js.CKA_VALUE_LEN, value: 256 / 8 }, // 256-bit key + { type: pkcs11js.CKA_VALUE_LEN, value: HMAC_KEY_SIZE / 8 }, // 256-bit key { type: pkcs11js.CKA_LABEL, value: `${appCfg.HSM_KEY_LABEL!}_HMAC` }, { type: pkcs11js.CKA_SIGN, value: true }, // Allow signing { type: pkcs11js.CKA_VERIFY, value: true }, // Allow verification From 148f522c580cebaa6b17da6937e9a7839372c1c9 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 11 Nov 2024 21:52:35 +0400 Subject: [PATCH 31/32] updated migrations --- ...337_kms-root-cfg-hsm.ts => 20241111175154_kms-root-cfg-hsm.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/db/migrations/{20241028134337_kms-root-cfg-hsm.ts => 20241111175154_kms-root-cfg-hsm.ts} (100%) diff --git a/backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts b/backend/src/db/migrations/20241111175154_kms-root-cfg-hsm.ts similarity index 100% rename from backend/src/db/migrations/20241028134337_kms-root-cfg-hsm.ts rename to backend/src/db/migrations/20241111175154_kms-root-cfg-hsm.ts From f22a5580a653d35cd3c86ea61b44494c5c297031 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 12 Nov 2024 02:27:38 +0400 Subject: [PATCH 32/32] requested changes --- Dockerfile.fips.standalone-infisical | 12 ++-- backend/src/ee/services/hsm/hsm-service.ts | 57 +++++++++---------- backend/src/server/app.ts | 3 +- backend/src/server/routes/v1/admin-router.ts | 1 - .../super-admin/super-admin-service.ts | 4 +- frontend/src/hooks/api/admin/types.ts | 1 - .../admin/DashboardPage/DashboardPage.tsx | 10 ++-- .../admin/DashboardPage/EncryptionPanel.tsx | 53 ++++++++++------- 8 files changed, 73 insertions(+), 68 deletions(-) diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index 53c24db5d5..dfcb87deb3 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -3,7 +3,7 @@ ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id ARG CAPTCHA_SITE_KEY=captcha-site-key -FROM --platform=linux/amd64 node:20-slim AS base +FROM node:20-slim AS base FROM base AS frontend-dependencies WORKDIR /app @@ -14,7 +14,7 @@ COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./ RUN npm ci --only-production --ignore-scripts # Rebuild the source code only when needed -FROM --platform=linux/amd64 base AS frontend-builder +FROM base AS frontend-builder WORKDIR /app # Copy dependencies @@ -39,7 +39,7 @@ ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY RUN npm run build # Production image -FROM --platform=linux/amd64 base AS frontend-runner +FROM base AS frontend-runner WORKDIR /app RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user @@ -61,7 +61,7 @@ ENV NEXT_TELEMETRY_DISABLED 1 ## ## BACKEND ## -FROM --platform=linux/amd64 base AS backend-build +FROM base AS backend-build ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ @@ -85,7 +85,7 @@ RUN npm i -D tsconfig-paths RUN npm run build # Production stage -FROM --platform=linux/amd64 base AS backend-runner +FROM base AS backend-runner ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ @@ -106,7 +106,7 @@ COPY --from=backend-build /app . RUN mkdir frontend-build # Production stage -FROM --platform=linux/amd64 base AS production +FROM base AS production # Install necessary packages RUN apt-get update && apt-get install -y \ diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index 6ff5665e16..a1a0773fc4 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -27,8 +27,8 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm const HMAC_KEY_SIZE = 256; const $withSession = async (callbackWithSession: SessionCallback): Promise => { - const RETRY_INTERVAL = 300; // 300ms between attempts - const MAX_TIMEOUT = 30_000; // 30 seconds maximum total time + const RETRY_INTERVAL = 200; // 200ms between attempts + const MAX_TIMEOUT = 90_000; // 90 seconds maximum total time let sessionHandle: pkcs11js.Handle | null = null; @@ -39,7 +39,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm pkcs11.C_CloseSession(sessionHandle); logger.info("HSM: Terminated session successfully"); } catch (error) { - logger.error("Error during session cleanup:", error); + logger.error(error, "HSM: Failed to terminate session"); } finally { sessionHandle = null; } @@ -82,15 +82,15 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm logger.info("HSM: Successfully authenticated"); break; } catch (error) { + // Handle specific error cases if (error instanceof pkcs11js.Pkcs11Error) { - // Handle specific error cases if (error.code === pkcs11js.CKR_PIN_INCORRECT) { - logger.error(error, `Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`); - throw new Error("Incorrect HSM Pin detected. Please check the HSM configuration."); + // We throw instantly here to prevent further attempts, because if too many attempts are made, the HSM will potentially wipe all key material + logger.error(error, `HSM: Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`); + throw new Error("HSM: Incorrect HSM Pin detected. Please check the HSM configuration."); } - if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) { - logger.warn("HSM session already logged in"); + logger.warn("HSM: Session already logged in"); } } throw error; // Re-throw other errors @@ -102,7 +102,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm try { pkcs11.C_CloseSession(sessionHandle); } catch (closeError) { - logger.error("Error closing failed session:", closeError); + logger.error(closeError, "HSM: Failed to close session"); } sessionHandle = null; } @@ -116,7 +116,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm } if (sessionHandle === null) { - throw new Error("Failed to open session after maximum retries"); + throw new Error("HSM: Failed to open session after maximum retries"); } // Execute callback with session handle @@ -124,7 +124,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm removeSession(); return result; } catch (error) { - logger.error("Error in HSM session handling:", error); + logger.error(error, "HSM: Failed to open session"); throw error; } finally { // Ensure cleanup @@ -160,7 +160,6 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm pkcs11.C_FindObjectsFinal(sessionHandle); } } catch (error) { - logger.error("Error finding master key:", error); return null; } }; @@ -174,7 +173,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm } catch (error) { // If items(0) throws, it means no key was found // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call - logger.error(error, "Error checking for HSM key presence"); + logger.error(error, "HSM: Failed while checking for HSM key presence"); if (error instanceof pkcs11js.Pkcs11Error) { if (error.code === pkcs11js.CKR_OBJECT_HANDLE_INVALID) { @@ -198,12 +197,12 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm try { const aesKey = $findKey(sessionHandle, HsmKeyType.AES); if (!aesKey) { - throw new Error("AES key not found"); + throw new Error("HSM: Encryption failed, AES key not found"); } const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC); if (!hmacKey) { - throw new Error("HMAC key not found"); + throw new Error("HSM: Encryption failed, HMAC key not found"); } const iv = Buffer.alloc(IV_LENGTH); @@ -244,8 +243,8 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm return Buffer.concat([iv, finalBuffer]); } catch (error) { - logger.error("Encryption error:", error); - throw new Error(`Encryption failed: ${(error as Error)?.message}`); + logger.error(error, "HSM: Failed to perform encryption"); + throw new Error(`HSM: Encryption failed: ${(error as Error)?.message}`); } }; @@ -261,8 +260,8 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm (encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise; (encryptedBlob: Buffer): Promise; } = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => { - if (!isInitialized) { - throw new Error("HSM service not initialized"); + if (!pkcs11 || !isInitialized) { + throw new Error("PKCS#11 module is not initialized"); } const $performDecryption = (sessionHandle: pkcs11js.Handle) => { @@ -279,12 +278,12 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm // Find the keys const aesKey = $findKey(sessionHandle, HsmKeyType.AES); if (!aesKey) { - throw new Error("AES key not found"); + throw new Error("HSM: Decryption failed, AES key not found"); } const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC); if (!hmacKey) { - throw new Error("HMAC key not found"); + throw new Error("HSM: Decryption failed, HMAC key not found"); } // Verify HMAC first @@ -333,12 +332,12 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm // We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device. const $testPkcs11Module = async (session: pkcs11js.Handle) => { try { - if (!isInitialized) { - throw new Error("HSM service not initialized"); + if (!pkcs11 || !isInitialized) { + throw new Error("PKCS#11 module is not initialized"); } if (!session) { - throw new Error("Session not initialized"); + throw new Error("HSM: Attempted to run test without a valid session"); } const randomData = pkcs11.C_GenerateRandom(session, Buffer.alloc(500)); @@ -350,12 +349,12 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm const decryptedDataHex = decryptedData.toString("hex"); if (randomDataHex !== decryptedDataHex && Buffer.compare(randomData, decryptedData)) { - throw new Error("Decrypted data does not match original data"); + throw new Error("HSM: Startup test failed. Decrypted data does not match original data"); } return true; } catch (error) { - logger.error(error, "Error testing PKCS#11 module"); + logger.error(error, "HSM: Error testing PKCS#11 module"); return false; } }; @@ -411,7 +410,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm keyTemplate ); - logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); + logger.info(`HSM: Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); } // Check if HMAC key exists, create if not @@ -435,7 +434,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm hmacKeyTemplate ); - logger.info(`HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`); + logger.info(`HSM: HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`); } // Get slot info to check supported mechanisms @@ -457,7 +456,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm } }); } catch (error) { - logger.error("Error initializing HSM service:", error); + logger.error(error, "HSM: Error initializing HSM service:"); throw error; } }; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 3a22aafba0..6484860ea7 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -43,10 +43,11 @@ type TMain = { // Run the server! export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore }: TMain) => { const appCfg = getConfig(); + const server = fastify({ logger: appCfg.NODE_ENV === "test" ? false : logger, trustProxy: true, - connectionTimeout: 30 * 1000, + connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000, ignoreTrailingSlash: true, pluginTimeout: 40_000 }).withTypeProvider(); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 9891f58e50..6ecebb274c 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -208,7 +208,6 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { strategies: z .object({ strategy: z.nativeEnum(RootKeyEncryptionStrategy), - name: z.string(), enabled: z.boolean() }) .array() diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 370890a09d..8ef998ac3c 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -303,13 +303,12 @@ export const superAdminServiceFactory = ({ } const selectedStrategy = kmsRootCfg.encryptionStrategy; - const enabledStrategies: { enabled: boolean; strategy: RootKeyEncryptionStrategy; name: string }[] = []; + const enabledStrategies: { enabled: boolean; strategy: RootKeyEncryptionStrategy }[] = []; if (appCfg.ROOT_ENCRYPTION_KEY || appCfg.ENCRYPTION_KEY) { const basicStrategy = RootKeyEncryptionStrategy.Software; enabledStrategies.push({ - name: "Software-based Encryption", enabled: selectedStrategy === basicStrategy, strategy: basicStrategy }); @@ -318,7 +317,6 @@ export const superAdminServiceFactory = ({ const hsmStrategy = RootKeyEncryptionStrategy.HSM; enabledStrategies.push({ - name: "Hardware Security Module (HSM)", enabled: selectedStrategy === hsmStrategy, strategy: hsmStrategy }); diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 7d35f70bed..60fa3ab988 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -59,7 +59,6 @@ export type TGetServerRootKmsEncryptionDetails = { strategies: { strategy: RootKeyEncryptionStrategy; enabled: boolean; - name: string; }[]; }; diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index 67c2a84ca1..ac87596ae2 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -144,7 +144,7 @@ export const AdminDashboardPage = () => {
General - {!!serverRootKmsDetails && Encryption} + Encryption Authentication Rate Limit Integrations @@ -329,11 +329,9 @@ export const AdminDashboardPage = () => { - {!!serverRootKmsDetails && ( - - - - )} + + + diff --git a/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx b/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx index 8be1982d83..0383423f89 100644 --- a/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx +++ b/frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx @@ -17,10 +17,15 @@ const formSchema = z.object({ encryptionStrategy: z.nativeEnum(RootKeyEncryptionStrategy) }); +const strategies: Record = { + [RootKeyEncryptionStrategy.Software]: "Software-based Encryption", + [RootKeyEncryptionStrategy.HSM]: "Hardware Security Module (HSM)" +}; + type TForm = z.infer; type Props = { - rootKmsDetails: TGetServerRootKmsEncryptionDetails; + rootKmsDetails?: TGetServerRootKmsEncryptionDetails; }; export const EncryptionPanel = ({ rootKmsDetails }: Props) => { @@ -84,27 +89,33 @@ export const EncryptionPanel = ({ rootKmsDetails }: Props) => { supported on Enterprise plans.
- ( - - - - )} - /> + + + )} + /> + )}