From d4652e69cebfbb85d3b17165dcaaa8836694d2e8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 7 Jul 2025 09:47:02 +0400 Subject: [PATCH] feat: fips inside (checkpoint) --- backend/e2e-test/vitest-environment-knex.ts | 25 +++-- backend/src/lib/config/env.ts | 19 +++- backend/src/lib/crypto/cipher/cipher.ts | 1 + backend/src/lib/crypto/cryptography.ts | 105 ++++++++----------- backend/src/lib/errors/index.ts | 12 +++ backend/src/lib/telemetry/instrumentation.ts | 28 ++--- backend/src/main.ts | 39 +++---- backend/src/server/plugins/error-handler.ts | 8 ++ 8 files changed, 131 insertions(+), 106 deletions(-) diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 6e1f0ff49..2b4559022 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -26,15 +26,18 @@ export default { transformMode: "ssr", async setup() { const logger = initLogger(); - const envConfig = initEnvConfig(logger); + const { envCfg, updateRootEncryptionKey } = initEnvConfig(logger); const db = initDbConnection({ - dbConnectionUri: envConfig.DB_CONNECTION_URI, - dbRootCert: envConfig.DB_ROOT_CERT + dbConnectionUri: envCfg.DB_CONNECTION_URI, + dbRootCert: envCfg.DB_ROOT_CERT }); const superAdminDAL = superAdminDALFactory(db); - await crypto.initialize(superAdminDAL); + const fipsEnabled = await crypto.initialize(superAdminDAL); + if (fipsEnabled) { + updateRootEncryptionKey(envCfg.ENCRYPTION_KEY); + } - const redis = buildRedisFromConfig(envConfig); + const redis = buildRedisFromConfig(envCfg); await redis.flushdb("SYNC"); try { @@ -59,10 +62,10 @@ export default { }); const smtp = mockSmtpServer(); - const queue = queueServiceFactory(envConfig, { dbConnectionUrl: envConfig.DB_CONNECTION_URI }); - const keyStore = keyStoreFactory(envConfig); + const queue = queueServiceFactory(envCfg, { dbConnectionUrl: envCfg.DB_CONNECTION_URI }); + const keyStore = keyStoreFactory(envCfg); - const hsmModule = initializeHsmModule(envConfig); + const hsmModule = initializeHsmModule(envCfg); hsmModule.initialize(); const server = await main({ @@ -74,7 +77,7 @@ export default { hsmModule: hsmModule.getModule(), superAdminDAL, redis, - envConfig + envConfig: envCfg }); // @ts-expect-error type @@ -91,8 +94,8 @@ export default { organizationId: seedData1.organization.id, accessVersion: 1 }, - envConfig.AUTH_SECRET, - { expiresIn: envConfig.JWT_AUTH_LIFETIME } + envCfg.AUTH_SECRET, + { expiresIn: envCfg.JWT_AUTH_LIFETIME } ); } catch (error) { // eslint-disable-next-line diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 205ae3ed5..818b3e254 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -353,8 +353,23 @@ export const initEnvConfig = (logger?: CustomLogger) => { process.exit(-1); } - envCfg = Object.freeze(parsedEnv.data); - return envCfg; + const updateRootEncryptionKey = (key?: string) => { + if (!key) { + throw new Error("Failed to update root encryption key. Key is unset."); + } + + const newEnvCfg = { + ...envCfg + }; + + newEnvCfg.ROOT_ENCRYPTION_KEY = key; + delete newEnvCfg.ENCRYPTION_KEY; + + envCfg = Object.freeze(newEnvCfg); + return envCfg; + }; + + return { envCfg, updateRootEncryptionKey }; }; export const formatSmtpConfig = () => { diff --git a/backend/src/lib/crypto/cipher/cipher.ts b/backend/src/lib/crypto/cipher/cipher.ts index e193c8c3f..ca5890f87 100644 --- a/backend/src/lib/crypto/cipher/cipher.ts +++ b/backend/src/lib/crypto/cipher/cipher.ts @@ -5,6 +5,7 @@ import { SymmetricKeyAlgorithm, TSymmetricEncryptionFns } from "./types"; const IV_LENGTH = 12; const TAG_LENGTH = 16; +// todo(daniel): Decide if we should move this into the cryptography module export const symmetricCipherService = ( type: SymmetricKeyAlgorithm.AES_GCM_128 | SymmetricKeyAlgorithm.AES_GCM_256 ): TSymmetricEncryptionFns => { diff --git a/backend/src/lib/crypto/cryptography.ts b/backend/src/lib/crypto/cryptography.ts index 8486837cb..036048c45 100644 --- a/backend/src/lib/crypto/cryptography.ts +++ b/backend/src/lib/crypto/cryptography.ts @@ -11,7 +11,9 @@ import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service"; +import { isBase64 } from "../base64"; import { getConfig } from "../config/env"; +import { CryptographyError } from "../errors"; import { logger } from "../logger"; enum DigestType { @@ -59,6 +61,8 @@ type TDecryptAsymmetricInput = { privateKey: string; }; +const bytesToBits = (bytes: number) => bytes * 8; + const IV_BYTES_SIZE = 12; const BLOCK_SIZE_BYTES_16 = 16; @@ -294,6 +298,33 @@ const cryptographyFactory = () => { return $fipsEnabled; }; const $setFipsModeEnabled = (enabled: boolean) => { + // If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key. + if (enabled) { + const appCfg = getConfig(); + + if (appCfg.ENCRYPTION_KEY) { + // we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key + + if (!isBase64(appCfg.ENCRYPTION_KEY)) { + throw new CryptographyError({ + message: + "FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not a base64 encoded 256-bit key.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`" + }); + } + + if (bytesToBits(Buffer.from(appCfg.ENCRYPTION_KEY, "base64").length) !== 256) { + throw new CryptographyError({ + message: + "FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not a 256-bit key.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`" + }); + } + } else { + throw new CryptographyError({ + message: + "FIPS mode is enabled, but the ENCRYPTION_KEY environment variable is not set.\nYou can generate a 256-bit key using the following command: `openssl rand -base64 32`" + }); + } + } $fipsEnabled = enabled; $isInitialized = true; }; @@ -337,52 +368,22 @@ const cryptographyFactory = () => { const asymmetric = () => { const generateKeyPair = () => { if (isFipsModeEnabled()) { - logger.info("[FIPS]: Generating asymmetric key pair. FIPS mode is enabled."); - logger.info("[FIPS]: Generating asymmetric key pair. FIPS mode is enabled."); - logger.info("[FIPS]: Generating asymmetric key pair. FIPS mode is enabled."); - logger.info("[FIPS]: Generating asymmetric key pair. FIPS mode is enabled."); - logger.info("[FIPS]: Generating asymmetric key pair. FIPS mode is enabled."); return generateAsymmetricKeyPairFipsValidated(); } - logger.info("[FIPS]: Generating asymmetric key pair. FIPS mode is DISABLED."); - logger.info("[FIPS]: Generating asymmetric key pair. FIPS mode is DISABLED."); - logger.info("[FIPS]: Generating asymmetric key pair. FIPS mode is DISABLED."); - logger.info("[FIPS]: Generating asymmetric key pair. FIPS mode is DISABLED."); - logger.info("[FIPS]: Generating asymmetric key pair. FIPS mode is DISABLED."); return generateAsymmetricKeyPairNoFipsValidation(); }; const encrypt = (data: string, publicKey: string, privateKey: string) => { if (isFipsModeEnabled()) { - logger.info("[FIPS]: Encrypting asymmetric data. FIPS mode is enabled."); - logger.info("[FIPS]: Encrypting asymmetric data. FIPS mode is enabled."); - logger.info("[FIPS]: Encrypting asymmetric data. FIPS mode is enabled."); - logger.info("[FIPS]: Encrypting asymmetric data. FIPS mode is enabled."); - logger.info("[FIPS]: Encrypting asymmetric data. FIPS mode is enabled."); return encryptAsymmetricFipsValidated(data, publicKey, privateKey); } - logger.info("[FIPS]: Encrypting asymmetric data. FIPS mode is DISABLED."); - logger.info("[FIPS]: Encrypting asymmetric data. FIPS mode is DISABLED."); - logger.info("[FIPS]: Encrypting asymmetric data. FIPS mode is DISABLED."); - logger.info("[FIPS]: Encrypting asymmetric data. FIPS mode is DISABLED."); - logger.info("[FIPS]: Encrypting asymmetric data. FIPS mode is DISABLED."); return encryptAsymmetricNoFipsValidation(data, publicKey, privateKey); }; const decrypt = ({ ciphertext, nonce, publicKey, privateKey }: TDecryptAsymmetricInput) => { if (isFipsModeEnabled()) { - logger.info("[FIPS]: Decrypting asymmetric data. FIPS mode is enabled."); - logger.info("[FIPS]: Decrypting asymmetric data. FIPS mode is enabled."); - logger.info("[FIPS]: Decrypting asymmetric data. FIPS mode is enabled."); - logger.info("[FIPS]: Decrypting asymmetric data. FIPS mode is enabled."); - logger.info("[FIPS]: Decrypting asymmetric data. FIPS mode is enabled."); return decryptAsymmetricFipsValidated({ ciphertext, nonce, publicKey, privateKey }); } - logger.info("[FIPS]: Decrypting asymmetric data. FIPS mode is DISABLED."); - logger.info("[FIPS]: Decrypting asymmetric data. FIPS mode is DISABLED."); - logger.info("[FIPS]: Decrypting asymmetric data. FIPS mode is DISABLED."); - logger.info("[FIPS]: Decrypting asymmetric data. FIPS mode is DISABLED."); - logger.info("[FIPS]: Decrypting asymmetric data. FIPS mode is DISABLED."); return decryptAsymmetricNoFipsValidation({ ciphertext, nonce, publicKey, privateKey }); }; @@ -397,6 +398,12 @@ const cryptographyFactory = () => { let decipher; if (keySize === SymmetricKeySize.Bits128) { + if (isFipsModeEnabled()) { + throw new CryptographyError({ + message: "128-bit symmetric key is not supported in FIPS mode of operation." + }); + } + // Not ideal: 128-bit hex key (32 chars) gets interpreted as 32 UTF-8 bytes (256 bits) // This works but reduces effective key entropy from 256 to 128 bits // Note: Never use this for FIPS mode of operation. @@ -418,6 +425,12 @@ const cryptographyFactory = () => { let cipher; if (keySize === SymmetricKeySize.Bits128) { + if (isFipsModeEnabled()) { + throw new CryptographyError({ + message: "128-bit symmetric key is not supported in FIPS mode of operation." + }); + } + iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16); cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv); } else { @@ -478,21 +491,6 @@ const cryptographyFactory = () => { }: Omit & { keyEncoding: SecretKeyEncoding; }) => { - logger.info( - `[FIPS]: decryptWithRootEncryptionKey -> Decrypting symmetric data. FIPS mode is: ${isFipsModeEnabled()}` - ); - logger.info( - `[FIPS]: decryptWithRootEncryptionKey -> Decrypting symmetric data. FIPS mode is: ${isFipsModeEnabled()}` - ); - logger.info( - `[FIPS]: decryptWithRootEncryptionKey -> Decrypting symmetric data. FIPS mode is: ${isFipsModeEnabled()}` - ); - logger.info( - `[FIPS]: decryptWithRootEncryptionKey -> Decrypting symmetric data. FIPS mode is: ${isFipsModeEnabled()}` - ); - logger.info( - `[FIPS]: decryptWithRootEncryptionKey -> Decrypting symmetric data. FIPS mode is: ${isFipsModeEnabled()}` - ); const appCfg = getConfig(); // the or gate is used used in migration const rootEncryptionKey = appCfg?.ROOT_ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY; @@ -530,11 +528,6 @@ const cryptographyFactory = () => { * @deprecated Do not use MD5 unless you absolutely have to. It is considered an unsafe hashing algorithm, and should only be used if absolutely necessary. */ const md5 = (message: string, digest: DigestType = DigestType.Hex) => { - logger.info(`[FIPS]: md5 -> Hashing message. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: md5 -> Hashing message. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: md5 -> Hashing message. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: md5 -> Hashing message. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: md5 -> Hashing message. FIPS mode is: ${isFipsModeEnabled()}`); // If FIPS is enabled and we need MD5, we use the crypto-js implementation. // Avoid this at all costs unless strictly necessary, like for mongo atlas digest auth. if (isFipsModeEnabled()) { @@ -544,12 +537,6 @@ const cryptographyFactory = () => { }; const createHash = async (password: string, saltRounds: number) => { - logger.info(`[FIPS]: createHash -> Hashing password. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: createHash -> Hashing password. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: createHash -> Hashing password. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: createHash -> Hashing password. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: createHash -> Hashing password. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: createHash -> Hashing password. FIPS mode is: ${isFipsModeEnabled()}`); if (isFipsModeEnabled()) { const hasher = hasherFipsValidated(); @@ -566,12 +553,6 @@ const cryptographyFactory = () => { }; const compareHash = async (password: string, hash: string) => { - logger.info(`[FIPS]: compareHash -> Comparing password. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: compareHash -> Comparing password. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: compareHash -> Comparing password. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: compareHash -> Comparing password. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: compareHash -> Comparing password. FIPS mode is: ${isFipsModeEnabled()}`); - logger.info(`[FIPS]: compareHash -> Comparing password. FIPS mode is: ${isFipsModeEnabled()}`); if (isFipsModeEnabled()) { const isValid = await hasherFipsValidated().compare(password, hash); return isValid; diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index a5df64caf..dab9d3278 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -171,3 +171,15 @@ export class OidcAuthError extends Error { this.error = error; } } + +export class CryptographyError extends Error { + name: string; + + error: unknown; + + constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) { + super(message || "Cryptographic operation failed"); + this.name = name || "CryptographyError"; + this.error = error; + } +} diff --git a/backend/src/lib/telemetry/instrumentation.ts b/backend/src/lib/telemetry/instrumentation.ts index faa7560d3..1cb35e143 100644 --- a/backend/src/lib/telemetry/instrumentation.ts +++ b/backend/src/lib/telemetry/instrumentation.ts @@ -75,27 +75,29 @@ const initTelemetryInstrumentation = ({ }; const setupTelemetry = () => { - const appCfg = initEnvConfig(); + const { envCfg } = initEnvConfig(); - if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + console.log("envCfg", envCfg); + + if (envCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { console.log("Initializing telemetry instrumentation"); initTelemetryInstrumentation({ - otlpURL: appCfg.OTEL_EXPORT_OTLP_ENDPOINT, - otlpUser: appCfg.OTEL_COLLECTOR_BASIC_AUTH_USERNAME, - otlpPassword: appCfg.OTEL_COLLECTOR_BASIC_AUTH_PASSWORD, - otlpPushInterval: appCfg.OTEL_OTLP_PUSH_INTERVAL, - exportType: appCfg.OTEL_EXPORT_TYPE + otlpURL: envCfg.OTEL_EXPORT_OTLP_ENDPOINT, + otlpUser: envCfg.OTEL_COLLECTOR_BASIC_AUTH_USERNAME, + otlpPassword: envCfg.OTEL_COLLECTOR_BASIC_AUTH_PASSWORD, + otlpPushInterval: envCfg.OTEL_OTLP_PUSH_INTERVAL, + exportType: envCfg.OTEL_EXPORT_TYPE }); } - if (appCfg.SHOULD_USE_DATADOG_TRACER) { + if (envCfg.SHOULD_USE_DATADOG_TRACER) { console.log("Initializing Datadog tracer"); tracer.init({ - profiling: appCfg.DATADOG_PROFILING_ENABLED, - version: appCfg.INFISICAL_PLATFORM_VERSION, - env: appCfg.DATADOG_ENV, - service: appCfg.DATADOG_SERVICE, - hostname: appCfg.DATADOG_HOSTNAME + profiling: envCfg.DATADOG_PROFILING_ENABLED, + version: envCfg.INFISICAL_PLATFORM_VERSION, + env: envCfg.DATADOG_ENV, + service: envCfg.DATADOG_SERVICE, + hostname: envCfg.DATADOG_HOSTNAME }); } }; diff --git a/backend/src/main.ts b/backend/src/main.ts index 15dd0322b..78cd05acf 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -22,26 +22,29 @@ dotenv.config(); const run = async () => { const logger = initLogger(); - const envConfig = initEnvConfig(logger); + const { envCfg, updateRootEncryptionKey } = initEnvConfig(logger); await removeTemporaryBaseDirectory(); const db = initDbConnection({ - dbConnectionUri: envConfig.DB_CONNECTION_URI, - dbRootCert: envConfig.DB_ROOT_CERT, - readReplicas: envConfig.DB_READ_REPLICAS?.map((el) => ({ + dbConnectionUri: envCfg.DB_CONNECTION_URI, + dbRootCert: envCfg.DB_ROOT_CERT, + readReplicas: envCfg.DB_READ_REPLICAS?.map((el) => ({ dbRootCert: el.DB_ROOT_CERT, dbConnectionUri: el.DB_CONNECTION_URI })) }); const superAdminDAL = superAdminDALFactory(db); - await crypto.initialize(superAdminDAL); + const fipsEnabled = await crypto.initialize(superAdminDAL); + if (fipsEnabled) { + updateRootEncryptionKey(envCfg.ENCRYPTION_KEY); + } - const auditLogDb = envConfig.AUDIT_LOGS_DB_CONNECTION_URI + const auditLogDb = envCfg.AUDIT_LOGS_DB_CONNECTION_URI ? initAuditLogDbConnection({ - dbConnectionUri: envConfig.AUDIT_LOGS_DB_CONNECTION_URI, - dbRootCert: envConfig.AUDIT_LOGS_DB_ROOT_CERT + dbConnectionUri: envCfg.AUDIT_LOGS_DB_CONNECTION_URI, + dbRootCert: envCfg.AUDIT_LOGS_DB_ROOT_CERT }) : undefined; @@ -49,17 +52,17 @@ const run = async () => { const smtp = smtpServiceFactory(formatSmtpConfig()); - const queue = queueServiceFactory(envConfig, { - dbConnectionUrl: envConfig.DB_CONNECTION_URI, - dbRootCert: envConfig.DB_ROOT_CERT + const queue = queueServiceFactory(envCfg, { + dbConnectionUrl: envCfg.DB_CONNECTION_URI, + dbRootCert: envCfg.DB_ROOT_CERT }); await queue.initialize(); - const keyStore = keyStoreFactory(envConfig); - const redis = buildRedisFromConfig(envConfig); + const keyStore = keyStoreFactory(envCfg); + const redis = buildRedisFromConfig(envCfg); - const hsmModule = initializeHsmModule(envConfig); + const hsmModule = initializeHsmModule(envCfg); hsmModule.initialize(); const server = await main({ @@ -72,7 +75,7 @@ const run = async () => { queue, keyStore, redis, - envConfig + envConfig: envCfg }); const bootstrap = await bootstrapCheck({ db }); @@ -96,7 +99,7 @@ const run = async () => { process.exit(0); }); - if (!envConfig.isDevelopmentMode) { + if (!envCfg.isDevelopmentMode) { process.on("uncaughtException", (error) => { logger.error(error, "CRITICAL ERROR: Uncaught Exception"); }); @@ -107,8 +110,8 @@ const run = async () => { } await server.listen({ - port: envConfig.PORT, - host: envConfig.HOST, + port: envCfg.PORT, + host: envCfg.HOST, listenTextResolver: (address) => { void bootstrap(); return address; diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index c8170a023..62df05eec 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -7,6 +7,7 @@ import { ZodError } from "zod"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, + CryptographyError, DatabaseError, ForbiddenRequestError, GatewayTimeoutError, @@ -147,6 +148,13 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider message: error.message, error: error.name }); + } else if (error instanceof CryptographyError) { + void res.status(HttpStatusCodes.BadRequest).send({ + reqId: req.id, + statusCode: HttpStatusCodes.BadRequest, + message: error.message, + error: error.name + }); } else if (error instanceof jwt.JsonWebTokenError) { let errorMessage = error.message;