diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4c7aee7ff..0ef53e14e 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 2208a7295..a2dd14bc7 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; +type SyncOrAsync = T | Promise; +type SessionCallback = (session: grapheneLib.Session) => SyncOrAsync; - private lastUsed: number = 0; +export const withSession = async ( + { module, graphene }: HsmModule, + callbackWithSession: SessionCallback +): Promise => { + const appCfg = getConfig(); - 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(); - } + let session: grapheneLib.Session | null = null; + try { + if (!module) { + throw new Error("PKCS#11 module is not initialized"); } // Create new session - const slot = this.module.getSlots(appCfg.HSM_SLOT); + const slot = module.getSlots(appCfg.HSM_SLOT); // eslint-disable-next-line no-bitwise - if (!(slot.flags & this.graphene.SlotFlag.TOKEN_PRESENT)) { + if (!(slot.flags & 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; + 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); + }); } - this.session = session; - this.lastUsed = Date.now(); + if (!session) { + throw new Error("Failed to open session"); + } - return 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); + } + } } - - 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 $performEncryption = (s: grapheneLib.Session) => { + // Generate IV for encryption + const iv = s.generateRandom(IV_LENGTH); + + // Generate and wrap a new session key + const { wrappedKey, sessionKey } = $generateAndWrapKey(s); + + const alg = { + name: appCfg.HSM_MECHANISM, + params: new graphene.AesGcm240Params(iv) + } as grapheneLib.IAlgorithm; + + const cipher = s.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); + + // Format: [Wrapped Key (40)][IV (16)][Encrypted Data + Tag] + return Buffer.concat([wrappedKey, iv, encryptedData]); + }; + + if (providedSession) { + return $performEncryption(providedSession); } - const session = sessionManager.getSession(); - // Generate IV for encryption - const iv = session.generateRandom(IV_LENGTH); + const encrypted = await withSession({ module, graphene }, $performEncryption); - // 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(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); - - // Format: [Wrapped Key (40)][IV (16)][Encrypted Data + Tag] - return Buffer.concat([wrappedKey, iv, encryptedData]); + return encrypted; }; - const decrypt = (encryptedBlob: Buffer) => { - const WRAPPED_KEY_LENGTH = 32 + 8; // AES-256 key + padding - - 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"}`); + if (!session) { + throw new Error("Session not initialized"); } - 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"}`); - } + 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}`); - } + 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[] = []; + // 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); - } + // 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); + 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`); - } + 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."); - } + 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 102333038..92b909870 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");