diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index 253065138..fe273b3ee 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,80 +22,99 @@ export type THsmServiceFactory = ReturnType; type SyncOrAsync = T | Promise; type SessionCallback = (session: grapheneLib.Session) => SyncOrAsync; -export const withSession = async ( - { module, graphene }: HsmModule, - callbackWithSession: SessionCallback -): Promise => { - const appCfg = getConfig(); - - let session: grapheneLib.Session | null = null; - try { - if (!module) { - 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"); - } - - 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"); - session = null; - } - - if (session) { - break; - } - - 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; - - setTimeout(resolve, sleepAmount); - }); - } - - 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); - 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); - } - } - } -}; - // eslint-disable-next-line no-empty-pattern -export const hsmServiceFactory = ({ hsmModule: { module, graphene } }: THsmServiceFactoryDep) => { +export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: THsmServiceFactoryDep) => { const appCfg = getConfig(); // Constants for buffer structure const IV_LENGTH = 12; const TAG_LENGTH = 16; + 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; + + const removeSession = () => { + if (session) { + session.logout(); + session.close(); + session = null; + } + }; + + try { + if (!module) { + 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"); + } + + lock = await keyStore.acquireLock(["HSM_SESSION_LOCK"], 10_000, { retryCount: 3 }).catch(() => null); + + 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...") + }); + } + + 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 { + removeSession(); + } catch (error) { + logger.error(error, "Error cleaning up HSM session:"); + } + + await lock?.release(); + } + }; + const $findMasterKey = (session: grapheneLib.Session) => { // Find the master key (root key) const template = { @@ -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 fa2c9910a..3a22aafba 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 88bfaaaae..065b3f617 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({