mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-08 23:18:05 -05:00
feat(fips): fips inside
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<void> {
|
||||
}
|
||||
|
||||
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<Awaited<ReturnType<(typeof kmsService)["createCipherPairWithDataKey"]>>>(25);
|
||||
const webhooks = await knex(TableName.Webhook)
|
||||
|
||||
@@ -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<void> {
|
||||
}
|
||||
|
||||
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 =
|
||||
|
||||
@@ -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<void> {
|
||||
}
|
||||
|
||||
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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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<void> {
|
||||
);
|
||||
|
||||
initLogger();
|
||||
const envConfig = getMigrationEnvConfig();
|
||||
const superAdminDAL = superAdminDALFactory(knex);
|
||||
const envConfig = await getMigrationEnvConfig(superAdminDAL);
|
||||
const keyStore = inMemoryKeyStore();
|
||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||
|
||||
|
||||
@@ -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<typeof envSchema>;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -58,7 +58,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
sshHostGroups: false,
|
||||
secretScanning: false,
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseAppConnections: false
|
||||
enterpriseAppConnections: false,
|
||||
fips: true
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (
|
||||
|
||||
@@ -75,6 +75,7 @@ export type TFeatureSet = {
|
||||
secretScanning: false;
|
||||
enterpriseSecretSyncs: false;
|
||||
enterpriseAppConnections: false;
|
||||
fips: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
||||
@@ -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<string, string>) => {
|
||||
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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<string>((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<Buffer>((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<TLicenseServiceFactory, "onPremFeatures">) => {
|
||||
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 = <T = string>({
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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" });
|
||||
|
||||
Reference in New Issue
Block a user