diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 58f2bffebf..9dfb38aeb9 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -23,14 +23,14 @@ export default { name: "knex-env", transformMode: "ssr", async setup() { - const logger = await initLogger(); - const cfg = initEnvConfig(logger); + const logger = initLogger(); + const envConfig = initEnvConfig(logger); const db = initDbConnection({ - dbConnectionUri: cfg.DB_CONNECTION_URI, - dbRootCert: cfg.DB_ROOT_CERT + dbConnectionUri: envConfig.DB_CONNECTION_URI, + dbRootCert: envConfig.DB_ROOT_CERT }); - const redis = new Redis(cfg.REDIS_URL); + const redis = new Redis(envConfig.REDIS_URL); await redis.flushdb("SYNC"); try { @@ -42,6 +42,7 @@ export default { }, true ); + await db.migrate.latest({ directory: path.join(__dirname, "../src/db/migrations"), extension: "ts", @@ -52,14 +53,24 @@ export default { directory: path.join(__dirname, "../src/db/seeds"), extension: "ts" }); - const smtp = mockSmtpServer(); - const queue = queueServiceFactory(cfg.REDIS_URL, { dbConnectionUrl: cfg.DB_CONNECTION_URI }); - const keyStore = keyStoreFactory(cfg.REDIS_URL); - const hsmModule = initializeHsmModule(); + const smtp = mockSmtpServer(); + const queue = queueServiceFactory(envConfig.REDIS_URL, { dbConnectionUrl: envConfig.DB_CONNECTION_URI }); + const keyStore = keyStoreFactory(envConfig.REDIS_URL); + + const hsmModule = initializeHsmModule(envConfig); hsmModule.initialize(); - const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: hsmModule.getModule(), redis }); + const server = await main({ + db, + smtp, + logger, + queue, + keyStore, + hsmModule: hsmModule.getModule(), + redis, + envConfig + }); // @ts-expect-error type globalThis.testServer = server; @@ -73,8 +84,8 @@ export default { organizationId: seedData1.organization.id, accessVersion: 1 }, - cfg.AUTH_SECRET, - { expiresIn: cfg.JWT_AUTH_LIFETIME } + envConfig.AUTH_SECRET, + { expiresIn: envConfig.JWT_AUTH_LIFETIME } ); } catch (error) { // eslint-disable-next-line @@ -109,3 +120,4 @@ export default { }; } }; + diff --git a/backend/package.json b/backend/package.json index 43409e619d..4f8647eab6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -45,21 +45,21 @@ "test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts", "generate:component": "tsx ./scripts/create-backend-file.ts", "generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas", - "auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest", - "auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up", - "auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down", - "auditlog-migration:list": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:list", - "auditlog-migration:status": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:status", - "auditlog-migration:unlock": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:unlock", - "auditlog-migration:rollback": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:rollback", + "auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.mjs --client pg migrate:latest", + "auditlog-migration:up": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:up", + "auditlog-migration:down": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:down", + "auditlog-migration:list": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:list", + "auditlog-migration:status": "knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:status", + "auditlog-migration:unlock": "knex --knexfile ./dist/db/auditlog-knexfile.mjs migrate:unlock", + "auditlog-migration:rollback": "knex --knexfile ./dist/db/auditlog-knexfile.mjs migrate:rollback", "migration:new": "tsx ./scripts/create-migration.ts", - "migration:up": "npm run auditlog-migration:up && knex --knexfile ./src/db/knexfile.ts --client pg migrate:up", - "migration:down": "npm run auditlog-migration:down && knex --knexfile ./src/db/knexfile.ts --client pg migrate:down", - "migration:list": "npm run auditlog-migration:list && knex --knexfile ./src/db/knexfile.ts --client pg migrate:list", - "migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", - "migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status", - "migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback", - "migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./src/db/knexfile.ts migrate:unlock", + "migration:up": "npm run auditlog-migration:up && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:up", + "migration:down": "npm run auditlog-migration:down && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:down", + "migration:list": "npm run auditlog-migration:list && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:list", + "migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && npm run auditlog-migration:latest && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:latest", + "migration:status": "npm run auditlog-migration:status && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:status", + "migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./dist/db/knexfile.mjs migrate:rollback", + "migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./dist/db/knexfile.mjs migrate:unlock", "migrate:org": "tsx ./scripts/migrate-organization.ts", "seed:new": "tsx ./scripts/create-seed-file.ts", "seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index f3298625e6..c023470385 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -93,6 +93,12 @@ import { TUserEngagementServiceFactory } from "@app/services/user-engagement/use import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; +declare module "@fastify/request-context" { + interface RequestContextData { + reqId: string; + } +} + declare module "fastify" { interface Session { callbackPort: string; diff --git a/backend/src/auto-start-migrations.ts b/backend/src/auto-start-migrations.ts new file mode 100644 index 0000000000..88f6dea692 --- /dev/null +++ b/backend/src/auto-start-migrations.ts @@ -0,0 +1,105 @@ +import path from "node:path"; + +import dotenv from "dotenv"; +import { Knex } from "knex"; +import { Logger } from "pino"; + +import { PgSqlLock } from "./keystore/keystore"; + +dotenv.config(); + +type TArgs = { + auditLogDb?: Knex; + applicationDb: Knex; + logger: Logger; +}; + +const isProduction = process.env.NODE_ENV === "production"; +const migrationConfig = { + directory: path.join(__dirname, "./db/migrations"), + loadExtensions: [".mjs", ".ts"], + tableName: "infisical_migrations" +}; + +const migrationStatusCheckErrorHandler = (err: Error) => { + // happens for first time in which the migration table itself is not created yet + // error: select * from "infisical_migrations" - relation "infisical_migrations" does not exist + if (err?.message?.includes("does not exist")) { + return true; + } + throw err; +}; + +export const runMigrations = async ({ applicationDb, auditLogDb, logger }: TArgs) => { + try { + // akhilmhdh(Feb 10 2025): 2 years from now remove this + if (isProduction) { + const migrationTable = migrationConfig.tableName; + const hasMigrationTable = await applicationDb.schema.hasTable(migrationTable); + if (hasMigrationTable) { + const firstFile = (await applicationDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await applicationDb(migrationTable).update({ + name: applicationDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + if (auditLogDb) { + const hasMigrationTableInAuditLog = await auditLogDb.schema.hasTable(migrationTable); + if (hasMigrationTableInAuditLog) { + const firstFile = (await auditLogDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await auditLogDb(migrationTable).update({ + name: auditLogDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + } + } + + const shouldRunMigration = Boolean( + await applicationDb.migrate.status(migrationConfig).catch(migrationStatusCheckErrorHandler) + ); // db.length - code.length + if (!shouldRunMigration) { + logger.info("No migrations pending: Skipping migration process."); + return; + } + + if (auditLogDb) { + await auditLogDb.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.BootUpMigration]); + logger.info("Running audit log migrations."); + + const didPreviousInstanceRunMigration = !(await auditLogDb.migrate + .status(migrationConfig) + .catch(migrationStatusCheckErrorHandler)); + if (didPreviousInstanceRunMigration) { + logger.info("No audit log migrations pending: Applied by previous instance. Skipping migration process."); + return; + } + + await auditLogDb.migrate.latest(migrationConfig); + logger.info("Finished audit log migrations."); + }); + } + + await applicationDb.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.BootUpMigration]); + logger.info("Running application migrations."); + + const didPreviousInstanceRunMigration = !(await applicationDb.migrate + .status(migrationConfig) + .catch(migrationStatusCheckErrorHandler)); + if (didPreviousInstanceRunMigration) { + logger.info("No application migrations pending: Applied by previous instance. Skipping migration process."); + return; + } + + await applicationDb.migrate.latest(migrationConfig); + logger.info("Finished application migrations."); + }); + } catch (err) { + logger.error(err, "Boot up migration failed"); + process.exit(1); + } +}; diff --git a/backend/src/db/instance.ts b/backend/src/db/instance.ts index d4a2a5b2ca..5a8dd3d059 100644 --- a/backend/src/db/instance.ts +++ b/backend/src/db/instance.ts @@ -49,6 +49,9 @@ export const initDbConnection = ({ ca: Buffer.from(dbRootCert, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); @@ -64,6 +67,9 @@ export const initDbConnection = ({ ca: Buffer.from(replicaDbCertificate, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); }); @@ -98,6 +104,9 @@ export const initAuditLogDbConnection = ({ ca: Buffer.from(dbRootCert, "base64").toString("ascii") } : false + }, + migrations: { + tableName: "infisical_migrations" } }); diff --git a/backend/src/db/knexfile.ts b/backend/src/db/knexfile.ts index 8af2b59ab2..8cf80b7445 100644 --- a/backend/src/db/knexfile.ts +++ b/backend/src/db/knexfile.ts @@ -38,7 +38,8 @@ export default { directory: "./seeds" }, migrations: { - tableName: "infisical_migrations" + tableName: "infisical_migrations", + loadExtensions: [".mjs"] } }, production: { @@ -62,7 +63,8 @@ export default { max: 10 }, migrations: { - tableName: "infisical_migrations" + tableName: "infisical_migrations", + loadExtensions: [".mjs"] } } } as Knex.Config; diff --git a/backend/src/db/migrations/20250210101840_webhook-to-kms.ts b/backend/src/db/migrations/20250210101840_webhook-to-kms.ts new file mode 100644 index 0000000000..c9b8e7fec5 --- /dev/null +++ b/backend/src/db/migrations/20250210101840_webhook-to-kms.ts @@ -0,0 +1,127 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasEncryptedKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedPassKey"); + const hasEncryptedUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl"); + const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url"); + + const hasWebhookTable = await knex.schema.hasTable(TableName.Webhook); + if (hasWebhookTable) { + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (!hasEncryptedKey) t.binary("encryptedPassKey"); + if (!hasEncryptedUrl) t.binary("encryptedUrl"); + if (hasUrl) t.string("url").nullable().alter(); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const projectEncryptionRingBuffer = + createCircularCache>>(25); + const webhooks = await knex(TableName.Webhook) + .where({}) + .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`) + .select( + "url", + "encryptedSecretKey", + "iv", + "tag", + "keyEncoding", + "urlCipherText", + "urlIV", + "urlTag", + knex.ref("id").withSchema(TableName.Webhook), + "envId" + ) + .select(knex.ref("projectId").withSchema(TableName.Environment)) + .orderBy(`${TableName.Environment}.projectId` as "projectId"); + + const updatedWebhooks = await Promise.all( + webhooks.map(async (el) => { + let projectKmsService = projectEncryptionRingBuffer.getItem(el.projectId); + if (!projectKmsService) { + projectKmsService = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: el.projectId + }, knex); + projectEncryptionRingBuffer.push(el.projectId, projectKmsService); + } + + let encryptedSecretKey = null; + if (el.encryptedSecretKey && el.iv && el.tag && el.keyEncoding) { + const decyptedSecretKey = infisicalSymmetricDecrypt({ + keyEncoding: el.keyEncoding as SecretKeyEncoding, + iv: el.iv, + tag: el.tag, + ciphertext: el.encryptedSecretKey + }); + encryptedSecretKey = projectKmsService.encryptor({ + plainText: Buffer.from(decyptedSecretKey, "utf8") + }).cipherTextBlob; + } + + const decryptedUrl = + el.urlIV && el.urlTag && el.urlCipherText && el.keyEncoding + ? infisicalSymmetricDecrypt({ + keyEncoding: el.keyEncoding as SecretKeyEncoding, + iv: el.urlIV, + tag: el.urlTag, + ciphertext: el.urlCipherText + }) + : null; + + const encryptedUrl = projectKmsService.encryptor({ + plainText: Buffer.from(decryptedUrl || el.url || "") + }).cipherTextBlob; + return { id: el.id, encryptedUrl, encryptedSecretKey, envId: el.envId }; + }) + ); + + for (let i = 0; i < updatedWebhooks.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.Webhook) + .insert( + updatedWebhooks.slice(i, i + BATCH_SIZE).map((el) => ({ + id: el.id, + envId: el.envId, + url: "", + encryptedUrl: el.encryptedUrl, + encryptedPassKey: el.encryptedSecretKey + })) + ) + .onConflict("id") + .merge(); + } + + if (hasWebhookTable) { + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (!hasEncryptedUrl) t.binary("encryptedUrl").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEncryptedKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedPassKey"); + const hasEncryptedUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl"); + + const hasWebhookTable = await knex.schema.hasTable(TableName.Webhook); + if (hasWebhookTable) { + await knex.schema.alterTable(TableName.Webhook, (t) => { + if (hasEncryptedKey) t.dropColumn("encryptedPassKey"); + if (hasEncryptedUrl) t.dropColumn("encryptedUrl"); + }); + } +} 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 new file mode 100644 index 0000000000..41dc6ba9f1 --- /dev/null +++ b/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts @@ -0,0 +1,108 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasEncryptedInputColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedInput"); + const hasInputCiphertextColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext"); + const hasInputIVColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV"); + const hasInputTagColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag"); + + const hasDynamicSecretTable = await knex.schema.hasTable(TableName.DynamicSecret); + if (hasDynamicSecretTable) { + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (!hasEncryptedInputColumn) t.binary("encryptedInput"); + if (hasInputCiphertextColumn) t.text("inputCiphertext").nullable().alter(); + if (hasInputIVColumn) t.string("inputIV").nullable().alter(); + if (hasInputTagColumn) t.string("inputTag").nullable().alter(); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const projectEncryptionRingBuffer = + createCircularCache>>(25); + + const dynamicSecretRootCredentials = await knex(TableName.DynamicSecret) + .join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`) + .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .select(selectAllTableCols(TableName.DynamicSecret)) + .select(knex.ref("projectId").withSchema(TableName.Environment)) + .orderBy(`${TableName.Environment}.projectId` as "projectId"); + + const updatedDynamicSecrets = await Promise.all( + dynamicSecretRootCredentials.map(async ({ projectId, ...el }) => { + let projectKmsService = projectEncryptionRingBuffer.getItem(projectId); + if (!projectKmsService) { + projectKmsService = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }, knex); + projectEncryptionRingBuffer.push(projectId, projectKmsService); + } + + const decryptedInputData = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.inputIV && el.inputTag && el.inputCiphertext && el.keyEncoding + ? infisicalSymmetricDecrypt({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + keyEncoding: el.keyEncoding as SecretKeyEncoding, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.inputIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.inputTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.inputCiphertext + }) + : ""; + + const encryptedInput = projectKmsService.encryptor({ + plainText: Buffer.from(decryptedInputData) + }).cipherTextBlob; + + return { ...el, encryptedInput }; + }) + ); + + for (let i = 0; i < updatedDynamicSecrets.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.DynamicSecret) + .insert(updatedDynamicSecrets.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + if (hasDynamicSecretTable) { + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (!hasEncryptedInputColumn) t.binary("encryptedInput").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEncryptedInputColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedInput"); + + const hasDynamicSecretTable = await knex.schema.hasTable(TableName.DynamicSecret); + if (hasDynamicSecretTable) { + await knex.schema.alterTable(TableName.DynamicSecret, (t) => { + if (hasEncryptedInputColumn) t.dropColumn("encryptedInput"); + }); + } +} diff --git a/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts b/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts new file mode 100644 index 0000000000..567cace991 --- /dev/null +++ b/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts @@ -0,0 +1,100 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasEncryptedRotationData = await knex.schema.hasColumn(TableName.SecretRotation, "encryptedRotationData"); + + const hasRotationTable = await knex.schema.hasTable(TableName.SecretRotation); + if (hasRotationTable) { + await knex.schema.alterTable(TableName.SecretRotation, (t) => { + if (!hasEncryptedRotationData) t.binary("encryptedRotationData"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const projectEncryptionRingBuffer = + createCircularCache>>(25); + + const secretRotations = await knex(TableName.SecretRotation) + .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretRotation}.envId`) + .select(selectAllTableCols(TableName.SecretRotation)) + .select(knex.ref("projectId").withSchema(TableName.Environment)) + .orderBy(`${TableName.Environment}.projectId` as "projectId"); + + const updatedRotationData = await Promise.all( + secretRotations.map(async ({ projectId, ...el }) => { + let projectKmsService = projectEncryptionRingBuffer.getItem(projectId); + if (!projectKmsService) { + projectKmsService = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }, knex); + projectEncryptionRingBuffer.push(projectId, projectKmsService); + } + + const decryptedRotationData = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedDataTag && el.encryptedDataIV && el.encryptedData && el.keyEncoding + ? infisicalSymmetricDecrypt({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + keyEncoding: el.keyEncoding as SecretKeyEncoding, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.encryptedDataIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.encryptedDataTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedData + }) + : ""; + + const encryptedRotationData = projectKmsService.encryptor({ + plainText: Buffer.from(decryptedRotationData) + }).cipherTextBlob; + return { ...el, encryptedRotationData }; + }) + ); + + for (let i = 0; i < updatedRotationData.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.SecretRotation) + .insert(updatedRotationData.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + if (hasRotationTable) { + await knex.schema.alterTable(TableName.SecretRotation, (t) => { + if (!hasEncryptedRotationData) t.binary("encryptedRotationData").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasEncryptedRotationData = await knex.schema.hasColumn(TableName.SecretRotation, "encryptedRotationData"); + + const hasRotationTable = await knex.schema.hasTable(TableName.SecretRotation); + if (hasRotationTable) { + await knex.schema.alterTable(TableName.SecretRotation, (t) => { + if (hasEncryptedRotationData) t.dropColumn("encryptedRotationData"); + }); + } +} 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 new file mode 100644 index 0000000000..3d62ab04fb --- /dev/null +++ b/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts @@ -0,0 +1,190 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { decryptSymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +const reencryptIdentityK8sAuth = async (knex: Knex) => { + const hasEncryptedKubernetesTokenReviewerJwt = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesTokenReviewerJwt" + ); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesCaCertificate" + ); + const hasidentityKubernetesAuthTable = await knex.schema.hasTable(TableName.IdentityKubernetesAuth); + + const hasEncryptedCaCertColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "encryptedCaCert"); + const hasCaCertIVColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "caCertIV"); + const hasCaCertTagColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "caCertTag"); + const hasEncryptedTokenReviewerJwtColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedTokenReviewerJwt" + ); + const hasTokenReviewerJwtIVColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "tokenReviewerJwtIV" + ); + const hasTokenReviewerJwtTagColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "tokenReviewerJwtTag" + ); + + if (hasidentityKubernetesAuthTable) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => { + if (hasEncryptedCaCertColumn) t.text("encryptedCaCert").nullable().alter(); + if (hasCaCertIVColumn) t.string("caCertIV").nullable().alter(); + if (hasCaCertTagColumn) t.string("caCertTag").nullable().alter(); + if (hasEncryptedTokenReviewerJwtColumn) t.text("encryptedTokenReviewerJwt").nullable().alter(); + if (hasTokenReviewerJwtIVColumn) t.string("tokenReviewerJwtIV").nullable().alter(); + if (hasTokenReviewerJwtTagColumn) t.string("tokenReviewerJwtTag").nullable().alter(); + + if (!hasEncryptedKubernetesTokenReviewerJwt) t.binary("encryptedKubernetesTokenReviewerJwt"); + if (!hasEncryptedCertificateColumn) t.binary("encryptedKubernetesCaCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + const identityKubernetesConfigs = await knex(TableName.IdentityKubernetesAuth) + .join( + TableName.IdentityOrgMembership, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityKubernetesAuth}.identityId` + ) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.IdentityOrgMembership}.orgId`) + .select(selectAllTableCols(TableName.IdentityKubernetesAuth)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot), + knex.ref("orgId").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedIdentityKubernetesConfigs = []; + + for (const { encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, orgId, ...el } of identityKubernetesConfigs) { + let orgKmsService = orgEncryptionRingBuffer.getItem(orgId); + + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }, knex); + orgEncryptionRingBuffer.push(orgId, orgKmsService); + } + + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedTokenReviewerJwt = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedTokenReviewerJwt && el.tokenReviewerJwtIV && el.tokenReviewerJwtTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.tokenReviewerJwtIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.tokenReviewerJwtTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedTokenReviewerJwt + }) + : ""; + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCaCert && el.caCertIV && el.caCertTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.caCertIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.caCertTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCaCert + }) + : ""; + + const encryptedKubernetesTokenReviewerJwt = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedTokenReviewerJwt) + }).cipherTextBlob; + const encryptedKubernetesCaCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + + updatedIdentityKubernetesConfigs.push({ + ...el, + accessTokenTrustedIps: JSON.stringify(el.accessTokenTrustedIps), + encryptedKubernetesCaCertificate, + encryptedKubernetesTokenReviewerJwt + }); + } + + for (let i = 0; i < updatedIdentityKubernetesConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityKubernetesAuth) + .insert(updatedIdentityKubernetesConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + if (hasidentityKubernetesAuthTable) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => { + if (!hasEncryptedKubernetesTokenReviewerJwt) + t.binary("encryptedKubernetesTokenReviewerJwt").notNullable().alter(); + }); + } +}; + +export async function up(knex: Knex): Promise { + await reencryptIdentityK8sAuth(knex); +} + +const dropIdentityK8sColumns = async (knex: Knex) => { + const hasEncryptedKubernetesTokenReviewerJwt = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesTokenReviewerJwt" + ); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityKubernetesAuth, + "encryptedKubernetesCaCertificate" + ); + const hasidentityKubernetesAuthTable = await knex.schema.hasTable(TableName.IdentityKubernetesAuth); + + if (hasidentityKubernetesAuthTable) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => { + if (hasEncryptedKubernetesTokenReviewerJwt) t.dropColumn("encryptedKubernetesTokenReviewerJwt"); + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedKubernetesCaCertificate"); + }); + } +}; + +export async function down(knex: Knex): Promise { + await dropIdentityK8sColumns(knex); +} 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 new file mode 100644 index 0000000000..dc87726a47 --- /dev/null +++ b/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts @@ -0,0 +1,138 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { decryptSymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName, TOrgBots } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +const reencryptIdentityOidcAuth = async (knex: Knex) => { + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityOidcAuth, + "encryptedCaCertificate" + ); + const hasidentityOidcAuthTable = await knex.schema.hasTable(TableName.IdentityOidcAuth); + + const hasEncryptedCaCertColumn = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "encryptedCaCert"); + const hasCaCertIVColumn = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "caCertIV"); + const hasCaCertTagColumn = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "caCertTag"); + + if (hasidentityOidcAuthTable) { + await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => { + if (hasEncryptedCaCertColumn) t.text("encryptedCaCert").nullable().alter(); + if (hasCaCertIVColumn) t.string("caCertIV").nullable().alter(); + if (hasCaCertTagColumn) t.string("caCertTag").nullable().alter(); + + if (!hasEncryptedCertificateColumn) t.binary("encryptedCaCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const identityOidcConfig = await knex(TableName.IdentityOidcAuth) + .join( + TableName.IdentityOrgMembership, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityOidcAuth}.identityId` + ) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.IdentityOrgMembership}.orgId`) + .select(selectAllTableCols(TableName.IdentityOidcAuth)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot), + knex.ref("orgId").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedIdentityOidcConfigs = await Promise.all( + identityOidcConfig.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, orgId, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }, knex); + orgEncryptionRingBuffer.push(orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCaCert && el.caCertIV && el.caCertTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.caCertIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.caCertTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCaCert + }) + : ""; + + const encryptedCaCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + + return { + ...el, + accessTokenTrustedIps: JSON.stringify(el.accessTokenTrustedIps), + encryptedCaCertificate + }; + } + ) + ); + + for (let i = 0; i < updatedIdentityOidcConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.IdentityOidcAuth) + .insert(updatedIdentityOidcConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } +}; + +export async function up(knex: Knex): Promise { + await reencryptIdentityOidcAuth(knex); +} + +const dropIdentityOidcColumns = async (knex: Knex) => { + const hasEncryptedCertificateColumn = await knex.schema.hasColumn( + TableName.IdentityOidcAuth, + "encryptedCaCertificate" + ); + const hasidentityOidcTable = await knex.schema.hasTable(TableName.IdentityOidcAuth); + + if (hasidentityOidcTable) { + await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => { + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedCaCertificate"); + }); + } +}; + +export async function down(knex: Knex): Promise { + await dropIdentityOidcColumns(knex); +} diff --git a/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts b/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts new file mode 100644 index 0000000000..05db409584 --- /dev/null +++ b/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts @@ -0,0 +1,484 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { decryptSymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { selectAllTableCols } from "@app/lib/knex"; +import { initLogger } from "@app/lib/logger"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { SecretKeyEncoding, TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { createCircularCache } from "./utils/ring-buffer"; +import { getMigrationEncryptionServices } from "./utils/services"; + +const BATCH_SIZE = 500; +const reencryptSamlConfig = async (knex: Knex) => { + const hasEncryptedEntrypointColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlEntryPoint"); + const hasEncryptedIssuerColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlIssuer"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlCertificate"); + const hasSamlConfigTable = await knex.schema.hasTable(TableName.SamlConfig); + + if (hasSamlConfigTable) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + if (!hasEncryptedEntrypointColumn) t.binary("encryptedSamlEntryPoint"); + if (!hasEncryptedIssuerColumn) t.binary("encryptedSamlIssuer"); + if (!hasEncryptedCertificateColumn) t.binary("encryptedSamlCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const samlConfigs = await knex(TableName.SamlConfig) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.SamlConfig}.orgId`) + .select(selectAllTableCols(TableName.SamlConfig)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedSamlConfigs = await Promise.all( + samlConfigs.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: el.orgId + }, knex); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedEntryPoint = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedEntryPoint && el.entryPointIV && el.entryPointTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.entryPointIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.entryPointTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedEntryPoint + }) + : ""; + + const decryptedIssuer = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedIssuer && el.issuerIV && el.issuerTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.issuerIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.issuerTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedIssuer + }) + : ""; + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCert && el.certIV && el.certTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.certIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.certTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCert + }) + : ""; + + const encryptedSamlIssuer = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedIssuer) + }).cipherTextBlob; + const encryptedSamlCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + const encryptedSamlEntryPoint = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedEntryPoint) + }).cipherTextBlob; + return { ...el, encryptedSamlCertificate, encryptedSamlEntryPoint, encryptedSamlIssuer }; + } + ) + ); + + for (let i = 0; i < updatedSamlConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.SamlConfig) + .insert(updatedSamlConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + + if (hasSamlConfigTable) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + if (!hasEncryptedEntrypointColumn) t.binary("encryptedSamlEntryPoint").notNullable().alter(); + if (!hasEncryptedIssuerColumn) t.binary("encryptedSamlIssuer").notNullable().alter(); + if (!hasEncryptedCertificateColumn) t.binary("encryptedSamlCertificate").notNullable().alter(); + }); + } +}; + +const reencryptLdapConfig = async (knex: Knex) => { + const hasEncryptedLdapBindDNColum = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindDN"); + const hasEncryptedLdapBindPassColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindPass"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapCaCertificate"); + const hasLdapConfigTable = await knex.schema.hasTable(TableName.LdapConfig); + + const hasEncryptedCACertColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedCACert"); + const hasCaCertIVColumn = await knex.schema.hasColumn(TableName.LdapConfig, "caCertIV"); + const hasCaCertTagColumn = await knex.schema.hasColumn(TableName.LdapConfig, "caCertTag"); + const hasEncryptedBindPassColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedBindPass"); + const hasBindPassIVColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindPassIV"); + const hasBindPassTagColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindPassTag"); + const hasEncryptedBindDNColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedBindDN"); + const hasBindDNIVColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindDNIV"); + const hasBindDNTagColumn = await knex.schema.hasColumn(TableName.LdapConfig, "bindDNTag"); + + if (hasLdapConfigTable) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + if (hasEncryptedCACertColumn) t.text("encryptedCACert").nullable().alter(); + if (hasCaCertIVColumn) t.string("caCertIV").nullable().alter(); + if (hasCaCertTagColumn) t.string("caCertTag").nullable().alter(); + if (hasEncryptedBindPassColumn) t.string("encryptedBindPass").nullable().alter(); + if (hasBindPassIVColumn) t.string("bindPassIV").nullable().alter(); + if (hasBindPassTagColumn) t.string("bindPassTag").nullable().alter(); + if (hasEncryptedBindDNColumn) t.string("encryptedBindDN").nullable().alter(); + if (hasBindDNIVColumn) t.string("bindDNIV").nullable().alter(); + if (hasBindDNTagColumn) t.string("bindDNTag").nullable().alter(); + + if (!hasEncryptedLdapBindDNColum) t.binary("encryptedLdapBindDN"); + if (!hasEncryptedLdapBindPassColumn) t.binary("encryptedLdapBindPass"); + if (!hasEncryptedCertificateColumn) t.binary("encryptedLdapCaCertificate"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const ldapConfigs = await knex(TableName.LdapConfig) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.LdapConfig}.orgId`) + .select(selectAllTableCols(TableName.LdapConfig)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedLdapConfigs = await Promise.all( + ldapConfigs.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: el.orgId + }, knex); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedBindDN = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedBindDN && el.bindDNIV && el.bindDNTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.bindDNIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.bindDNTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedBindDN + }) + : ""; + + const decryptedBindPass = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedBindPass && el.bindPassIV && el.bindPassTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.bindPassIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.bindPassTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedBindPass + }) + : ""; + + const decryptedCertificate = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedCACert && el.caCertIV && el.caCertTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.caCertIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.caCertTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedCACert + }) + : ""; + + const encryptedLdapBindDN = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedBindDN) + }).cipherTextBlob; + const encryptedLdapBindPass = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedBindPass) + }).cipherTextBlob; + const encryptedLdapCaCertificate = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedCertificate) + }).cipherTextBlob; + return { ...el, encryptedLdapBindPass, encryptedLdapBindDN, encryptedLdapCaCertificate }; + } + ) + ); + + for (let i = 0; i < updatedLdapConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.LdapConfig) + .insert(updatedLdapConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + if (hasLdapConfigTable) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + if (!hasEncryptedLdapBindPassColumn) t.binary("encryptedLdapBindPass").notNullable().alter(); + if (!hasEncryptedLdapBindDNColum) t.binary("encryptedLdapBindDN").notNullable().alter(); + }); + } +}; + +const reencryptOidcConfig = async (knex: Knex) => { + const hasEncryptedOidcClientIdColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientId"); + const hasEncryptedOidcClientSecretColumn = await knex.schema.hasColumn( + TableName.OidcConfig, + "encryptedOidcClientSecret" + ); + + const hasEncryptedClientIdColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedClientId"); + const hasClientIdIVColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientIdIV"); + const hasClientIdTagColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientIdTag"); + const hasEncryptedClientSecretColumn = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedClientSecret"); + const hasClientSecretIVColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientSecretIV"); + const hasClientSecretTagColumn = await knex.schema.hasColumn(TableName.OidcConfig, "clientSecretTag"); + + const hasOidcConfigTable = await knex.schema.hasTable(TableName.OidcConfig); + + if (hasOidcConfigTable) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (hasEncryptedClientIdColumn) t.text("encryptedClientId").nullable().alter(); + if (hasClientIdIVColumn) t.string("clientIdIV").nullable().alter(); + if (hasClientIdTagColumn) t.string("clientIdTag").nullable().alter(); + if (hasEncryptedClientSecretColumn) t.text("encryptedClientSecret").nullable().alter(); + if (hasClientSecretIVColumn) t.string("clientSecretIV").nullable().alter(); + if (hasClientSecretTagColumn) t.string("clientSecretTag").nullable().alter(); + + if (!hasEncryptedOidcClientIdColumn) t.binary("encryptedOidcClientId"); + if (!hasEncryptedOidcClientSecretColumn) t.binary("encryptedOidcClientSecret"); + }); + } + + initLogger(); + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + const orgEncryptionRingBuffer = + createCircularCache>>(25); + + const oidcConfigs = await knex(TableName.OidcConfig) + .join(TableName.OrgBot, `${TableName.OrgBot}.orgId`, `${TableName.OidcConfig}.orgId`) + .select(selectAllTableCols(TableName.OidcConfig)) + .select( + knex.ref("encryptedSymmetricKey").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyIV").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyTag").withSchema(TableName.OrgBot), + knex.ref("symmetricKeyKeyEncoding").withSchema(TableName.OrgBot) + ) + .orderBy(`${TableName.OrgBot}.orgId` as "orgId"); + + const updatedOidcConfigs = await Promise.all( + oidcConfigs.map( + async ({ encryptedSymmetricKey, symmetricKeyKeyEncoding, symmetricKeyTag, symmetricKeyIV, ...el }) => { + let orgKmsService = orgEncryptionRingBuffer.getItem(el.orgId); + if (!orgKmsService) { + orgKmsService = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: el.orgId + }, knex); + orgEncryptionRingBuffer.push(el.orgId, orgKmsService); + } + const key = infisicalSymmetricDecrypt({ + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + keyEncoding: symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const decryptedClientId = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedClientId && el.clientIdIV && el.clientIdTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.clientIdIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.clientIdTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedClientId + }) + : ""; + + const decryptedClientSecret = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + el.encryptedClientSecret && el.clientSecretIV && el.clientSecretTag + ? decryptSymmetric({ + key, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + iv: el.clientSecretIV, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + tag: el.clientSecretTag, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This will be removed in next cycle so ignore the ts missing error + ciphertext: el.encryptedClientSecret + }) + : ""; + + const encryptedOidcClientId = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedClientId) + }).cipherTextBlob; + const encryptedOidcClientSecret = orgKmsService.encryptor({ + plainText: Buffer.from(decryptedClientSecret) + }).cipherTextBlob; + return { ...el, encryptedOidcClientId, encryptedOidcClientSecret }; + } + ) + ); + + for (let i = 0; i < updatedOidcConfigs.length; i += BATCH_SIZE) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.OidcConfig) + .insert(updatedOidcConfigs.slice(i, i + BATCH_SIZE)) + .onConflict("id") + .merge(); + } + if (hasOidcConfigTable) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (!hasEncryptedOidcClientIdColumn) t.binary("encryptedOidcClientId").notNullable().alter(); + if (!hasEncryptedOidcClientSecretColumn) t.binary("encryptedOidcClientSecret").notNullable().alter(); + }); + } +}; + +export async function up(knex: Knex): Promise { + await reencryptSamlConfig(knex); + await reencryptLdapConfig(knex); + await reencryptOidcConfig(knex); +} + +const dropSamlConfigColumns = async (knex: Knex) => { + const hasEncryptedEntrypointColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlEntryPoint"); + const hasEncryptedIssuerColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlIssuer"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.SamlConfig, "encryptedSamlCertificate"); + const hasSamlConfigTable = await knex.schema.hasTable(TableName.SamlConfig); + + if (hasSamlConfigTable) { + await knex.schema.alterTable(TableName.SamlConfig, (t) => { + if (hasEncryptedEntrypointColumn) t.dropColumn("encryptedSamlEntryPoint"); + if (hasEncryptedIssuerColumn) t.dropColumn("encryptedSamlIssuer"); + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedSamlCertificate"); + }); + } +}; + +const dropLdapConfigColumns = async (knex: Knex) => { + const hasEncryptedBindDN = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindDN"); + const hasEncryptedBindPass = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapBindPass"); + const hasEncryptedCertificateColumn = await knex.schema.hasColumn(TableName.LdapConfig, "encryptedLdapCaCertificate"); + const hasLdapConfigTable = await knex.schema.hasTable(TableName.LdapConfig); + + if (hasLdapConfigTable) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + if (hasEncryptedBindDN) t.dropColumn("encryptedLdapBindDN"); + if (hasEncryptedBindPass) t.dropColumn("encryptedLdapBindPass"); + if (hasEncryptedCertificateColumn) t.dropColumn("encryptedLdapCaCertificate"); + }); + } +}; + +const dropOidcConfigColumns = async (knex: Knex) => { + const hasEncryptedClientId = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientId"); + const hasEncryptedClientSecret = await knex.schema.hasColumn(TableName.OidcConfig, "encryptedOidcClientSecret"); + const hasOidcConfigTable = await knex.schema.hasTable(TableName.OidcConfig); + + if (hasOidcConfigTable) { + await knex.schema.alterTable(TableName.OidcConfig, (t) => { + if (hasEncryptedClientId) t.dropColumn("encryptedOidcClientId"); + if (hasEncryptedClientSecret) t.dropColumn("encryptedOidcClientSecret"); + }); + } +}; + +export async function down(knex: Knex): Promise { + await dropSamlConfigColumns(knex); + await dropLdapConfigColumns(knex); + await dropOidcConfigColumns(knex); +} diff --git a/backend/src/db/migrations/utils/env-config.ts b/backend/src/db/migrations/utils/env-config.ts new file mode 100644 index 0000000000..05ccae97b5 --- /dev/null +++ b/backend/src/db/migrations/utils/env-config.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +import { zpStr } from "@app/lib/zod"; + +const envSchema = z + .object({ + DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default( + `postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` + ), + DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()), + DB_HOST: zpStr(z.string().describe("Postgres database host").optional()), + DB_PORT: zpStr(z.string().describe("Postgres database port").optional()).default("5432"), + DB_USER: zpStr(z.string().describe("Postgres database username").optional()), + DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()), + DB_NAME: zpStr(z.string().describe("Postgres database name").optional()), + // TODO(akhilmhdh): will be changed to one + ENCRYPTION_KEY: zpStr(z.string().optional()), + ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()), + // HSM + HSM_LIB_PATH: zpStr(z.string().optional()), + HSM_PIN: zpStr(z.string().optional()), + HSM_KEY_LABEL: zpStr(z.string().optional()), + HSM_SLOT: z.coerce.number().optional().default(0) + }) + // To ensure that basic encryption is always possible. + .refine( + (data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY), + "Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined." + ) + .transform((data) => ({ + ...data, + isHsmConfigured: + Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined + })); + +export type TMigrationEnvConfig = z.infer; + +export const getMigrationEnvConfig = () => { + const parsedEnv = envSchema.safeParse(process.env); + if (!parsedEnv.success) { + // eslint-disable-next-line no-console + console.error("Invalid environment variables. Check the error below"); + // eslint-disable-next-line no-console + console.error( + "Migration is now automatic at startup. Please remove this step from your workflow and start the application as normal." + ); + // eslint-disable-next-line no-console + console.error(parsedEnv.error.issues); + process.exit(-1); + } + + return Object.freeze(parsedEnv.data); +}; diff --git a/backend/src/db/migrations/utils/kms.ts b/backend/src/db/migrations/utils/kms.ts deleted file mode 100644 index 9ed0909783..0000000000 --- a/backend/src/db/migrations/utils/kms.ts +++ /dev/null @@ -1,105 +0,0 @@ -import slugify from "@sindresorhus/slugify"; -import { Knex } from "knex"; - -import { TableName } from "@app/db/schemas"; -import { randomSecureBytes } from "@app/lib/crypto"; -import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; -import { alphaNumericNanoId } from "@app/lib/nanoid"; - -const getInstanceRootKey = async (knex: Knex) => { - const encryptionKey = process.env.ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY; - // if root key its base64 encoded - const isBase64 = !process.env.ENCRYPTION_KEY; - if (!encryptionKey) throw new Error("ENCRYPTION_KEY variable needed for migration"); - const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8"); - - const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; - const kmsRootConfig = await knex(TableName.KmsServerRootConfig).where({ id: KMS_ROOT_CONFIG_UUID }).first(); - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - if (kmsRootConfig) { - const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); - // set the flag so that other instancen nodes can start - return decryptedRootKey; - } - - const newRootKey = randomSecureBytes(32); - const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer); - await knex(TableName.KmsServerRootConfig).insert({ - encryptedRootKey, - // eslint-disable-next-line - // @ts-ignore id is kept as fixed for idempotence and to avoid race condition - id: KMS_ROOT_CONFIG_UUID - }); - return encryptedRootKey; -}; - -export const getSecretManagerDataKey = async (knex: Knex, projectId: string) => { - const KMS_VERSION = "v01"; - const KMS_VERSION_BLOB_LENGTH = 3; - const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const project = await knex(TableName.Project).where({ id: projectId }).first(); - if (!project) throw new Error("Missing project id"); - - const ROOT_ENCRYPTION_KEY = await getInstanceRootKey(knex); - - let secretManagerKmsKey; - const projectSecretManagerKmsId = project?.kmsSecretManagerKeyId; - if (projectSecretManagerKmsId) { - const kmsDoc = await knex(TableName.KmsKey) - .leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`) - .where({ [`${TableName.KmsKey}.id` as "id"]: projectSecretManagerKmsId }) - .first(); - if (!kmsDoc) throw new Error("missing kms"); - secretManagerKmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY); - } else { - const [kmsDoc] = await knex(TableName.KmsKey) - .insert({ - name: slugify(alphaNumericNanoId(8).toLowerCase()), - orgId: project.orgId, - isReserved: false - }) - .returning("*"); - - secretManagerKmsKey = randomSecureBytes(32); - const encryptedKeyMaterial = cipher.encrypt(secretManagerKmsKey, ROOT_ENCRYPTION_KEY); - await knex(TableName.InternalKms).insert({ - version: 1, - encryptedKey: encryptedKeyMaterial, - encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, - kmsKeyId: kmsDoc.id - }); - } - - const encryptedSecretManagerDataKey = project?.kmsSecretManagerEncryptedDataKey; - let dataKey: Buffer; - if (!encryptedSecretManagerDataKey) { - dataKey = randomSecureBytes(); - // the below versioning we do it automatically in kms service - const unversionedDataKey = cipher.encrypt(dataKey, secretManagerKmsKey); - const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 - await knex(TableName.Project) - .where({ id: projectId }) - .update({ - kmsSecretManagerEncryptedDataKey: Buffer.concat([unversionedDataKey, versionBlob]) - }); - } else { - const cipherTextBlob = encryptedSecretManagerDataKey.subarray(0, -KMS_VERSION_BLOB_LENGTH); - dataKey = cipher.decrypt(cipherTextBlob, secretManagerKmsKey); - } - - return { - encryptor: ({ plainText }: { plainText: Buffer }) => { - const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey); - - // Buffer#1 encrypted text + Buffer#2 version number - const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 - const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]); - return { cipherTextBlob }; - }, - decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: { cipherTextBlob: Buffer }) => { - const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); - const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey); - return decryptedBlob; - } - }; -}; diff --git a/backend/src/db/migrations/utils/ring-buffer.ts b/backend/src/db/migrations/utils/ring-buffer.ts new file mode 100644 index 0000000000..8e5c586621 --- /dev/null +++ b/backend/src/db/migrations/utils/ring-buffer.ts @@ -0,0 +1,19 @@ +export const createCircularCache = (bufferSize = 10) => { + const bufferItems: { id: string; item: T }[] = []; + let bufferIndex = 0; + + const push = (id: string, item: T) => { + if (bufferItems.length < bufferSize) { + bufferItems.push({ id, item }); + } else { + bufferItems[bufferIndex] = { id, item }; + } + bufferIndex = (bufferIndex + 1) % bufferSize; + }; + + const getItem = (id: string) => { + return bufferItems.find((i) => i.id === id)?.item; + }; + + return { push, getItem }; +}; diff --git a/backend/src/db/migrations/utils/services.ts b/backend/src/db/migrations/utils/services.ts new file mode 100644 index 0000000000..731f703e2c --- /dev/null +++ b/backend/src/db/migrations/utils/services.ts @@ -0,0 +1,52 @@ +import { Knex } from "knex"; + +import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; +import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; +import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; +import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; +import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; +import { kmsServiceFactory } from "@app/services/kms/kms-service"; +import { orgDALFactory } from "@app/services/org/org-dal"; +import { projectDALFactory } from "@app/services/project/project-dal"; + +import { TMigrationEnvConfig } from "./env-config"; + +type TDependencies = { + envConfig: TMigrationEnvConfig; + db: Knex; + keyStore: TKeyStoreFactory; +}; + +export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }: TDependencies) => { + // eslint-disable-next-line no-param-reassign + const hsmModule = initializeHsmModule(envConfig); + hsmModule.initialize(); + + const hsmService = hsmServiceFactory({ + hsmModule: hsmModule.getModule(), + envConfig + }); + + const orgDAL = orgDALFactory(db); + const kmsRootConfigDAL = kmsRootConfigDALFactory(db); + const kmsDAL = kmskeyDALFactory(db); + const internalKmsDAL = internalKmsDALFactory(db); + const projectDAL = projectDALFactory(db); + + const kmsService = kmsServiceFactory({ + kmsRootConfigDAL, + keyStore, + kmsDAL, + internalKmsDAL, + orgDAL, + projectDAL, + hsmService, + envConfig + }); + + await hsmService.startService(); + await kmsService.startService(); + + return { kmsService }; +}; diff --git a/backend/src/db/rename-migrations-to-mjs.ts b/backend/src/db/rename-migrations-to-mjs.ts new file mode 100644 index 0000000000..d09b5097df --- /dev/null +++ b/backend/src/db/rename-migrations-to-mjs.ts @@ -0,0 +1,56 @@ +import path from "node:path"; + +import dotenv from "dotenv"; + +import { initAuditLogDbConnection, initDbConnection } from "./instance"; + +const isProduction = process.env.NODE_ENV === "production"; + +// Update with your config settings. . +dotenv.config({ + path: path.join(__dirname, "../../../.env.migration") +}); +dotenv.config({ + path: path.join(__dirname, "../../../.env") +}); + +const runRename = async () => { + if (!isProduction) return; + const migrationTable = "infisical_migrations"; + const applicationDb = initDbConnection({ + dbConnectionUri: process.env.DB_CONNECTION_URI as string, + dbRootCert: process.env.DB_ROOT_CERT + }); + + const auditLogDb = process.env.AUDIT_LOGS_DB_CONNECTION_URI + ? initAuditLogDbConnection({ + dbConnectionUri: process.env.AUDIT_LOGS_DB_CONNECTION_URI, + dbRootCert: process.env.AUDIT_LOGS_DB_ROOT_CERT + }) + : undefined; + + const hasMigrationTable = await applicationDb.schema.hasTable(migrationTable); + if (hasMigrationTable) { + const firstFile = (await applicationDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await applicationDb(migrationTable).update({ + name: applicationDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + if (auditLogDb) { + const hasMigrationTableInAuditLog = await auditLogDb.schema.hasTable(migrationTable); + if (hasMigrationTableInAuditLog) { + const firstFile = (await auditLogDb(migrationTable).where({}).first()) as { name: string }; + if (firstFile?.name?.includes(".ts")) { + await auditLogDb(migrationTable).update({ + name: auditLogDb.raw("REPLACE(name, '.ts', '.mjs')") + }); + } + } + } + await applicationDb.destroy(); + await auditLogDb?.destroy(); +}; + +void runRename(); diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index b27da396c6..eaddea8fed 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const DynamicSecretsSchema = z.object({ @@ -14,16 +16,17 @@ export const DynamicSecretsSchema = z.object({ type: z.string(), defaultTTL: z.string(), maxTTL: z.string().nullable().optional(), - inputIV: z.string(), - inputCiphertext: z.string(), - inputTag: z.string(), + inputIV: z.string().nullable().optional(), + inputCiphertext: z.string().nullable().optional(), + inputTag: z.string().nullable().optional(), algorithm: z.string().default("aes-256-gcm"), keyEncoding: z.string().default("utf8"), folderId: z.string().uuid(), status: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedInput: zodBuffer }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/identity-kubernetes-auths.ts b/backend/src/db/schemas/identity-kubernetes-auths.ts index ed99dec86b..85f210ff1b 100644 --- a/backend/src/db/schemas/identity-kubernetes-auths.ts +++ b/backend/src/db/schemas/identity-kubernetes-auths.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const IdentityKubernetesAuthsSchema = z.object({ @@ -17,15 +19,17 @@ export const IdentityKubernetesAuthsSchema = z.object({ updatedAt: z.date(), identityId: z.string().uuid(), kubernetesHost: z.string(), - encryptedCaCert: z.string(), - caCertIV: z.string(), - caCertTag: z.string(), - encryptedTokenReviewerJwt: z.string(), - tokenReviewerJwtIV: z.string(), - tokenReviewerJwtTag: z.string(), + encryptedCaCert: z.string().nullable().optional(), + caCertIV: z.string().nullable().optional(), + caCertTag: z.string().nullable().optional(), + encryptedTokenReviewerJwt: z.string().nullable().optional(), + tokenReviewerJwtIV: z.string().nullable().optional(), + tokenReviewerJwtTag: z.string().nullable().optional(), allowedNamespaces: z.string(), allowedNames: z.string(), - allowedAudience: z.string() + allowedAudience: z.string(), + encryptedKubernetesTokenReviewerJwt: zodBuffer, + encryptedKubernetesCaCertificate: zodBuffer.nullable().optional() }); export type TIdentityKubernetesAuths = z.infer; diff --git a/backend/src/db/schemas/identity-oidc-auths.ts b/backend/src/db/schemas/identity-oidc-auths.ts index 3d7d38c41a..ebde5e7dcf 100644 --- a/backend/src/db/schemas/identity-oidc-auths.ts +++ b/backend/src/db/schemas/identity-oidc-auths.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const IdentityOidcAuthsSchema = z.object({ @@ -15,15 +17,16 @@ export const IdentityOidcAuthsSchema = z.object({ accessTokenTrustedIps: z.unknown(), identityId: z.string().uuid(), oidcDiscoveryUrl: z.string(), - encryptedCaCert: z.string(), - caCertIV: z.string(), - caCertTag: z.string(), + encryptedCaCert: z.string().nullable().optional(), + caCertIV: z.string().nullable().optional(), + caCertTag: z.string().nullable().optional(), boundIssuer: z.string(), boundAudiences: z.string(), boundClaims: z.unknown(), boundSubject: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedCaCertificate: zodBuffer.nullable().optional() }); export type TIdentityOidcAuths = z.infer; diff --git a/backend/src/db/schemas/ldap-configs.ts b/backend/src/db/schemas/ldap-configs.ts index 460c2cff66..778e7be6e4 100644 --- a/backend/src/db/schemas/ldap-configs.ts +++ b/backend/src/db/schemas/ldap-configs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const LdapConfigsSchema = z.object({ @@ -12,22 +14,25 @@ export const LdapConfigsSchema = z.object({ orgId: z.string().uuid(), isActive: z.boolean(), url: z.string(), - encryptedBindDN: z.string(), - bindDNIV: z.string(), - bindDNTag: z.string(), - encryptedBindPass: z.string(), - bindPassIV: z.string(), - bindPassTag: z.string(), + encryptedBindDN: z.string().nullable().optional(), + bindDNIV: z.string().nullable().optional(), + bindDNTag: z.string().nullable().optional(), + encryptedBindPass: z.string().nullable().optional(), + bindPassIV: z.string().nullable().optional(), + bindPassTag: z.string().nullable().optional(), searchBase: z.string(), - encryptedCACert: z.string(), - caCertIV: z.string(), - caCertTag: z.string(), + encryptedCACert: z.string().nullable().optional(), + caCertIV: z.string().nullable().optional(), + caCertTag: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), groupSearchBase: z.string().default(""), groupSearchFilter: z.string().default(""), searchFilter: z.string().default(""), - uniqueUserAttribute: z.string().default("") + uniqueUserAttribute: z.string().default(""), + encryptedLdapBindDN: zodBuffer, + encryptedLdapBindPass: zodBuffer, + encryptedLdapCaCertificate: zodBuffer.nullable().optional() }); export type TLdapConfigs = z.infer; diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts index d7bf2f00f7..76923aee82 100644 --- a/backend/src/db/schemas/oidc-configs.ts +++ b/backend/src/db/schemas/oidc-configs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const OidcConfigsSchema = z.object({ @@ -15,20 +17,22 @@ export const OidcConfigsSchema = z.object({ jwksUri: z.string().nullable().optional(), tokenEndpoint: z.string().nullable().optional(), userinfoEndpoint: z.string().nullable().optional(), - encryptedClientId: z.string(), + encryptedClientId: z.string().nullable().optional(), configurationType: z.string(), - clientIdIV: z.string(), - clientIdTag: z.string(), - encryptedClientSecret: z.string(), - clientSecretIV: z.string(), - clientSecretTag: z.string(), + clientIdIV: z.string().nullable().optional(), + clientIdTag: z.string().nullable().optional(), + encryptedClientSecret: z.string().nullable().optional(), + clientSecretIV: z.string().nullable().optional(), + clientSecretTag: z.string().nullable().optional(), allowedEmailDomains: z.string().nullable().optional(), isActive: z.boolean(), createdAt: z.date(), updatedAt: z.date(), orgId: z.string().uuid(), lastUsed: z.date().nullable().optional(), - manageGroupMemberships: z.boolean().default(false) + manageGroupMemberships: z.boolean().default(false), + encryptedOidcClientId: zodBuffer, + encryptedOidcClientSecret: zodBuffer }); export type TOidcConfigs = z.infer; diff --git a/backend/src/db/schemas/saml-configs.ts b/backend/src/db/schemas/saml-configs.ts index 67171469a8..350e84492c 100644 --- a/backend/src/db/schemas/saml-configs.ts +++ b/backend/src/db/schemas/saml-configs.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const SamlConfigsSchema = z.object({ @@ -23,7 +25,10 @@ export const SamlConfigsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), orgId: z.string().uuid(), - lastUsed: z.date().nullable().optional() + lastUsed: z.date().nullable().optional(), + encryptedSamlEntryPoint: zodBuffer, + encryptedSamlIssuer: zodBuffer, + encryptedSamlCertificate: zodBuffer }); export type TSamlConfigs = z.infer; diff --git a/backend/src/db/schemas/secret-rotations.ts b/backend/src/db/schemas/secret-rotations.ts index b491edc469..a3cd04ebb0 100644 --- a/backend/src/db/schemas/secret-rotations.ts +++ b/backend/src/db/schemas/secret-rotations.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const SecretRotationsSchema = z.object({ @@ -22,7 +24,8 @@ export const SecretRotationsSchema = z.object({ keyEncoding: z.string().nullable().optional(), envId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + encryptedRotationData: zodBuffer }); export type TSecretRotations = z.infer; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index a7aac29339..60f031ffff 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -5,12 +5,14 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const WebhooksSchema = z.object({ id: z.string().uuid(), secretPath: z.string().default("/"), - url: z.string(), + url: z.string().nullable().optional(), lastStatus: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(), isDisabled: z.boolean().default(false), @@ -25,7 +27,9 @@ export const WebhooksSchema = z.object({ urlCipherText: z.string().nullable().optional(), urlIV: z.string().nullable().optional(), urlTag: z.string().nullable().optional(), - type: z.string().default("general").nullable().optional() + type: z.string().default("general").nullable().optional(), + encryptedPassKey: zodBuffer.nullable().optional(), + encryptedUrl: zodBuffer }); export type TWebhooks = z.infer; diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index 735ba632c0..2057677cf7 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -14,7 +14,7 @@ import { FastifyRequest } from "fastify"; import LdapStrategy from "passport-ldapauth"; import { z } from "zod"; -import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas"; +import { LdapGroupMapsSchema } from "@app/db/schemas"; import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types"; import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; import { getConfig } from "@app/lib/config/env"; @@ -22,6 +22,7 @@ import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { SanitizedLdapConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerLdapRouter = async (server: FastifyZodProvider) => { @@ -187,7 +188,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { caCert: z.string().trim().default("") }), response: { - 200: LdapConfigsSchema + 200: SanitizedLdapConfigSchema } }, handler: async (req) => { @@ -228,7 +229,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ organizationId: z.string() })), response: { - 200: LdapConfigsSchema + 200: SanitizedLdapConfigSchema } }, handler: async (req) => { diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts index 71daa3446b..df5c61fe49 100644 --- a/backend/src/ee/routes/v1/oidc-router.ts +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -11,13 +11,28 @@ import fastifySession from "@fastify/session"; import RedisStore from "connect-redis"; import { z } from "zod"; -import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs"; +import { OidcConfigsSchema } from "@app/db/schemas"; import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types"; import { getConfig } from "@app/lib/config/env"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({ + id: true, + issuer: true, + authorizationEndpoint: true, + configurationType: true, + discoveryURL: true, + jwksUri: true, + tokenEndpoint: true, + userinfoEndpoint: true, + orgId: true, + isActive: true, + allowedEmailDomains: true, + manageGroupMemberships: true +}); + export const registerOidcRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" }); @@ -142,7 +157,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { orgSlug: z.string().trim() }), response: { - 200: OidcConfigsSchema.pick({ + 200: SanitizedOidcConfigSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, @@ -214,7 +229,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ orgSlug: z.string() })), response: { - 200: OidcConfigsSchema.pick({ + 200: SanitizedOidcConfigSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, @@ -327,20 +342,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { } }), response: { - 200: OidcConfigsSchema.pick({ - id: true, - issuer: true, - authorizationEndpoint: true, - configurationType: true, - discoveryURL: true, - jwksUri: true, - tokenEndpoint: true, - userinfoEndpoint: true, - orgId: true, - isActive: true, - allowedEmailDomains: true, - manageGroupMemberships: true - }) + 200: SanitizedOidcConfigSchema } }, diff --git a/backend/src/ee/routes/v1/project-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index 60f93d65dc..cabf653373 100644 --- a/backend/src/ee/routes/v1/project-template-router.ts +++ b/backend/src/ee/routes/v1/project-template-router.ts @@ -9,7 +9,7 @@ import { ProjectTemplates } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { AuthMode } from "@app/services/auth/auth-type"; const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768; diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index 933015a663..71facb22af 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -12,13 +12,13 @@ import { MultiSamlStrategy } from "@node-saml/passport-saml"; import { FastifyRequest } from "fastify"; import { z } from "zod"; -import { SamlConfigsSchema } from "@app/db/schemas"; import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config"; import { AuthMode } from "@app/services/auth/auth-type"; type TSAMLConfig = { @@ -298,7 +298,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { cert: z.string() }), response: { - 200: SamlConfigsSchema + 200: SanitizedSamlConfigSchema } }, handler: async (req) => { @@ -333,7 +333,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ organizationId: z.string() })), response: { - 200: SamlConfigsSchema + 200: SanitizedSamlConfigSchema } }, handler: async (req) => { diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index bb3e179dd8..de37a4cded 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -9,7 +9,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege"; +import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/sanitizedSchema/user-additional-privilege"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index 7934c3f904..d9c3a05b56 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -9,7 +9,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege"; +import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchema/identitiy-additional-privilege"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts index 8106280309..e9f00f4015 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts @@ -37,11 +37,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"), db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"), db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"), - db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"), - db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"), - db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"), - db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"), - db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"), + db.ref("encryptedInput").withSchema(TableName.DynamicSecret).as("dynEncryptedInput"), db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"), db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"), db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"), @@ -59,11 +55,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { type: doc.dynType, defaultTTL: doc.dynDefaultTTL, maxTTL: doc.dynMaxTTL, - inputIV: doc.dynInputIV, - inputTag: doc.dynInputTag, - inputCiphertext: doc.dynInputCiphertext, - algorithm: doc.dynAlgorithm, - keyEncoding: doc.dynKeyEncoding, + encryptedInput: doc.dynEncryptedInput, folderId: doc.dynFolderId, status: doc.dynStatus, statusDetails: doc.dynStatusDetails, diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index 9bdb1c24e5..fa1a80ac3d 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -1,8 +1,10 @@ -import { SecretKeyEncoding } from "@app/db/schemas"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; @@ -14,6 +16,8 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = { dynamicSecretLeaseDAL: Pick; dynamicSecretDAL: Pick; dynamicSecretProviders: Record; + kmsService: Pick; + folderDAL: Pick; }; export type TDynamicSecretLeaseQueueServiceFactory = ReturnType; @@ -22,7 +26,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ queueService, dynamicSecretDAL, dynamicSecretProviders, - dynamicSecretLeaseDAL + dynamicSecretLeaseDAL, + kmsService, + folderDAL }: TDynamicSecretLeaseQueueServiceFactoryDep) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { await queueService.queue( @@ -76,15 +82,21 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); + const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId); + if (!folder) + throw new NotFoundError({ + message: `Failed to find folder with ${dynamicSecretLease.dynamicSecret.folderId}` + }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: folder.projectId + }); + const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); @@ -100,16 +112,22 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting) throw new DisableRotationErrors({ message: "Document not deleted" }); + const folder = await folderDAL.findById(dynamicSecretCfg.folderId); + if (!folder) + throw new NotFoundError({ + message: `Failed to find folder with ${dynamicSecretCfg.folderId}` + }); + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: folder.projectId + }); + const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId }); if (dynamicSecretLeases.length) { const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 830c1aa57e..39e8ae7e2c 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import ms from "ms"; -import { ActionProjectType, SecretKeyEncoding } from "@app/db/schemas"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { @@ -9,9 +9,10 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -37,6 +38,7 @@ type TDynamicSecretLeaseServiceFactoryDep = { folderDAL: Pick; permissionService: Pick; projectDAL: Pick; + kmsService: Pick; }; export type TDynamicSecretLeaseServiceFactory = ReturnType; @@ -49,7 +51,8 @@ export const dynamicSecretLeaseServiceFactory = ({ permissionService, dynamicSecretQueueService, projectDAL, - licenseService + licenseService, + kmsService }: TDynamicSecretLeaseServiceFactoryDep) => { const create = async ({ environmentSlug, @@ -104,13 +107,14 @@ export const dynamicSecretLeaseServiceFactory = ({ throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() ) as object; const selectedTTL = ttl || dynamicSecretCfg.defaultTTL; @@ -160,6 +164,11 @@ export const dynamicSecretLeaseServiceFactory = ({ subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const plan = await licenseService.getPlan(actorOrgId); if (!plan?.dynamicSecret) { throw new BadRequestError({ @@ -181,12 +190,7 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() ) as object; const selectedTTL = ttl || dynamicSecretCfg.defaultTTL; @@ -240,6 +244,11 @@ export const dynamicSecretLeaseServiceFactory = ({ subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) ); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) throw new NotFoundError({ @@ -253,12 +262,7 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() ) as object; const revokeResponse = await selectedProvider diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 631d5b6ba3..eac5e2ecff 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,15 +1,16 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, SecretKeyEncoding } from "@app/db/schemas"; +import { ActionProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -42,6 +43,7 @@ type TDynamicSecretServiceFactoryDep = { folderDAL: Pick; projectDAL: Pick; permissionService: Pick; + kmsService: Pick; }; export type TDynamicSecretServiceFactory = ReturnType; @@ -54,7 +56,8 @@ export const dynamicSecretServiceFactory = ({ dynamicSecretProviders, permissionService, dynamicSecretQueueService, - projectDAL + projectDAL, + kmsService }: TDynamicSecretServiceFactoryDep) => { const create = async ({ path, @@ -108,16 +111,15 @@ export const dynamicSecretServiceFactory = ({ const isConnected = await selectedProvider.validateConnection(provider.inputs); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); const dynamicSecretCfg = await dynamicSecretDAL.create({ type: provider.type, version: 1, - inputIV: encryptedInput.iv, - inputTag: encryptedInput.tag, - inputCiphertext: encryptedInput.ciphertext, - algorithm: encryptedInput.algorithm, - keyEncoding: encryptedInput.encoding, + encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob, maxTTL, defaultTTL, folderId: folder.id, @@ -180,15 +182,15 @@ export const dynamicSecretServiceFactory = ({ if (existingDynamicSecret) throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" }); } + const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const updatedInput = await selectedProvider.validateProviderInputs(newInput); @@ -196,13 +198,8 @@ export const dynamicSecretServiceFactory = ({ const isConnected = await selectedProvider.validateConnection(newInput); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput)); const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { - inputIV: encryptedInput.iv, - inputTag: encryptedInput.tag, - inputCiphertext: encryptedInput.ciphertext, - algorithm: encryptedInput.algorithm, - keyEncoding: encryptedInput.encoding, + encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob, maxTTL, defaultTTL, name: newName ?? name, @@ -315,13 +312,13 @@ export const dynamicSecretServiceFactory = ({ if (!dynamicSecretCfg) { throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` }); } + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const decryptedStoredInput = JSON.parse( - infisicalSymmetricDecrypt({ - keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, - ciphertext: dynamicSecretCfg.inputCiphertext, - tag: dynamicSecretCfg.inputTag, - iv: dynamicSecretCfg.inputIV - }) + secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() ) as object; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; diff --git a/backend/src/ee/services/hsm/hsm-fns.ts b/backend/src/ee/services/hsm/hsm-fns.ts index 3124e1012d..ef975a371c 100644 --- a/backend/src/ee/services/hsm/hsm-fns.ts +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -1,25 +1,23 @@ import * as pkcs11js from "pkcs11js"; -import { getConfig } from "@app/lib/config/env"; +import { TEnvConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { HsmModule } from "./hsm-types"; -export const initializeHsmModule = () => { - const appCfg = getConfig(); - +export const initializeHsmModule = (envConfig: Pick) => { // Create a new instance of PKCS11 module const pkcs11 = new pkcs11js.PKCS11(); let isInitialized = false; const initialize = () => { - if (!appCfg.isHsmConfigured) { + if (!envConfig.isHsmConfigured) { return; } try { // Load the PKCS#11 module - pkcs11.load(appCfg.HSM_LIB_PATH!); + pkcs11.load(envConfig.HSM_LIB_PATH!); // Initialize the module pkcs11.C_Initialize(); diff --git a/backend/src/ee/services/hsm/hsm-service.ts b/backend/src/ee/services/hsm/hsm-service.ts index a1a0773fc4..d35d17a244 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -1,12 +1,13 @@ import pkcs11js from "pkcs11js"; -import { getConfig } from "@app/lib/config/env"; +import { TEnvConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { HsmKeyType, HsmModule } from "./hsm-types"; type THsmServiceFactoryDep = { hsmModule: HsmModule; + envConfig: Pick; }; export type THsmServiceFactory = ReturnType; @@ -15,9 +16,7 @@ type SyncOrAsync = T | Promise; type SessionCallback = (session: pkcs11js.Handle) => SyncOrAsync; // eslint-disable-next-line no-empty-pattern -export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsmServiceFactoryDep) => { - const appCfg = getConfig(); - +export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envConfig }: THsmServiceFactoryDep) => { // Constants for buffer structures const IV_LENGTH = 16; // Luna HSM typically expects 16-byte IV for cbc const BLOCK_SIZE = 16; @@ -63,11 +62,11 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm throw new Error("No slots available"); } - if (appCfg.HSM_SLOT >= slots.length) { - throw new Error(`HSM slot ${appCfg.HSM_SLOT} not found or not initialized`); + if (envConfig.HSM_SLOT >= slots.length) { + throw new Error(`HSM slot ${envConfig.HSM_SLOT} not found or not initialized`); } - const slotId = slots[appCfg.HSM_SLOT]; + const slotId = slots[envConfig.HSM_SLOT]; const startTime = Date.now(); while (Date.now() - startTime < MAX_TIMEOUT) { @@ -78,7 +77,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm // Login try { - pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, appCfg.HSM_PIN); + pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, envConfig.HSM_PIN); logger.info("HSM: Successfully authenticated"); break; } catch (error) { @@ -86,7 +85,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm if (error instanceof pkcs11js.Pkcs11Error) { if (error.code === pkcs11js.CKR_PIN_INCORRECT) { // We throw instantly here to prevent further attempts, because if too many attempts are made, the HSM will potentially wipe all key material - logger.error(error, `HSM: Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`); + logger.error(error, `HSM: Incorrect PIN detected for HSM slot ${envConfig.HSM_SLOT}`); throw new Error("HSM: Incorrect HSM Pin detected. Please check the HSM configuration."); } if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) { @@ -133,7 +132,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm }; const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => { - const label = type === HsmKeyType.HMAC ? `${appCfg.HSM_KEY_LABEL}_HMAC` : appCfg.HSM_KEY_LABEL; + const label = type === HsmKeyType.HMAC ? `${envConfig.HSM_KEY_LABEL}_HMAC` : envConfig.HSM_KEY_LABEL; const keyType = type === HsmKeyType.HMAC ? pkcs11js.CKK_GENERIC_SECRET : pkcs11js.CKK_AES; const template = [ @@ -360,7 +359,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm }; const isActive = async () => { - if (!isInitialized || !appCfg.isHsmConfigured) { + if (!isInitialized || !envConfig.isHsmConfigured) { return false; } @@ -372,11 +371,11 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm logger.error(err, "HSM: Error testing PKCS#11 module"); } - return appCfg.isHsmConfigured && isInitialized && pkcs11TestPassed; + return envConfig.isHsmConfigured && isInitialized && pkcs11TestPassed; }; const startService = async () => { - if (!appCfg.isHsmConfigured || !pkcs11 || !isInitialized) return; + if (!envConfig.isHsmConfigured || !pkcs11 || !isInitialized) return; try { await $withSession(async (sessionHandle) => { @@ -395,7 +394,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_AES }, { type: pkcs11js.CKA_VALUE_LEN, value: AES_KEY_SIZE / 8 }, - { type: pkcs11js.CKA_LABEL, value: appCfg.HSM_KEY_LABEL! }, + { type: pkcs11js.CKA_LABEL, value: envConfig.HSM_KEY_LABEL! }, { type: pkcs11js.CKA_ENCRYPT, value: true }, // Allow encryption { type: pkcs11js.CKA_DECRYPT, value: true }, // Allow decryption ...genericAttributes @@ -410,7 +409,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm keyTemplate ); - logger.info(`HSM: Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); + logger.info(`HSM: Master key created successfully with label: ${envConfig.HSM_KEY_LABEL}`); } // Check if HMAC key exists, create if not @@ -419,7 +418,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm { type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY }, { type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_GENERIC_SECRET }, { type: pkcs11js.CKA_VALUE_LEN, value: HMAC_KEY_SIZE / 8 }, // 256-bit key - { type: pkcs11js.CKA_LABEL, value: `${appCfg.HSM_KEY_LABEL!}_HMAC` }, + { type: pkcs11js.CKA_LABEL, value: `${envConfig.HSM_KEY_LABEL!}_HMAC` }, { type: pkcs11js.CKA_SIGN, value: true }, // Allow signing { type: pkcs11js.CKA_VERIFY, value: true }, // Allow verification ...genericAttributes @@ -434,7 +433,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsm hmacKeyTemplate ); - logger.info(`HSM: HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`); + logger.info(`HSM: HMAC key created successfully with label: ${envConfig.HSM_KEY_LABEL}_HMAC`); } // Get slot info to check supported mechanisms diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index 3a38c0d65c..eb9c66c1c1 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -5,7 +5,7 @@ import ms from "ms"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission"; +import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 16c0cc2127..d74f9c504f 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -5,7 +5,7 @@ import ms from "ms"; import { ActionProjectType } from "@app/db/schemas"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index cafc7abf04..e22b18e1b9 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,25 +1,18 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; -import { OrgMembershipStatus, SecretKeyEncoding, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; +import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; -import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; @@ -59,7 +52,6 @@ type TLdapConfigServiceFactoryDep = { TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; - orgBotDAL: Pick; groupDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; @@ -84,6 +76,7 @@ type TLdapConfigServiceFactoryDep = { licenseService: Pick; tokenService: Pick; smtpService: Pick; + kmsService: Pick; }; export type TLdapConfigServiceFactory = ReturnType; @@ -93,7 +86,6 @@ export const ldapConfigServiceFactory = ({ ldapGroupMapDAL, orgDAL, orgMembershipDAL, - orgBotDAL, groupDAL, groupProjectDAL, projectKeyDAL, @@ -105,7 +97,8 @@ export const ldapConfigServiceFactory = ({ permissionService, licenseService, tokenService, - smtpService + smtpService, + kmsService }: TLdapConfigServiceFactoryDep) => { const createLdapCfg = async ({ actor, @@ -133,77 +126,23 @@ export const ldapConfigServiceFactory = ({ message: "Failed to create LDAP configuration due to plan restriction. Upgrade plan to create LDAP configuration." }); - - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); - const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - const ldapConfig = await ldapConfigDAL.create({ orgId, isActive, url, - encryptedBindDN, - bindDNIV, - bindDNTag, - encryptedBindPass, - bindPassIV, - bindPassTag, uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, groupSearchFilter, - encryptedCACert, - caCertIV, - caCertTag + encryptedLdapCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob, + encryptedLdapBindDN: encryptor({ plainText: Buffer.from(bindDN) }).cipherTextBlob, + encryptedLdapBindPass: encryptor({ plainText: Buffer.from(bindPass) }).cipherTextBlob }); return ldapConfig; @@ -246,38 +185,21 @@ export const ldapConfigServiceFactory = ({ uniqueUserAttribute }; - const orgBot = await orgBotDAL.findOne({ orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot in organization with ID '${orgId}' not found`, - name: "OrgBotNotFound" - }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); if (bindDN !== undefined) { - const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); - updateQuery.encryptedBindDN = encryptedBindDN; - updateQuery.bindDNIV = bindDNIV; - updateQuery.bindDNTag = bindDNTag; + updateQuery.encryptedLdapBindDN = encryptor({ plainText: Buffer.from(bindDN) }).cipherTextBlob; } if (bindPass !== undefined) { - const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); - updateQuery.encryptedBindPass = encryptedBindPass; - updateQuery.bindPassIV = bindPassIV; - updateQuery.bindPassTag = bindPassTag; + updateQuery.encryptedLdapBindPass = encryptor({ plainText: Buffer.from(bindPass) }).cipherTextBlob; } if (caCert !== undefined) { - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - updateQuery.encryptedCACert = encryptedCACert; - updateQuery.caCertIV = caCertIV; - updateQuery.caCertTag = caCertTag; + updateQuery.encryptedLdapCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; } const [ldapConfig] = await ldapConfigDAL.update({ orgId }, updateQuery); @@ -293,61 +215,24 @@ export const ldapConfigServiceFactory = ({ }); } - const orgBot = await orgBotDAL.findOne({ orgId: ldapConfig.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found in organization with ID ${ldapConfig.orgId}`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: ldapConfig.orgId }); - const { - encryptedBindDN, - bindDNIV, - bindDNTag, - encryptedBindPass, - bindPassIV, - bindPassTag, - encryptedCACert, - caCertIV, - caCertTag - } = ldapConfig; - let bindDN = ""; - if (encryptedBindDN && bindDNIV && bindDNTag) { - bindDN = decryptSymmetric({ - ciphertext: encryptedBindDN, - key, - tag: bindDNTag, - iv: bindDNIV - }); + if (ldapConfig.encryptedLdapBindDN) { + bindDN = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindDN }).toString(); } let bindPass = ""; - if (encryptedBindPass && bindPassIV && bindPassTag) { - bindPass = decryptSymmetric({ - ciphertext: encryptedBindPass, - key, - tag: bindPassTag, - iv: bindPassIV - }); + if (ldapConfig.encryptedLdapBindPass) { + bindPass = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindPass }).toString(); } let caCert = ""; - if (encryptedCACert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCACert, - key, - tag: caCertTag, - iv: caCertIV - }); + if (ldapConfig.encryptedLdapCaCertificate) { + caCert = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapCaCertificate }).toString(); } return { diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 0c037a2d3f..52c8dd5977 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; -import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; +import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; @@ -14,21 +14,14 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; -import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; @@ -70,7 +63,6 @@ type TOidcConfigServiceFactoryDep = { "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; orgMembershipDAL: Pick; - orgBotDAL: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; @@ -91,6 +83,7 @@ type TOidcConfigServiceFactoryDep = { projectDAL: Pick; projectBotDAL: Pick; auditLogService: Pick; + kmsService: Pick; }; export type TOidcConfigServiceFactory = ReturnType; @@ -103,7 +96,6 @@ export const oidcConfigServiceFactory = ({ licenseService, permissionService, tokenService, - orgBotDAL, smtpService, oidcConfigDAL, userGroupMembershipDAL, @@ -112,7 +104,8 @@ export const oidcConfigServiceFactory = ({ projectKeyDAL, projectDAL, projectBotDAL, - auditLogService + auditLogService, + kmsService }: TOidcConfigServiceFactoryDep) => { const getOidc = async (dto: TGetOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: dto.orgSlug }); @@ -143,43 +136,19 @@ export const oidcConfigServiceFactory = ({ }); } - // decrypt and return cfg - const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot for organization with ID '${oidcCfg.orgId}' not found`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: oidcCfg.orgId }); - const { encryptedClientId, clientIdIV, clientIdTag, encryptedClientSecret, clientSecretIV, clientSecretTag } = - oidcCfg; - let clientId = ""; - if (encryptedClientId && clientIdIV && clientIdTag) { - clientId = decryptSymmetric({ - ciphertext: encryptedClientId, - key, - tag: clientIdTag, - iv: clientIdIV - }); + if (oidcCfg.encryptedOidcClientId) { + clientId = decryptor({ cipherTextBlob: oidcCfg.encryptedOidcClientId }).toString(); } let clientSecret = ""; - if (encryptedClientSecret && clientSecretIV && clientSecretTag) { - clientSecret = decryptSymmetric({ - key, - tag: clientSecretTag, - iv: clientSecretIV, - ciphertext: encryptedClientSecret - }); + if (oidcCfg.encryptedOidcClientSecret) { + clientSecret = decryptor({ cipherTextBlob: oidcCfg.encryptedOidcClientSecret }).toString(); } return { @@ -540,12 +509,10 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); - const orgBot = await orgBotDAL.findOne({ orgId: org.id }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot for organization with ID '${org.id}' not found`, - name: "OrgBotNotFound" - }); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: org.id + }); const serverCfg = await getServerCfg(); if (isActive && !serverCfg.trustOidcEmails) { @@ -558,13 +525,6 @@ export const oidcConfigServiceFactory = ({ } } - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - const updateQuery: TOidcConfigsUpdate = { allowedEmailDomains, configurationType, @@ -580,22 +540,11 @@ export const oidcConfigServiceFactory = ({ }; if (clientId !== undefined) { - const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key); - updateQuery.encryptedClientId = encryptedClientId; - updateQuery.clientIdIV = clientIdIV; - updateQuery.clientIdTag = clientIdTag; + updateQuery.encryptedOidcClientId = encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob; } if (clientSecret !== undefined) { - const { - ciphertext: encryptedClientSecret, - iv: clientSecretIV, - tag: clientSecretTag - } = encryptSymmetric(clientSecret, key); - - updateQuery.encryptedClientSecret = encryptedClientSecret; - updateQuery.clientSecretIV = clientSecretIV; - updateQuery.clientSecretTag = clientSecretTag; + updateQuery.encryptedOidcClientSecret = encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob; } const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery); @@ -647,61 +596,11 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId: org.id }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId: org.id, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: org.id }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key); - const { - ciphertext: encryptedClientSecret, - iv: clientSecretIV, - tag: clientSecretTag - } = encryptSymmetric(clientSecret, key); - const oidcCfg = await oidcConfigDAL.create({ issuer, isActive, @@ -713,13 +612,9 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, orgId: org.id, - encryptedClientId, - clientIdIV, - clientIdTag, - encryptedClientSecret, - clientSecretIV, - clientSecretTag, - manageGroupMemberships + manageGroupMemberships, + encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob, + encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob }); return oidcCfg; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index e9ba491278..657e9ce3a5 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -6,7 +6,7 @@ import { CASL_ACTION_SCHEMA_NATIVE_ENUM } from "@app/ee/services/permission/permission-schemas"; import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { PermissionConditionSchema } from "./permission-types"; diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index 5afa58caf4..b2430ac14c 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -15,7 +15,7 @@ import { } from "@app/ee/services/project-template/project-template-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; -import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission"; +import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { getPredefinedRoles } from "@app/services/project-role/project-role-fns"; import { TProjectTemplateDALFactory } from "./project-template-dal"; diff --git a/backend/src/ee/services/project-template/project-template-types.ts b/backend/src/ee/services/project-template/project-template-types.ts index 6b600f3868..c2764dc531 100644 --- a/backend/src/ee/services/project-template/project-template-types.ts +++ b/backend/src/ee/services/project-template/project-template-types.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { TProjectEnvironments } from "@app/db/schemas"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; export type TProjectTemplateEnvironment = Pick; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index 14586d5e2a..6f87663b2d 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -5,7 +5,7 @@ import ms from "ms"; import { ActionProjectType, TableName } from "@app/db/schemas"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 068a455202..f22e2ad581 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -1,29 +1,15 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; -import { - OrgMembershipStatus, - SecretKeyEncoding, - TableName, - TSamlConfigs, - TSamlConfigsUpdate, - TUsers -} from "@app/db/schemas"; +import { OrgMembershipStatus, TableName, TSamlConfigs, TSamlConfigsUpdate, TUsers } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; -import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; @@ -52,21 +38,19 @@ type TSamlConfigServiceFactoryDep = { TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; - identityMetadataDAL: Pick; orgMembershipDAL: Pick; - orgBotDAL: Pick; permissionService: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; + kmsService: Pick; }; export type TSamlConfigServiceFactory = ReturnType; export const samlConfigServiceFactory = ({ samlConfigDAL, - orgBotDAL, orgDAL, orgMembershipDAL, userDAL, @@ -75,7 +59,8 @@ export const samlConfigServiceFactory = ({ licenseService, tokenService, smtpService, - identityMetadataDAL + identityMetadataDAL, + kmsService }: TSamlConfigServiceFactoryDep) => { const createSamlCfg = async ({ cert, @@ -99,70 +84,18 @@ export const samlConfigServiceFactory = ({ "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to create SSO configuration." }); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedEntryPoint, iv: entryPointIV, tag: entryPointTag } = encryptSymmetric(entryPoint, key); - const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); - const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); const samlConfig = await samlConfigDAL.create({ orgId, authProvider, isActive, - encryptedEntryPoint, - entryPointIV, - entryPointTag, - encryptedIssuer, - issuerIV, - issuerTag, - encryptedCert, - certIV, - certTag + encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob, + encryptedSamlEntryPoint: encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob, + encryptedSamlCertificate: encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob }); return samlConfig; @@ -190,40 +123,21 @@ export const samlConfigServiceFactory = ({ }); const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null }; - const orgBot = await orgBotDAL.findOne({ orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot not found for organization with ID '${orgId}'`, - name: "OrgBotNotFound" - }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId }); if (entryPoint !== undefined) { - const { - ciphertext: encryptedEntryPoint, - iv: entryPointIV, - tag: entryPointTag - } = encryptSymmetric(entryPoint, key); - updateQuery.encryptedEntryPoint = encryptedEntryPoint; - updateQuery.entryPointIV = entryPointIV; - updateQuery.entryPointTag = entryPointTag; + updateQuery.encryptedSamlEntryPoint = encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob; } + if (issuer !== undefined) { - const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); - updateQuery.encryptedIssuer = encryptedIssuer; - updateQuery.issuerIV = issuerIV; - updateQuery.issuerTag = issuerTag; + updateQuery.encryptedSamlIssuer = encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob; } + if (cert !== undefined) { - const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); - updateQuery.encryptedCert = encryptedCert; - updateQuery.certIV = certIV; - updateQuery.certTag = certTag; + updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob; } const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery); @@ -233,14 +147,14 @@ export const samlConfigServiceFactory = ({ }; const getSaml = async (dto: TGetSamlCfgDTO) => { - let ssoConfig: TSamlConfigs | undefined; + let samlConfig: TSamlConfigs | undefined; if (dto.type === "org") { - ssoConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); - if (!ssoConfig) return; + samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); + if (!samlConfig) return; } else if (dto.type === "orgSlug") { const org = await orgDAL.findOne({ slug: dto.orgSlug }); if (!org) return; - ssoConfig = await samlConfigDAL.findOne({ orgId: org.id }); + samlConfig = await samlConfigDAL.findOne({ orgId: org.id }); } else if (dto.type === "ssoId") { // TODO: // We made this change because saml config ids were not moved over during the migration @@ -259,81 +173,51 @@ export const samlConfigServiceFactory = ({ const id = UUIDToMongoId[dto.id] ?? dto.id; - ssoConfig = await samlConfigDAL.findById(id); + samlConfig = await samlConfigDAL.findById(id); } - if (!ssoConfig) throw new NotFoundError({ message: `Failed to find SSO data` }); + if (!samlConfig) throw new NotFoundError({ message: `Failed to find SSO data` }); // when dto is type id means it's internally used if (dto.type === "org") { const { permission } = await permissionService.getOrgPermission( dto.actor, dto.actorId, - ssoConfig.orgId, + samlConfig.orgId, dto.actorAuthMethod, dto.actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } - const { - entryPointTag, - entryPointIV, - encryptedEntryPoint, - certTag, - certIV, - encryptedCert, - issuerTag, - issuerIV, - encryptedIssuer - } = ssoConfig; - - const orgBot = await orgBotDAL.findOne({ orgId: ssoConfig.orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot not found in organization with ID '${ssoConfig.orgId}'`, - name: "OrgBotNotFound" - }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: samlConfig.orgId }); let entryPoint = ""; - if (encryptedEntryPoint && entryPointIV && entryPointTag) { - entryPoint = decryptSymmetric({ - ciphertext: encryptedEntryPoint, - key, - tag: entryPointTag, - iv: entryPointIV - }); + if (samlConfig.encryptedSamlEntryPoint) { + entryPoint = decryptor({ cipherTextBlob: samlConfig.encryptedSamlEntryPoint }).toString(); } let issuer = ""; - if (encryptedIssuer && issuerTag && issuerIV) { - issuer = decryptSymmetric({ - key, - tag: issuerTag, - iv: issuerIV, - ciphertext: encryptedIssuer - }); + if (samlConfig.encryptedSamlIssuer) { + issuer = decryptor({ cipherTextBlob: samlConfig.encryptedSamlIssuer }).toString(); } let cert = ""; - if (encryptedCert && certTag && certIV) { - cert = decryptSymmetric({ key, tag: certTag, iv: certIV, ciphertext: encryptedCert }); + if (samlConfig.encryptedSamlCertificate) { + cert = decryptor({ cipherTextBlob: samlConfig.encryptedSamlCertificate }).toString(); } return { - id: ssoConfig.id, - organization: ssoConfig.orgId, - orgId: ssoConfig.orgId, - authProvider: ssoConfig.authProvider, - isActive: ssoConfig.isActive, + id: samlConfig.id, + organization: samlConfig.orgId, + orgId: samlConfig.orgId, + authProvider: samlConfig.authProvider, + isActive: samlConfig.isActive, entryPoint, issuer, cert, - lastUsed: ssoConfig.lastUsed + lastUsed: samlConfig.lastUsed }; }; diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index 355507ecfe..fdc493b9fd 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -5,13 +5,9 @@ import { IAMClient } from "@aws-sdk/client-iam"; -import { SecretKeyEncoding, SecretType } from "@app/db/schemas"; +import { SecretType } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { - encryptSymmetric128BitHexKeyUTF8, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; +import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -135,20 +131,15 @@ export const secretRotationQueueFactory = ({ // deep copy const provider = JSON.parse(JSON.stringify(rotationProvider)) as TSecretRotationProviderTemplate; + const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: secretRotation.projectId + }); - // now get the encrypted variable values - // in includes the inputs, the previous outputs - // internal mapping variables etc - const { encryptedDataTag, encryptedDataIV, encryptedData, keyEncoding } = secretRotation; - if (!encryptedDataTag || !encryptedDataIV || !encryptedData || !keyEncoding) { - throw new DisableRotationErrors({ message: "No inputs found" }); - } - const decryptedData = infisicalSymmetricDecrypt({ - keyEncoding: keyEncoding as SecretKeyEncoding, - ciphertext: encryptedData, - iv: encryptedDataIV, - tag: encryptedDataTag - }); + const decryptedData = secretManagerDecryptor({ + cipherTextBlob: secretRotation.encryptedRotationData + }).toString(); const variables = JSON.parse(decryptedData) as TSecretRotationEncData; // rotation set cycle @@ -303,11 +294,9 @@ export const secretRotationQueueFactory = ({ outputs: newCredential.outputs, internal: newCredential.internal }); - const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables)); - const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId: secretRotation.projectId - }); + const encryptedRotationData = secretManagerEncryptor({ + plainText: Buffer.from(JSON.stringify(variables)) + }).cipherTextBlob; const numberOfSecretsRotated = rotationOutputs.length; if (shouldUseSecretV2Bridge) { @@ -323,11 +312,7 @@ export const secretRotationQueueFactory = ({ await secretRotationDAL.updateById( rotationId, { - encryptedData: encVarData.ciphertext, - encryptedDataIV: encVarData.iv, - encryptedDataTag: encVarData.tag, - keyEncoding: encVarData.encoding, - algorithm: encVarData.algorithm, + encryptedRotationData, lastRotatedAt: new Date(), statusMessage: "Rotated successfull", status: "success" @@ -371,11 +356,7 @@ export const secretRotationQueueFactory = ({ await secretRotationDAL.updateById( rotationId, { - encryptedData: encVarData.ciphertext, - encryptedDataIV: encVarData.iv, - encryptedDataTag: encVarData.tag, - keyEncoding: encVarData.encoding, - algorithm: encVarData.algorithm, + encryptedRotationData, lastRotatedAt: new Date(), statusMessage: "Rotated successfull", status: "success" diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index c456e15815..02da4b7eaf 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -2,9 +2,11 @@ import { ForbiddenError, subject } from "@casl/ability"; import Ajv from "ajv"; import { ActionProjectType, ProjectVersion, TableName } from "@app/db/schemas"; -import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; @@ -30,6 +32,7 @@ type TSecretRotationServiceFactoryDep = { permissionService: Pick; secretRotationQueue: TSecretRotationQueueFactory; projectBotService: Pick; + kmsService: Pick; }; export type TSecretRotationServiceFactory = ReturnType; @@ -44,7 +47,8 @@ export const secretRotationServiceFactory = ({ folderDAL, secretDAL, projectBotService, - secretV2BridgeDAL + secretV2BridgeDAL, + kmsService }: TSecretRotationServiceFactoryDep) => { const getProviderTemplates = async ({ actor, @@ -156,7 +160,11 @@ export const secretRotationServiceFactory = ({ inputs: formattedInputs, creds: [] }; - const encData = infisicalSymmetricEncypt(JSON.stringify(unencryptedData)); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + const secretRotation = await secretRotationDAL.transaction(async (tx) => { const doc = await secretRotationDAL.create( { @@ -164,11 +172,8 @@ export const secretRotationServiceFactory = ({ secretPath, interval, envId: folder.envId, - encryptedDataTag: encData.tag, - encryptedDataIV: encData.iv, - encryptedData: encData.ciphertext, - algorithm: encData.algorithm, - keyEncoding: encData.encoding + encryptedRotationData: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(unencryptedData)) }) + .cipherTextBlob }, tx ); diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 2e4ed0f931..1c34f6b3d4 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ +// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts index 8a9eeab8ce..d8240f27e1 100644 --- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts +++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-await-in-loop */ +/* eslint-disable no-await-in-loop,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ import { Knex } from "knex"; import { z } from "zod"; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index dbfdfd063c..a5f3c24c0e 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -2,6 +2,12 @@ import { Redis } from "ioredis"; import { Redlock, Settings } from "@app/lib/red-lock"; +export enum PgSqlLock { + BootUpMigration = 2023, + SuperAdminInit = 2024, + KmsRootKeyInit = 2025 +} + export type TKeyStoreFactory = ReturnType; // all the key prefixes used must be set here to avoid conflict diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts new file mode 100644 index 0000000000..1fe78cf7ed --- /dev/null +++ b/backend/src/keystore/memory.ts @@ -0,0 +1,38 @@ +import { Lock } from "@app/lib/red-lock"; + +import { TKeyStoreFactory } from "./keystore"; + +export const inMemoryKeyStore = (): TKeyStoreFactory => { + const store: Record = {}; + + return { + setItem: async (key, value) => { + store[key] = value; + return "OK"; + }, + setItemWithExpiry: async (key, value) => { + store[key] = value; + return "OK"; + }, + deleteItem: async (key) => { + delete store[key]; + return 1; + }, + getItem: async (key) => { + const value = store[key]; + if (typeof value === "string") { + return value; + } + return null; + }, + incrementBy: async () => { + return 1; + }, + acquireLock: () => { + return Promise.resolve({ + release: () => {} + }) as Promise; + }, + waitTillReady: async () => {} + }; +}; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 7f0f317283..801e937a2d 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -258,7 +258,8 @@ const envSchema = z SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") })); -let envCfg: Readonly>; +export type TEnvConfig = Readonly>; +let envCfg: TEnvConfig; export const getConfig = () => envCfg; // cannot import singleton logger directly as it needs config to load various transport diff --git a/backend/src/lib/logger/logger.ts b/backend/src/lib/logger/logger.ts index 9676496f72..170a0285fe 100644 --- a/backend/src/lib/logger/logger.ts +++ b/backend/src/lib/logger/logger.ts @@ -98,7 +98,7 @@ const extractReqId = () => { } }; -export const initLogger = async () => { +export const initLogger = () => { const cfg = loggerConfig.parse(process.env); const targets: pino.TransportMultiOptions["targets"][number][] = [ { diff --git a/backend/src/main.ts b/backend/src/main.ts index 850298f89a..461601fc0e 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -2,14 +2,13 @@ import "./lib/telemetry/instrumentation"; import dotenv from "dotenv"; import { Redis } from "ioredis"; -import path from "path"; 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, IS_PACKAGED } from "./lib/config/env"; -import { isMigrationMode } from "./lib/fn"; +import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; @@ -19,58 +18,53 @@ import { smtpServiceFactory } from "./services/smtp/smtp-service"; dotenv.config(); const run = async () => { - const logger = await initLogger(); - const appCfg = initEnvConfig(logger); + const logger = initLogger(); + const envConfig = initEnvConfig(logger); const db = initDbConnection({ - dbConnectionUri: appCfg.DB_CONNECTION_URI, - dbRootCert: appCfg.DB_ROOT_CERT, - readReplicas: appCfg.DB_READ_REPLICAS?.map((el) => ({ + dbConnectionUri: envConfig.DB_CONNECTION_URI, + dbRootCert: envConfig.DB_ROOT_CERT, + readReplicas: envConfig.DB_READ_REPLICAS?.map((el) => ({ dbRootCert: el.DB_ROOT_CERT, dbConnectionUri: el.DB_CONNECTION_URI })) }); - const auditLogDb = appCfg.AUDIT_LOGS_DB_CONNECTION_URI + const auditLogDb = envConfig.AUDIT_LOGS_DB_CONNECTION_URI ? initAuditLogDbConnection({ - dbConnectionUri: appCfg.AUDIT_LOGS_DB_CONNECTION_URI, - dbRootCert: appCfg.AUDIT_LOGS_DB_ROOT_CERT + dbConnectionUri: envConfig.AUDIT_LOGS_DB_CONNECTION_URI, + dbRootCert: envConfig.AUDIT_LOGS_DB_ROOT_CERT }) : undefined; - // Case: App is running in packaged mode (binary), and migration mode is enabled. - // Run the migrations and exit the process after completion. - if (IS_PACKAGED && isMigrationMode()) { - try { - logger.info("Running Postgres migrations.."); - await db.migrate.latest({ - directory: path.join(__dirname, "./db/migrations") - }); - logger.info("Postgres migrations completed"); - } catch (err) { - logger.error(err, "Failed to run migrations"); - process.exit(1); - } - - process.exit(0); - } + await runMigrations({ applicationDb: db, auditLogDb, logger }); const smtp = smtpServiceFactory(formatSmtpConfig()); - const queue = queueServiceFactory(appCfg.REDIS_URL, { - dbConnectionUrl: appCfg.DB_CONNECTION_URI, - dbRootCert: appCfg.DB_ROOT_CERT + const queue = queueServiceFactory(envConfig.REDIS_URL, { + dbConnectionUrl: envConfig.DB_CONNECTION_URI, + dbRootCert: envConfig.DB_ROOT_CERT }); await queue.initialize(); - const keyStore = keyStoreFactory(appCfg.REDIS_URL); - const redis = new Redis(appCfg.REDIS_URL); + const keyStore = keyStoreFactory(envConfig.REDIS_URL); + const redis = new Redis(envConfig.REDIS_URL); - const hsmModule = initializeHsmModule(); + const hsmModule = initializeHsmModule(envConfig); hsmModule.initialize(); - const server = await main({ db, auditLogDb, hsmModule: hsmModule.getModule(), smtp, logger, queue, keyStore, redis }); + const server = await main({ + db, + auditLogDb, + hsmModule: hsmModule.getModule(), + smtp, + logger, + queue, + keyStore, + redis, + envConfig + }); const bootstrap = await bootstrapCheck({ db }); // eslint-disable-next-line @@ -90,8 +84,8 @@ const run = async () => { }); await server.listen({ - port: appCfg.PORT, - host: appCfg.HOST, + port: envConfig.PORT, + host: envConfig.HOST, listenTextResolver: (address) => { void bootstrap(); return address; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index ce1be4a04e..26f556508e 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -17,7 +17,7 @@ import { Knex } from "knex"; import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; +import { getConfig, IS_PACKAGED, TEnvConfig } from "@app/lib/config/env"; import { CustomLogger } from "@app/lib/logger/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TQueueServiceFactory } from "@app/queue"; @@ -43,10 +43,11 @@ type TMain = { keyStore: TKeyStoreFactory; hsmModule: HsmModule; redis: Redis; + envConfig: TEnvConfig; }; // Run the server! -export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore, redis }: TMain) => { +export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore, redis, envConfig }: TMain) => { const appCfg = getConfig(); const server = fastify({ @@ -127,7 +128,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key }) }); - await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule }); + await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule, envConfig }); await server.register(registerServeUI, { standaloneMode: appCfg.STANDALONE_MODE || IS_PACKAGED, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 8cebbdebdb..10da81bf57 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -85,7 +85,7 @@ import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certi 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 } from "@app/lib/config/env"; +import { getConfig, TEnvConfig } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; import { readLimit } from "@app/server/config/rateLimiter"; import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue"; @@ -244,7 +244,8 @@ export const registerRoutes = async ( hsmModule, smtp: smtpService, queue: queueService, - keyStore + keyStore, + envConfig }: { auditLogDb?: Knex; db: Knex; @@ -252,6 +253,7 @@ export const registerRoutes = async ( smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory; + envConfig: TEnvConfig; } ) => { const appCfg = getConfig(); @@ -391,7 +393,8 @@ export const registerRoutes = async ( const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); const hsmService = hsmServiceFactory({ - hsmModule + hsmModule, + envConfig }); const kmsService = kmsServiceFactory({ @@ -401,7 +404,8 @@ export const registerRoutes = async ( internalKmsDAL, orgDAL, projectDAL, - hsmService + hsmService, + envConfig }); const externalKmsService = externalKmsServiceFactory({ @@ -447,7 +451,6 @@ export const registerRoutes = async ( const samlService = samlConfigServiceFactory({ identityMetadataDAL, permissionService, - orgBotDAL, orgDAL, orgMembershipDAL, userDAL, @@ -455,7 +458,8 @@ export const registerRoutes = async ( samlConfigDAL, licenseService, tokenService, - smtpService + smtpService, + kmsService }); const groupService = groupServiceFactory({ userDAL, @@ -506,7 +510,6 @@ export const registerRoutes = async ( ldapGroupMapDAL, orgDAL, orgMembershipDAL, - orgBotDAL, groupDAL, groupProjectDAL, projectKeyDAL, @@ -518,7 +521,8 @@ export const registerRoutes = async ( permissionService, licenseService, tokenService, - smtpService + smtpService, + kmsService }); const telemetryService = telemetryServiceFactory({ @@ -969,7 +973,8 @@ export const registerRoutes = async ( permissionService, webhookDAL, projectEnvDAL, - projectDAL + projectDAL, + kmsService }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); @@ -1149,7 +1154,8 @@ export const registerRoutes = async ( secretDAL, folderDAL, projectBotService, - secretV2BridgeDAL + secretV2BridgeDAL, + kmsService }); const integrationService = integrationServiceFactory({ @@ -1238,9 +1244,9 @@ export const registerRoutes = async ( identityKubernetesAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, - orgBotDAL, permissionService, - licenseService + licenseService, + kmsService }); const identityGcpAuthService = identityGcpAuthServiceFactory({ identityGcpAuthDAL, @@ -1272,7 +1278,7 @@ export const registerRoutes = async ( identityAccessTokenDAL, permissionService, licenseService, - orgBotDAL + kmsService }); const identityJwtAuthService = identityJwtAuthServiceFactory({ @@ -1289,7 +1295,9 @@ export const registerRoutes = async ( queueService, dynamicSecretLeaseDAL, dynamicSecretProviders, - dynamicSecretDAL + dynamicSecretDAL, + folderDAL, + kmsService }); const dynamicSecretService = dynamicSecretServiceFactory({ projectDAL, @@ -1299,7 +1307,8 @@ export const registerRoutes = async ( dynamicSecretProviders, folderDAL, permissionService, - licenseService + licenseService, + kmsService }); const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ projectDAL, @@ -1309,7 +1318,8 @@ export const registerRoutes = async ( dynamicSecretLeaseDAL, dynamicSecretProviders, folderDAL, - licenseService + licenseService, + kmsService }); const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, @@ -1337,7 +1347,7 @@ export const registerRoutes = async ( licenseService, tokenService, smtpService, - orgBotDAL, + kmsService, permissionService, oidcConfigDAL, projectBotDAL, diff --git a/backend/src/server/routes/sanitizedSchema/directory-config.ts b/backend/src/server/routes/sanitizedSchema/directory-config.ts new file mode 100644 index 0000000000..61be4d9cf6 --- /dev/null +++ b/backend/src/server/routes/sanitizedSchema/directory-config.ts @@ -0,0 +1,42 @@ +import { LdapConfigsSchema, OidcConfigsSchema, SamlConfigsSchema } from "@app/db/schemas"; + +export const SanitizedSamlConfigSchema = SamlConfigsSchema.pick({ + id: true, + orgId: true, + isActive: true, + lastUsed: true, + createdAt: true, + updatedAt: true, + authProvider: true +}); + +export const SanitizedLdapConfigSchema = LdapConfigsSchema.pick({ + updatedAt: true, + createdAt: true, + isActive: true, + orgId: true, + id: true, + url: true, + searchBase: true, + searchFilter: true, + groupSearchBase: true, + uniqueUserAttribute: true, + groupSearchFilter: true +}); + +export const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({ + id: true, + orgId: true, + isActive: true, + createdAt: true, + updatedAt: true, + lastUsed: true, + issuer: true, + jwksUri: true, + discoveryURL: true, + tokenEndpoint: true, + userinfoEndpoint: true, + configurationType: true, + allowedEmailDomains: true, + authorizationEndpoint: true +}); diff --git a/backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts b/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts similarity index 100% rename from backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts rename to backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts diff --git a/backend/src/server/routes/santizedSchemas/permission.ts b/backend/src/server/routes/sanitizedSchema/permission.ts similarity index 100% rename from backend/src/server/routes/santizedSchemas/permission.ts rename to backend/src/server/routes/sanitizedSchema/permission.ts diff --git a/backend/src/server/routes/santizedSchemas/user-additional-privilege.ts b/backend/src/server/routes/sanitizedSchema/user-additional-privilege.ts similarity index 100% rename from backend/src/server/routes/santizedSchemas/user-additional-privilege.ts rename to backend/src/server/routes/sanitizedSchema/user-additional-privilege.ts diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 67f01c9ba0..4d645ac4be 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -11,7 +11,7 @@ import { } from "@app/db/schemas"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { UnpackedPermissionSchema } from "./santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "./sanitizedSchema/permission"; // sometimes the return data must be santizied to avoid leaking important values // always prefer pick over omit in zod @@ -201,10 +201,11 @@ export const SanitizedRoleSchemaV1 = ProjectRolesSchema.extend({ }); export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ + encryptedInput: true, + keyEncoding: true, + inputCiphertext: true, inputIV: true, inputTag: true, - inputCiphertext: true, - keyEncoding: true, algorithm: true }); diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 3b30251794..263fa478e8 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -8,13 +8,19 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; -const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.omit({ - encryptedCaCert: true, - caCertIV: true, - caCertTag: true, - encryptedTokenReviewerJwt: true, - tokenReviewerJwtIV: true, - tokenReviewerJwtTag: true +const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick({ + id: true, + accessTokenTTL: true, + accessTokenMaxTTL: true, + accessTokenNumUsesLimit: true, + accessTokenTrustedIps: true, + createdAt: true, + updatedAt: true, + identityId: true, + kubernetesHost: true, + allowedNamespaces: true, + allowedNames: true, + allowedAudience: true }).extend({ caCert: z.string(), tokenReviewerJwt: z.string() diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 431ed3f4f4..7ce0b05b75 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -12,10 +12,20 @@ import { validateOidcBoundClaimsField } from "@app/services/identity-oidc-auth/identity-oidc-auth-validators"; -const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.omit({ - encryptedCaCert: true, - caCertIV: true, - caCertTag: true +const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({ + id: true, + accessTokenTTL: true, + accessTokenMaxTTL: true, + accessTokenNumUsesLimit: true, + accessTokenTrustedIps: true, + identityId: true, + oidcDiscoveryUrl: true, + boundIssuer: true, + boundAudiences: true, + boundClaims: true, + boundSubject: true, + createdAt: true, + updatedAt: true }).extend({ caCert: z.string() }); diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 4508a255da..a5677894d2 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -3,28 +3,21 @@ import axios, { AxiosError } from "axios"; import https from "https"; import jwt from "jsonwebtoken"; -import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { - decryptSymmetric, - encryptSymmetric, - generateAsymmetricKeyPair, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; -import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal"; import { extractK8sUsername } from "./identity-kubernetes-auth-fns"; import { @@ -43,9 +36,9 @@ type TIdentityKubernetesAuthServiceFactoryDep = { >; identityAccessTokenDAL: Pick; identityOrgMembershipDAL: Pick; - orgBotDAL: Pick; permissionService: Pick; licenseService: Pick; + kmsService: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; @@ -54,9 +47,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityKubernetesAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, - orgBotDAL, permissionService, - licenseService + licenseService, + kmsService }: TIdentityKubernetesAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); @@ -75,42 +68,21 @@ export const identityKubernetesAuthServiceFactory = ({ }); } - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } = - identityKubernetesAuth; - let caCert = ""; - if (encryptedCaCert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCaCert, - iv: caCertIV, - tag: caCertTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); } let tokenReviewerJwt = ""; - if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) { - tokenReviewerJwt = decryptSymmetric({ - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { + tokenReviewerJwt = decryptor({ + cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString(); } const { data } = await axios @@ -297,79 +269,25 @@ export const identityKubernetesAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId: identityMembershipOrg.orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - const { - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag - } = encryptSymmetric(tokenReviewerJwt, key); - const identityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const doc = await identityKubernetesAuthDAL.create( { identityId: identityMembershipOrg.identityId, kubernetesHost, - encryptedCaCert, - caCertIV, - caCertTag, - encryptedTokenReviewerJwt, - tokenReviewerJwtIV, - tokenReviewerJwtTag, allowedNamespaces, allowedNames, allowedAudience, accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, - accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), + encryptedKubernetesTokenReviewerJwt: encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob, + encryptedKubernetesCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob }, tx ); @@ -455,61 +373,34 @@ export const identityKubernetesAuthServiceFactory = ({ : undefined }; - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - } - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); if (caCert !== undefined) { - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - updateQuery.encryptedCaCert = encryptedCACert; - updateQuery.caCertIV = caCertIV; - updateQuery.caCertTag = caCertTag; + updateQuery.encryptedKubernetesCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; } if (tokenReviewerJwt !== undefined) { - const { - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag - } = encryptSymmetric(tokenReviewerJwt, key); - updateQuery.encryptedTokenReviewerJwt = encryptedTokenReviewerJwt; - updateQuery.tokenReviewerJwtIV = tokenReviewerJwtIV; - updateQuery.tokenReviewerJwtTag = tokenReviewerJwtTag; + updateQuery.encryptedKubernetesTokenReviewerJwt = encryptor({ + plainText: Buffer.from(tokenReviewerJwt) + }).cipherTextBlob; } const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery); - const updatedCACert = - updatedKubernetesAuth.encryptedCaCert && updatedKubernetesAuth.caCertIV && updatedKubernetesAuth.caCertTag - ? decryptSymmetric({ - ciphertext: updatedKubernetesAuth.encryptedCaCert, - iv: updatedKubernetesAuth.caCertIV, - tag: updatedKubernetesAuth.caCertTag, - key - }) - : ""; + const updatedCACert = updatedKubernetesAuth.encryptedKubernetesCaCertificate + ? decryptor({ + cipherTextBlob: updatedKubernetesAuth.encryptedKubernetesCaCertificate + }).toString() + : ""; - const updatedTokenReviewerJwt = - updatedKubernetesAuth.encryptedTokenReviewerJwt && - updatedKubernetesAuth.tokenReviewerJwtIV && - updatedKubernetesAuth.tokenReviewerJwtTag - ? decryptSymmetric({ - ciphertext: updatedKubernetesAuth.encryptedTokenReviewerJwt, - iv: updatedKubernetesAuth.tokenReviewerJwtIV, - tag: updatedKubernetesAuth.tokenReviewerJwtTag, - key - }) - : ""; + const updatedTokenReviewerJwt = updatedKubernetesAuth.encryptedKubernetesTokenReviewerJwt + ? decryptor({ + cipherTextBlob: updatedKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString() + : ""; return { ...updatedKubernetesAuth, @@ -545,41 +436,21 @@ export const identityKubernetesAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } = - identityKubernetesAuth; - let caCert = ""; - if (encryptedCaCert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCaCert, - iv: caCertIV, - tag: caCertTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); } let tokenReviewerJwt = ""; - if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) { - tokenReviewerJwt = decryptSymmetric({ - ciphertext: encryptedTokenReviewerJwt, - iv: tokenReviewerJwtIV, - tag: tokenReviewerJwtTag, - key - }); + if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { + tokenReviewerJwt = decryptor({ + cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString(); } return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId }; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index a1dbed46b3..ff7256a9cb 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -4,20 +4,12 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { IdentityAuthMethod, SecretKeyEncoding, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; +import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { generateAsymmetricKeyPair } from "@app/lib/crypto"; -import { - decryptSymmetric, - encryptSymmetric, - generateSymmetricKey, - infisicalSymmetricDecrypt, - infisicalSymmetricEncypt -} from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -25,7 +17,8 @@ import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; -import { TOrgBotDALFactory } from "../org/org-bot-dal"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; import { @@ -42,7 +35,7 @@ type TIdentityOidcAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; - orgBotDAL: Pick; + kmsService: Pick; }; export type TIdentityOidcAuthServiceFactory = ReturnType; @@ -53,7 +46,7 @@ export const identityOidcAuthServiceFactory = ({ permissionService, licenseService, identityAccessTokenDAL, - orgBotDAL + kmsService }: TIdentityOidcAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -70,31 +63,14 @@ export const identityOidcAuthServiceFactory = ({ }); } - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID '${identityMembershipOrg.orgId}'`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const { encryptedCaCert, caCertIV, caCertTag } = identityOidcAuth; - let caCert = ""; - if (encryptedCaCert && caCertIV && caCertTag) { - caCert = decryptSymmetric({ - ciphertext: encryptedCaCert, - iv: caCertIV, - tag: caCertTag, - key - }); + if (identityOidcAuth.encryptedCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString(); } const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert }); @@ -264,64 +240,17 @@ export const identityOidcAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); - const orgBot = await orgBotDAL.transaction(async (tx) => { - const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx); - if (doc) return doc; - - const { privateKey, publicKey } = generateAsymmetricKeyPair(); - const key = generateSymmetricKey(); - const { - ciphertext: encryptedPrivateKey, - iv: privateKeyIV, - tag: privateKeyTag, - encoding: privateKeyKeyEncoding, - algorithm: privateKeyAlgorithm - } = infisicalSymmetricEncypt(privateKey); - const { - ciphertext: encryptedSymmetricKey, - iv: symmetricKeyIV, - tag: symmetricKeyTag, - encoding: symmetricKeyKeyEncoding, - algorithm: symmetricKeyAlgorithm - } = infisicalSymmetricEncypt(key); - - return orgBotDAL.create( - { - name: "Infisical org bot", - publicKey, - privateKeyIV, - encryptedPrivateKey, - symmetricKeyIV, - symmetricKeyTag, - encryptedSymmetricKey, - symmetricKeyAlgorithm, - orgId: identityMembershipOrg.orgId, - privateKeyTag, - privateKeyAlgorithm, - privateKeyKeyEncoding, - symmetricKeyKeyEncoding - }, - tx - ); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding - }); - - const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - const identityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { const doc = await identityOidcAuthDAL.create( { identityId: identityMembershipOrg.identityId, oidcDiscoveryUrl, - encryptedCaCert, - caCertIV, - caCertTag, + encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob, boundIssuer, boundAudiences, boundClaims, @@ -415,38 +344,19 @@ export const identityOidcAuthServiceFactory = ({ : undefined }; - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID '${identityMembershipOrg.orgId}'`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); if (caCert !== undefined) { - const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); - updateQuery.encryptedCaCert = encryptedCACert; - updateQuery.caCertIV = caCertIV; - updateQuery.caCertTag = caCertTag; + updateQuery.encryptedCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; } const updatedOidcAuth = await identityOidcAuthDAL.updateById(identityOidcAuth.id, updateQuery); - const updatedCACert = - updatedOidcAuth.encryptedCaCert && updatedOidcAuth.caCertIV && updatedOidcAuth.caCertTag - ? decryptSymmetric({ - ciphertext: updatedOidcAuth.encryptedCaCert, - iv: updatedOidcAuth.caCertIV, - tag: updatedOidcAuth.caCertTag, - key - }) - : ""; + const updatedCACert = updatedOidcAuth.encryptedCaCertificate + ? decryptor({ cipherTextBlob: updatedOidcAuth.encryptedCaCertificate }).toString() + : ""; return { ...updatedOidcAuth, @@ -476,27 +386,14 @@ export const identityOidcAuthServiceFactory = ({ const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); - const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); - if (!orgBot) { - throw new NotFoundError({ - message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`, - name: "OrgBotNotFound" - }); - } - - const key = infisicalSymmetricDecrypt({ - ciphertext: orgBot.encryptedSymmetricKey, - iv: orgBot.symmetricKeyIV, - tag: orgBot.symmetricKeyTag, - keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId }); - const caCert = decryptSymmetric({ - ciphertext: identityOidcAuth.encryptedCaCert, - iv: identityOidcAuth.caCertIV, - tag: identityOidcAuth.caCertTag, - key - }); + const caCert = identityOidcAuth.encryptedCaCertificate + ? decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString() + : ""; return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; }; diff --git a/backend/src/services/kms/kms-root-config-dal.ts b/backend/src/services/kms/kms-root-config-dal.ts index f448e2df86..8745d286e8 100644 --- a/backend/src/services/kms/kms-root-config-dal.ts +++ b/backend/src/services/kms/kms-root-config-dal.ts @@ -1,10 +1,24 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; +import { Knex } from "knex"; export type TKmsRootConfigDALFactory = ReturnType; export const kmsRootConfigDALFactory = (db: TDbClient) => { const kmsOrm = ormify(db, TableName.KmsServerRootConfig); - return kmsOrm; + + const findById = async (id: string, tx?: Knex) => { + try { + const result = await (tx || db)(TableName.KmsServerRootConfig) + .where({ id } as never) + .first("*"); + return result; + } catch (error) { + throw new DatabaseError({ error, name: "Find by id" }); + } + }; + + return { ...kmsOrm, findById }; }; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index c417838603..babba4cb22 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -12,8 +12,8 @@ import { TExternalKmsProviderFns } from "@app/ee/services/external-kms/providers/model"; import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; -import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; -import { getConfig } from "@app/lib/config/env"; +import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; +import { TEnvConfig } from "@app/lib/config/env"; import { randomSecureBytes } from "@app/lib/crypto"; import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; import { generateHash } from "@app/lib/crypto/encryption"; @@ -44,23 +44,22 @@ type TKmsServiceFactoryDep = { kmsDAL: TKmsKeyDALFactory; projectDAL: Pick; orgDAL: Pick; - kmsRootConfigDAL: Pick; + kmsRootConfigDAL: Pick; keyStore: Pick; internalKmsDAL: Pick; hsmService: THsmServiceFactory; + envConfig: Pick; }; export type TKmsServiceFactory = ReturnType; -const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key"; -const KMS_ROOT_CREATION_WAIT_TIME = 10; - // akhilmhdh: Don't edit this value. This is measured for blob concatination in kms const KMS_VERSION = "v01"; const KMS_VERSION_BLOB_LENGTH = 3; const KmsSanitizedSchema = KmsKeysSchema.extend({ isExternal: z.boolean() }); export const kmsServiceFactory = ({ + envConfig, kmsDAL, kmsRootConfigDAL, keyStore, @@ -473,7 +472,8 @@ export const kmsServiceFactory = ({ } const kmsDecryptor = await decryptWithKmsKey({ - kmsId: kmsKeyId + kmsId: kmsKeyId, + tx: trx }); return kmsDecryptor({ @@ -635,10 +635,8 @@ export const kmsServiceFactory = ({ }; const $getBasicEncryptionKey = () => { - const appCfg = getConfig(); - - const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; - const isBase64 = !appCfg.ENCRYPTION_KEY; + const encryptionKey = envConfig.ENCRYPTION_KEY || envConfig.ROOT_ENCRYPTION_KEY; + const isBase64 = !envConfig.ENCRYPTION_KEY; if (!encryptionKey) throw new Error( "Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?" @@ -874,54 +872,33 @@ export const kmsServiceFactory = ({ return { id, name, orgId, isExternal }; }; - // akhilmhdh: a copy of this is made in migrations/utils/kms const startService = async () => { - const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null); - if (!lock) { - await keyStore.waitTillReady({ - key: KMS_ROOT_CREATION_WAIT_KEY, - keyCheckCb: (val) => val === "true", - waitingCb: () => logger.info("KMS. Waiting for leader to finish creation of KMS Root Key") + const kmsRootConfig = await kmsRootConfigDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.KmsRootKeyInit]); + // check if KMS root key was already generated and saved in DB + const existingRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); + if (existingRootConfig) return existingRootConfig; + + logger.info("KMS: Generating new ROOT Key"); + const newRootKey = randomSecureBytes(32); + const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => { + logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key"); + throw err; }); - } - // check if KMS root key was already generated and saved in DB - const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); - - // 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. [strategy=${kmsRootConfig.encryptionStrategy}]`); - - 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"); - logger.info("KMS: Loading ROOT Key into Memory."); - ROOT_ENCRYPTION_KEY = decryptedRootKey; - return; - } - - // case 2: no config is found, so we create a new root key with basic encryption - logger.info("KMS: Generating new ROOT Key"); - const newRootKey = randomSecureBytes(32); - const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => { - logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key"); - throw err; + const newRootConfig = await kmsRootConfigDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: KMS_ROOT_CONFIG_UUID, + encryptedRootKey, + encryptionStrategy: RootKeyEncryptionStrategy.Software + }); + return newRootConfig; }); - await kmsRootConfigDAL.create({ - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - id: KMS_ROOT_CONFIG_UUID, - encryptedRootKey, - encryptionStrategy: RootKeyEncryptionStrategy.Software - }); + 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"); - logger.info("KMS: Saved and loaded ROOT Key into memory"); - if (lock) await lock.release(); - ROOT_ENCRYPTION_KEY = newRootKey; + logger.info("KMS: Loading ROOT Key into Memory."); + ROOT_ENCRYPTION_KEY = decryptedRootKey; }; const updateEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => { diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 09bc6460df..1d695a5ff1 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -9,7 +9,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorAuthMethod } from "../auth/auth-type"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 2c6b8e2dae..83a59b6af4 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -4,8 +4,17 @@ import { ProjectType, TProjectKeys } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; -import { CaStatus } from "../certificate-authority/certificate-authority-types"; -import { KmsType } from "../kms/kms-types"; + +enum KmsType { + External = "external", + Internal = "internal" +} + +enum CaStatus { + ACTIVE = "active", + DISABLED = "disabled", + PENDING_CERTIFICATE = "pending-certificate" +} export enum ProjectFilterType { ID = "id", diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index dc973c0b1f..00b0e7da84 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1488,7 +1488,18 @@ export const secretQueueFactory = ({ }); queueService.start(QueueName.SecretWebhook, async (job) => { - await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL }); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: job.data.projectId + }); + + await fnTriggerWebhook({ + ...job.data, + projectEnvDAL, + webhookDAL, + projectDAL, + secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString() + }); }); return { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index b0fdd9c5cb..9a60754235 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -2,7 +2,7 @@ import bcrypt from "bcrypt"; import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; @@ -87,17 +87,21 @@ export const superAdminServiceFactory = ({ // reset on initialized await keyStore.deleteItem(ADMIN_CONFIG_KEY); - const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); - if (serverCfg) return; + const serverCfg = await serverCfgDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.SuperAdminInit]); + const serverCfgInDB = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (serverCfgInDB) return serverCfgInDB; - const newCfg = await serverCfgDAL.create({ - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - id: ADMIN_CONFIG_DB_UUID, - initialized: false, - allowSignUp: true, - defaultAuthOrgId: null + const newCfg = await serverCfgDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: ADMIN_CONFIG_DB_UUID, + initialized: false, + allowSignUp: true, + defaultAuthOrgId: null + }); + return newCfg; }); - return newCfg; + return serverCfg; }; const updateServerCfg = async ( diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index 58f51f8808..e46f9db2a2 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -3,9 +3,8 @@ import crypto from "node:crypto"; import { AxiosError } from "axios"; import picomatch from "picomatch"; -import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas"; +import { TWebhooks } from "@app/db/schemas"; import { request } from "@app/lib/config/request"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -16,28 +15,14 @@ import { WebhookType } from "./webhook-types"; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; -export const decryptWebhookDetails = (webhook: TWebhooks) => { - const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook; +export const decryptWebhookDetails = (webhook: TWebhooks, decryptor: (value: Buffer) => string) => { + const { encryptedPassKey, encryptedUrl } = webhook; + + const decryptedUrl = decryptor(encryptedUrl); let decryptedSecretKey = ""; - let decryptedUrl = url; - - if (encryptedSecretKey) { - decryptedSecretKey = infisicalSymmetricDecrypt({ - keyEncoding: keyEncoding as SecretKeyEncoding, - ciphertext: encryptedSecretKey, - iv: iv as string, - tag: tag as string - }); - } - - if (urlCipherText) { - decryptedUrl = infisicalSymmetricDecrypt({ - keyEncoding: keyEncoding as SecretKeyEncoding, - ciphertext: urlCipherText, - iv: urlIV as string, - tag: urlTag as string - }); + if (encryptedPassKey) { + decryptedSecretKey = decryptor(encryptedPassKey); } return { @@ -46,10 +31,14 @@ export const decryptWebhookDetails = (webhook: TWebhooks) => { }; }; -export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record) => { +export const triggerWebhookRequest = async ( + webhook: TWebhooks, + decryptor: (value: Buffer) => string, + data: Record +) => { const headers: Record = {}; const payload = { ...data, timestamp: Date.now() }; - const { secretKey, url } = decryptWebhookDetails(webhook); + const { secretKey, url } = decryptWebhookDetails(webhook, decryptor); if (secretKey) { const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex"); @@ -124,6 +113,7 @@ export type TFnTriggerWebhookDTO = { webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; + secretManagerDecryptor: (value: Buffer) => string; }; // this is reusable function @@ -134,7 +124,8 @@ export const fnTriggerWebhook = async ({ projectId, webhookDAL, projectEnvDAL, - projectDAL + projectDAL, + secretManagerDecryptor }: TFnTriggerWebhookDTO) => { const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const toBeTriggeredHooks = webhooks.filter( @@ -148,6 +139,7 @@ export const fnTriggerWebhook = async ({ toBeTriggeredHooks.map((hook) => triggerWebhookRequest( hook, + secretManagerDecryptor, getWebhookPayload("secrets.modified", { workspaceName: project.name, workspaceId: projectId, diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index 26136aaf61..bb078e0f1f 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -3,9 +3,10 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, TWebhooksInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { NotFoundError } from "@app/lib/errors"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; @@ -23,6 +24,7 @@ type TWebhookServiceFactoryDep = { projectEnvDAL: TProjectEnvDALFactory; projectDAL: Pick; permissionService: Pick; + kmsService: Pick; }; export type TWebhookServiceFactory = ReturnType; @@ -31,7 +33,8 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionService, - projectDAL + projectDAL, + kmsService }: TWebhookServiceFactoryDep) => { const createWebhook = async ({ actor, @@ -60,30 +63,20 @@ export const webhookServiceFactory = ({ message: `Environment with slug '${environment}' in project with ID '${projectId}' not found` }); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); const insertDoc: TWebhooksInsert = { - url: "", // deprecated - we are moving away from plaintext URLs envId: env.id, isDisabled: false, secretPath: secretPath || "/", - type + type, + encryptedUrl: secretManagerEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob }; if (webhookSecretKey) { - const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey); - insertDoc.encryptedSecretKey = ciphertext; - insertDoc.iv = iv; - insertDoc.tag = tag; - insertDoc.algorithm = algorithm; - insertDoc.keyEncoding = encoding; - } - - if (webhookUrl) { - const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl); - insertDoc.urlCipherText = ciphertext; - insertDoc.urlIV = iv; - insertDoc.urlTag = tag; - insertDoc.algorithm = algorithm; - insertDoc.keyEncoding = encoding; + insertDoc.encryptedPassKey = secretManagerEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob; } const webhook = await webhookDAL.create(insertDoc); @@ -140,12 +133,17 @@ export const webhookServiceFactory = ({ }); const project = await projectDAL.findById(webhook.projectId); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: project.id + }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); let webhookError: string | undefined; try { await triggerWebhookRequest( webhook, + (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(), getWebhookPayload("test", { workspaceName: project.name, workspaceId: webhook.projectId, @@ -185,8 +183,13 @@ export const webhookServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); + return webhooks.map((w) => { - const { url } = decryptWebhookDetails(w); + const { url } = decryptWebhookDetails(w, (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()); return { ...w, url diff --git a/backend/tsconfig.json b/backend/tsconfig.json index fcf5089223..90165acbe2 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,7 +1,8 @@ { "ts-node": { // Do not forget to `npm i -D tsconfig-paths` - "require": ["tsconfig-paths/register"] + "require": ["tsconfig-paths/register"], + "files": true }, "compilerOptions": { "target": "esnext", @@ -19,6 +20,7 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "moduleResolution": "Node", + "allowSyntheticDefaultImports": true, "skipLibCheck": true, "baseUrl": ".", "paths": { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 40d17c1b01..680a655d83 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -56,20 +56,6 @@ services: POSTGRES_USER: infisical POSTGRES_DB: infisical-test - db-migration: - container_name: infisical-db-migration - depends_on: - - db - build: - context: ./backend - dockerfile: Dockerfile.dev - env_file: .env - environment: - - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable - command: npm run migration:latest - volumes: - - ./backend/src:/app/src - backend: container_name: infisical-dev-api build: @@ -80,8 +66,6 @@ services: condition: service_started redis: condition: service_started - db-migration: - condition: service_completed_successfully env_file: - .env ports: @@ -192,7 +176,7 @@ services: depends_on: - openldap profiles: [ldap] - + keycloak: image: quay.io/keycloak/keycloak:26.1.0 restart: always @@ -202,7 +186,7 @@ services: command: start-dev ports: - 8088:8080 - profiles: [ sso ] + profiles: [sso] volumes: postgres-data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 77a1e04ab7..d3526d8416 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,18 +1,6 @@ version: "3" services: - db-migration: - container_name: infisical-db-migration - depends_on: - db: - condition: service_healthy - image: infisical/infisical:latest-postgres - env_file: .env - command: npm run migration:latest - pull_policy: always - networks: - - infisical - backend: container_name: infisical-backend restart: unless-stopped @@ -21,8 +9,6 @@ services: condition: service_healthy redis: condition: service_started - db-migration: - condition: service_completed_successfully image: infisical/infisical:latest-postgres pull_policy: always env_file: .env @@ -69,4 +55,5 @@ volumes: driver: local networks: - infisical: \ No newline at end of file + infisical: +