From ce88b0cbb1bff62d16dfaf4e694123c689b11b05 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 7 Jul 2025 18:16:53 +0400 Subject: [PATCH] feat(fips): fips inside --- .infisicalignore | 1 + backend/e2e-test/vitest-environment-knex.ts | 15 +- .../20250210101840_webhook-to-kms.ts | 6 +- ...250210101841_dynamic-secret-root-to-kms.ts | 5 +- .../20250210101841_secret-rotation-to-kms.ts | 5 +- .../20250210101842_identity-k8-auth-to-kms.ts | 5 +- ...0250210101842_identity-oidc-auth-to-kms.ts | 5 +- .../20250210101845_directory-config-to-kms.ts | 10 +- ...50513081738_remove-gateway-project-link.ts | 4 +- backend/src/db/migrations/utils/env-config.ts | 20 ++- .../src/ee/services/license/license-fns.ts | 3 +- .../src/ee/services/license/license-types.ts | 1 + backend/src/lib/config/env.ts | 52 +++++- backend/src/lib/crypto/cryptography.ts | 148 +++++++++++++----- backend/src/lib/telemetry/instrumentation.ts | 6 +- backend/src/main.ts | 47 +++--- backend/src/server/routes/index.ts | 8 +- backend/src/services/kms/kms-service.ts | 1 + .../super-admin/super-admin-service.ts | 2 + 19 files changed, 244 insertions(+), 100 deletions(-) diff --git a/.infisicalignore b/.infisicalignore index d705c0d66..4c50aa7fa 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -46,3 +46,4 @@ cli/detect/config/gitleaks.toml:gcp-api-key:582 .github/workflows/helm-release-infisical-core.yml:generic-api-key:47 backend/src/services/smtp/smtp-service.ts:generic-api-key:79 frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx:cloudflare-api-key:7 +docs/integrations/app-connections/zabbix.mdx:generic-api-key:91 diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 2b4559022..74276001d 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -6,7 +6,7 @@ import jwt from "jsonwebtoken"; import path from "path"; import { seedData1 } from "@app/db/seed-data"; -import { initEnvConfig } from "@app/lib/config/env"; +import { getDatabaseCredentials, initEnvConfig } from "@app/lib/config/env"; import { initLogger } from "@app/lib/logger"; import { main } from "@app/server/app"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; @@ -26,16 +26,15 @@ export default { transformMode: "ssr", async setup() { const logger = initLogger(); - const { envCfg, updateRootEncryptionKey } = initEnvConfig(logger); + const databaseCredentials = getDatabaseCredentials(logger); + const db = initDbConnection({ - dbConnectionUri: envCfg.DB_CONNECTION_URI, - dbRootCert: envCfg.DB_ROOT_CERT + dbConnectionUri: databaseCredentials.dbConnectionUri, + dbRootCert: databaseCredentials.dbRootCert }); + const superAdminDAL = superAdminDALFactory(db); - const fipsEnabled = await crypto.initialize(superAdminDAL); - if (fipsEnabled) { - updateRootEncryptionKey(envCfg.ENCRYPTION_KEY); - } + const envCfg = await initEnvConfig(superAdminDAL, logger); const redis = buildRedisFromConfig(envCfg); await redis.flushdb("SYNC"); diff --git a/backend/src/db/migrations/20250210101840_webhook-to-kms.ts b/backend/src/db/migrations/20250210101840_webhook-to-kms.ts index ff2503b32..63f1874e3 100644 --- a/backend/src/db/migrations/20250210101840_webhook-to-kms.ts +++ b/backend/src/db/migrations/20250210101840_webhook-to-kms.ts @@ -4,6 +4,7 @@ import { inMemoryKeyStore } from "@app/keystore/memory"; import { crypto } from "@app/lib/crypto/cryptography"; import { initLogger } from "@app/lib/logger"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName } from "../schemas"; import { getMigrationEnvConfig } from "./utils/env-config"; @@ -26,9 +27,12 @@ export async function up(knex: Knex): Promise { } initLogger(); - const envConfig = getMigrationEnvConfig(); + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); + const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const projectEncryptionRingBuffer = createCircularCache>>(25); const webhooks = await knex(TableName.Webhook) diff --git a/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts b/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts index e01fbb1db..ac0395111 100644 --- a/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts +++ b/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts @@ -5,6 +5,7 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName } from "../schemas"; import { getMigrationEnvConfig } from "./utils/env-config"; @@ -29,7 +30,9 @@ export async function up(knex: Knex): Promise { } initLogger(); - const envConfig = getMigrationEnvConfig(); + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); + const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const projectEncryptionRingBuffer = diff --git a/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts b/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts index 661d92f14..b8e23e757 100644 --- a/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts +++ b/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts @@ -5,6 +5,7 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName } from "../schemas"; import { getMigrationEnvConfig } from "./utils/env-config"; @@ -23,7 +24,9 @@ export async function up(knex: Knex): Promise { } initLogger(); - const envConfig = getMigrationEnvConfig(); + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); + const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const projectEncryptionRingBuffer = diff --git a/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts b/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts index 36c70c704..23a8c0e9b 100644 --- a/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts +++ b/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts @@ -5,6 +5,7 @@ import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas"; import { getMigrationEnvConfig } from "./utils/env-config"; @@ -54,7 +55,9 @@ const reencryptIdentityK8sAuth = async (knex: Knex) => { } initLogger(); - const envConfig = getMigrationEnvConfig(); + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); + const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const orgEncryptionRingBuffer = diff --git a/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts b/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts index c0496f823..25dfb69f1 100644 --- a/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts +++ b/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts @@ -5,6 +5,7 @@ import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas"; import { getMigrationEnvConfig } from "./utils/env-config"; @@ -34,7 +35,9 @@ const reencryptIdentityOidcAuth = async (knex: Knex) => { } initLogger(); - const envConfig = getMigrationEnvConfig(); + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); + const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const orgEncryptionRingBuffer = diff --git a/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts b/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts index b07fe7787..cd40318a4 100644 --- a/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts +++ b/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts @@ -10,6 +10,7 @@ import { SecretKeyEncoding, TableName } from "../schemas"; import { getMigrationEnvConfig } from "./utils/env-config"; import { createCircularCache } from "./utils/ring-buffer"; import { getMigrationEncryptionServices } from "./utils/services"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; const BATCH_SIZE = 500; const reencryptSamlConfig = async (knex: Knex) => { @@ -27,7 +28,8 @@ const reencryptSamlConfig = async (knex: Knex) => { } initLogger(); - const envConfig = getMigrationEnvConfig(); + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const orgEncryptionRingBuffer = @@ -189,7 +191,8 @@ const reencryptLdapConfig = async (knex: Knex) => { } initLogger(); - const envConfig = getMigrationEnvConfig(); + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const orgEncryptionRingBuffer = @@ -345,7 +348,8 @@ const reencryptOidcConfig = async (knex: Knex) => { } initLogger(); - const envConfig = getMigrationEnvConfig(); + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); const orgEncryptionRingBuffer = diff --git a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts index 3b3c5322e..a0985471f 100644 --- a/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts +++ b/backend/src/db/migrations/20250513081738_remove-gateway-project-link.ts @@ -4,6 +4,7 @@ import { inMemoryKeyStore } from "@app/keystore/memory"; import { selectAllTableCols } from "@app/lib/knex"; import { initLogger } from "@app/lib/logger"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { TableName } from "../schemas"; import { getMigrationEnvConfig } from "./utils/env-config"; @@ -39,7 +40,8 @@ export async function up(knex: Knex): Promise { ); initLogger(); - const envConfig = getMigrationEnvConfig(); + const superAdminDAL = superAdminDALFactory(knex); + const envConfig = await getMigrationEnvConfig(superAdminDAL); const keyStore = inMemoryKeyStore(); const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); diff --git a/backend/src/db/migrations/utils/env-config.ts b/backend/src/db/migrations/utils/env-config.ts index 8744308ab..14a6a2023 100644 --- a/backend/src/db/migrations/utils/env-config.ts +++ b/backend/src/db/migrations/utils/env-config.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { zpStr } from "@app/lib/zod"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; const envSchema = z .object({ @@ -35,7 +37,7 @@ const envSchema = z export type TMigrationEnvConfig = z.infer; -export const getMigrationEnvConfig = () => { +export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory) => { const parsedEnv = envSchema.safeParse(process.env); if (!parsedEnv.success) { // eslint-disable-next-line no-console @@ -49,5 +51,19 @@ export const getMigrationEnvConfig = () => { process.exit(-1); } - return Object.freeze(parsedEnv.data); + let envCfg = Object.freeze(parsedEnv.data); + + const fipsEnabled = await crypto.initialize(superAdminDAL); + + if (fipsEnabled) { + const newEnvCfg = { + ...envCfg, + ROOT_ENCRYPTION_KEY: envCfg.ENCRYPTION_KEY + }; + delete newEnvCfg.ENCRYPTION_KEY; + + envCfg = Object.freeze(newEnvCfg); + } + + return envCfg; }; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index c2db3e6e7..2b4054e1e 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -58,7 +58,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ sshHostGroups: false, secretScanning: false, enterpriseSecretSyncs: false, - enterpriseAppConnections: false + enterpriseAppConnections: false, + fips: true }); export const setupLicenseRequestWithStore = ( diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 2937ac265..a3412f574 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -75,6 +75,7 @@ export type TFeatureSet = { secretScanning: false; enterpriseSecretSyncs: false; enterpriseAppConnections: false; + fips: false; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 44ca5363f..8c9318d8b 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -1,6 +1,8 @@ import { z } from "zod"; +import { crypto } from "@app/lib/crypto/cryptography"; import { QueueWorkerProfile } from "@app/lib/types"; +import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { BadRequestError } from "../errors"; import { removeTrailingSlash } from "../fn"; @@ -349,7 +351,7 @@ export const getConfig = () => envCfg; export const getOriginalConfig = () => originalEnvConfig; // cannot import singleton logger directly as it needs config to load various transport -export const initEnvConfig = (logger?: CustomLogger) => { +export const initEnvConfig = async (superAdminDAL?: TSuperAdminDALFactory, logger?: CustomLogger) => { const parsedEnv = envSchema.safeParse(process.env); if (!parsedEnv.success) { (logger ?? console).error("Invalid environment variables. Check the error below"); @@ -357,16 +359,50 @@ export const initEnvConfig = (logger?: CustomLogger) => { process.exit(-1); } - const config = Object.freeze(parsedEnv.data); - envCfg = config; + if (superAdminDAL) { + const fipsEnabled = await crypto.initialize(superAdminDAL); - if (!originalEnvConfig) { - originalEnvConfig = config; + if (fipsEnabled) { + const newEnvCfg = { + ...envCfg, + ROOT_ENCRYPTION_KEY: envCfg.ENCRYPTION_KEY + }; + delete newEnvCfg.ENCRYPTION_KEY; + + envCfg = Object.freeze(newEnvCfg); + } + } + + if (!envCfg) { + const config = Object.freeze(parsedEnv.data); + envCfg = config; + + if (!originalEnvConfig) { + originalEnvConfig = config; + } } return envCfg; }; +export const getDatabaseCredentials = (logger?: CustomLogger) => { + const parsedEnv = envSchema.safeParse(process.env); + if (!parsedEnv.success) { + (logger ?? console).error("Invalid environment variables. Check the error below"); + (logger ?? console).error(parsedEnv.error.issues); + process.exit(-1); + } + + return { + 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 + })) + }; +}; + // A list of environment variables that can be overwritten export const overwriteSchema: { [key: string]: { @@ -462,7 +498,11 @@ export const overrideEnvConfig = (config: Record) => { const parsedResult = envSchema.safeParse(tempEnv); if (parsedResult.success) { - envCfg = Object.freeze(parsedResult.data); + envCfg = Object.freeze({ + ...parsedResult.data, + ENCRYPTION_KEY: envCfg.ENCRYPTION_KEY, + ROOT_ENCRYPTION_KEY: envCfg.ROOT_ENCRYPTION_KEY + }); } }; diff --git a/backend/src/lib/crypto/cryptography.ts b/backend/src/lib/crypto/cryptography.ts index 036048c45..3a44e5ce2 100644 --- a/backend/src/lib/crypto/cryptography.ts +++ b/backend/src/lib/crypto/cryptography.ts @@ -8,6 +8,7 @@ import nacl from "tweetnacl"; import naclUtils from "tweetnacl-util"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service"; @@ -67,40 +68,101 @@ const IV_BYTES_SIZE = 12; const BLOCK_SIZE_BYTES_16 = 16; const hasherFipsValidated = () => { - const $hashPassword = (password: string, salt: string, iterations: number, keyLength: number) => { - return new Promise((resolve, reject) => { + const keySize = 32; + + // For the salt when using pkdf2, we do salt rounds^6. If the salt rounds are 10, this will result in 10^6 = 1.000.000 iterations. + // The reason for this is because pbkdf2 is not as compute intense as bcrypt, making it faster to brute-force. + // From my testing, doing salt rounds^6 brings the computational power required to a little more than bcrypt. + // OWASP recommends a minimum of 600.000 iterations for pbkdf2, so 1.000.000 is more than enough. + // Ref: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 + const MIN_COST_FACTOR = 10; + const MAX_COST_FACTOR = 20; // Iterations scales polynomial (costFactor^6), so we need an upper bound + + const $calculateIterations = (costFactor: number) => { + return Math.round(costFactor ** 6); + }; + + const $hashPassword = (password: Buffer, salt: Buffer, iterations: number, keyLength: number) => { + return new Promise((resolve, reject) => { crypto.pbkdf2(password, salt, iterations, keyLength, "sha256", (err, derivedKey) => { if (err) { return reject(err); } - resolve(derivedKey.toString("hex")); + resolve(derivedKey); }); }); }; - const $validatePassword = ( - inputPassword: string, - storedHash: string, - salt: string, + const $validatePassword = async ( + inputPassword: Buffer, + storedHash: Buffer, + salt: Buffer, iterations: number, keyLength: number ) => { - return $hashPassword(inputPassword, salt, iterations, keyLength).then((hash) => hash === storedHash); + const computedHash = await $hashPassword(inputPassword, salt, iterations, keyLength); + + return crypto.timingSafeEqual(computedHash, storedHash); }; - const hash = async (password: string, saltRounds: number) => { - const salt = crypto.randomBytes(16).toString("hex"); - const derivedKey = await $hashPassword(password, salt, saltRounds, 32); - return `$infisical$${saltRounds}$${salt}$${derivedKey}`; + const hash = async (password: string, costFactor: number) => { + // Strict input validation + if (typeof password !== "string" || password.length === 0) { + throw new CryptographyError({ + message: "Invalid input, password must be a non-empty string" + }); + } + + if (!Number.isInteger(costFactor)) { + throw new CryptographyError({ + message: "Invalid cost factor, must be an integer" + }); + } + + if (costFactor < MIN_COST_FACTOR || costFactor > MAX_COST_FACTOR) { + throw new CryptographyError({ + message: `Invalid cost factor, must be between ${MIN_COST_FACTOR} and ${MAX_COST_FACTOR}` + }); + } + + const iterations = $calculateIterations(costFactor); + + const salt = crypto.randomBytes(16); + const derivedKey = await $hashPassword(Buffer.from(password), salt, iterations, keySize); + + const combined = Buffer.concat([salt, derivedKey]); + return `$v1$${costFactor}$${combined.toString("base64")}`; // Store original costFactor! }; const compare = async (password: string, hashedPassword: string) => { - if (!hashedPassword.startsWith("$infisical$")) { - throw new Error("Invalid hash format"); - } + try { + if (!hashedPassword?.startsWith("$v1$")) return false; - const [, , iterations, salt, storedHash] = hashedPassword.split("$"); - return $validatePassword(password, storedHash, salt, Number(iterations), 32); + const parts = hashedPassword.split("$"); + if (parts.length !== 4) return false; + + const [, , storedCostFactor, combined] = parts; + + if ( + !Number.isInteger(Number(storedCostFactor)) || + Number(storedCostFactor) < MIN_COST_FACTOR || + Number(storedCostFactor) > MAX_COST_FACTOR + ) { + return false; + } + + const combinedBuffer = Buffer.from(combined, "base64"); + const salt = combinedBuffer.subarray(0, 16); + const storedHash = combinedBuffer.subarray(16); + + const iterations = $calculateIterations(Number(storedCostFactor)); + + const isMatch = await $validatePassword(Buffer.from(password), storedHash, salt, iterations, keySize); + + return isMatch; + } catch { + return false; + } }; return { @@ -216,7 +278,9 @@ const decryptAsymmetricFipsValidated = ({ const final = decipher.final(); return Buffer.concat([plaintext, final]).toString("utf8"); } catch (error) { - throw new Error("Invalid ciphertext or keys"); + throw new CryptographyError({ + message: "Invalid ciphertext or keys" + }); } }; @@ -276,7 +340,9 @@ export const computeMd5 = (message: string, digest: DigestType = DigestType.Hex) encoder = cryptoJs.enc.Base64; break; default: - throw new Error(`Invalid digest type: ${digest as string}`); + throw new CryptographyError({ + message: `Invalid digest type: ${digest as string}` + }); } return cryptoJs.MD5(message).toString(encoder); @@ -289,7 +355,9 @@ const cryptographyFactory = () => { const $checkIsInitialized = () => { if (!$isInitialized) { - throw new Error("Internal cryptography module is not initialized"); + throw new CryptographyError({ + message: "Internal cryptography module is not initialized" + }); } }; @@ -297,6 +365,15 @@ const cryptographyFactory = () => { $checkIsInitialized(); return $fipsEnabled; }; + + const verifyFipsLicense = (licenseService: Pick) => { + if (isFipsModeEnabled() && !licenseService.onPremFeatures?.fips) { + throw new CryptographyError({ + message: "FIPS mode is enabled but your license does not include FIPS support. Please contact support." + }); + } + }; + 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) { @@ -305,6 +382,7 @@ const cryptographyFactory = () => { if (appCfg.ENCRYPTION_KEY) { // we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key + // note(daniel): for some reason this resolves as true for some hex-encoded strings. if (!isBase64(appCfg.ENCRYPTION_KEY)) { throw new CryptographyError({ message: @@ -398,15 +476,8 @@ 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. decipher = crypto.createDecipheriv(SecretEncryptionAlgo.AES_256_GCM, key, Buffer.from(iv, "base64")); } else { const secretKey = crypto.createSecretKey(key, "base64"); @@ -425,12 +496,6 @@ 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 { @@ -452,6 +517,7 @@ const cryptographyFactory = () => { const appCfg = getConfig(); const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY; const encryptionKey = appCfg.ENCRYPTION_KEY; + if (rootEncryptionKey) { const { iv, tag, ciphertext } = encryptSymmetric({ plaintext: data, @@ -480,7 +546,9 @@ const cryptographyFactory = () => { encoding: SecretKeyEncoding.UTF8 }; } - throw new Error("Missing both encryption keys"); + throw new CryptographyError({ + message: "Missing both encryption keys" + }); }; const decryptWithRootEncryptionKey = ({ @@ -509,7 +577,9 @@ const cryptographyFactory = () => { const data = decryptSymmetric({ key: encryptionKey, iv, tag, ciphertext, keySize: SymmetricKeySize.Bits128 }); return data as T; } - throw new Error("Missing both encryption keys"); + throw new CryptographyError({ + message: "Missing both encryption keys" + }); }; return { @@ -540,12 +610,7 @@ const cryptographyFactory = () => { if (isFipsModeEnabled()) { const hasher = hasherFipsValidated(); - // For the salt when using pkdf2, we do salt rounds * 100.000. - // The reason for this is because pbkdf2 is not as compute intense as bcrypt, making it faster to brute-force. - // From my testing, doing salt rounds * 100.000 brings the computational power required to roughly the same as bcrypt. - // OWASP recommends a minimum of 600.000 iterations for pbkdf2. - // Ref: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 - const hash = await hasher.hash(password, saltRounds * 100_000); + const hash = await hasher.hash(password, saltRounds); return hash; } const hash = await bcrypt.hash(password, saltRounds); @@ -572,6 +637,7 @@ const cryptographyFactory = () => { initialize, isFipsModeEnabled, hashing, + verifyFipsLicense, encryption, randomBytes: crypto.randomBytes, randomInt: crypto.randomInt, diff --git a/backend/src/lib/telemetry/instrumentation.ts b/backend/src/lib/telemetry/instrumentation.ts index 1cb35e143..44c910816 100644 --- a/backend/src/lib/telemetry/instrumentation.ts +++ b/backend/src/lib/telemetry/instrumentation.ts @@ -74,10 +74,8 @@ const initTelemetryInstrumentation = ({ }); }; -const setupTelemetry = () => { - const { envCfg } = initEnvConfig(); - - console.log("envCfg", envCfg); +const setupTelemetry = async () => { + const envCfg = await initEnvConfig(); if (envCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { console.log("Initializing telemetry instrumentation"); diff --git a/backend/src/main.ts b/backend/src/main.ts index 78cd05acf..1f62f5a76 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -7,9 +7,8 @@ import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; import { runMigrations } from "./auto-start-migrations"; import { initAuditLogDbConnection, initDbConnection } from "./db"; import { keyStoreFactory } from "./keystore/keystore"; -import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; +import { formatSmtpConfig, getDatabaseCredentials, initEnvConfig } from "./lib/config/env"; import { buildRedisFromConfig } from "./lib/config/redis"; -import { crypto } from "./lib/crypto/cryptography"; import { removeTemporaryBaseDirectory } from "./lib/files"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; @@ -22,29 +21,23 @@ dotenv.config(); const run = async () => { const logger = initLogger(); - const { envCfg, updateRootEncryptionKey } = initEnvConfig(logger); - await removeTemporaryBaseDirectory(); + const databaseCredentials = getDatabaseCredentials(logger); + const db = initDbConnection({ - 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 - })) + dbConnectionUri: databaseCredentials.dbConnectionUri, + dbRootCert: databaseCredentials.dbRootCert, + readReplicas: databaseCredentials.readReplicas }); const superAdminDAL = superAdminDALFactory(db); - const fipsEnabled = await crypto.initialize(superAdminDAL); - if (fipsEnabled) { - updateRootEncryptionKey(envCfg.ENCRYPTION_KEY); - } + const envConfig = await initEnvConfig(superAdminDAL, logger); - const auditLogDb = envCfg.AUDIT_LOGS_DB_CONNECTION_URI + const auditLogDb = envConfig.AUDIT_LOGS_DB_CONNECTION_URI ? initAuditLogDbConnection({ - dbConnectionUri: envCfg.AUDIT_LOGS_DB_CONNECTION_URI, - dbRootCert: envCfg.AUDIT_LOGS_DB_ROOT_CERT + dbConnectionUri: envConfig.AUDIT_LOGS_DB_CONNECTION_URI, + dbRootCert: envConfig.AUDIT_LOGS_DB_ROOT_CERT }) : undefined; @@ -52,17 +45,17 @@ const run = async () => { const smtp = smtpServiceFactory(formatSmtpConfig()); - const queue = queueServiceFactory(envCfg, { - dbConnectionUrl: envCfg.DB_CONNECTION_URI, - dbRootCert: envCfg.DB_ROOT_CERT + const queue = queueServiceFactory(envConfig, { + dbConnectionUrl: envConfig.DB_CONNECTION_URI, + dbRootCert: envConfig.DB_ROOT_CERT }); await queue.initialize(); - const keyStore = keyStoreFactory(envCfg); - const redis = buildRedisFromConfig(envCfg); + const keyStore = keyStoreFactory(envConfig); + const redis = buildRedisFromConfig(envConfig); - const hsmModule = initializeHsmModule(envCfg); + const hsmModule = initializeHsmModule(envConfig); hsmModule.initialize(); const server = await main({ @@ -75,7 +68,7 @@ const run = async () => { queue, keyStore, redis, - envConfig: envCfg + envConfig }); const bootstrap = await bootstrapCheck({ db }); @@ -99,7 +92,7 @@ const run = async () => { process.exit(0); }); - if (!envCfg.isDevelopmentMode) { + if (!envConfig.isDevelopmentMode) { process.on("uncaughtException", (error) => { logger.error(error, "CRITICAL ERROR: Uncaught Exception"); }); @@ -110,8 +103,8 @@ const run = async () => { } await server.listen({ - port: envCfg.PORT, - host: envCfg.HOST, + port: envConfig.PORT, + host: envConfig.HOST, listenTextResolver: (address) => { void bootstrap(); return address; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index db7f0954b..e3a335deb 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -119,6 +119,7 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal" import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig, TEnvConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto/cryptography"; import { logger } from "@app/lib/logger"; import { TQueueServiceFactory } from "@app/queue"; import { readLimit } from "@app/server/config/rateLimiter"; @@ -1903,11 +1904,14 @@ export const registerRoutes = async ( kmsService }); - await superAdminService.initServerCfg(); - // setup the communication with license key server await licenseService.init(); + // If FIPS is enabled, we check to ensure that the users license includes FIPS mode. + crypto.verifyFipsLicense(licenseService); + + await superAdminService.initServerCfg(); + // Start HSM service if it's configured/enabled. await hsmService.startService(); diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 850635f5d..c4a26afd8 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -830,6 +830,7 @@ export const kmsServiceFactory = ({ const $getBasicEncryptionKey = () => { const encryptionKey = envConfig.ENCRYPTION_KEY || envConfig.ROOT_ENCRYPTION_KEY; + const isBase64 = !envConfig.ENCRYPTION_KEY; if (!encryptionKey) throw new Error( diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 59db26147..70cf7d67f 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -277,6 +277,7 @@ export const superAdminServiceFactory = ({ const $syncEnvConfig = async () => { const config = await getEnvOverrides(); + overrideEnvConfig(config); }; @@ -483,6 +484,7 @@ export const superAdminServiceFactory = ({ userAgent }: TAdminSignUpDTO) => { const appCfg = getConfig(); + const sanitizedEmail = email.trim().toLowerCase(); const existingUser = await userDAL.findOne({ username: sanitizedEmail }); if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exists" });