diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 9dfb38aeb9..58f2bffebf 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 = initLogger(); - const envConfig = initEnvConfig(logger); + const logger = await initLogger(); + const cfg = initEnvConfig(logger); const db = initDbConnection({ - dbConnectionUri: envConfig.DB_CONNECTION_URI, - dbRootCert: envConfig.DB_ROOT_CERT + dbConnectionUri: cfg.DB_CONNECTION_URI, + dbRootCert: cfg.DB_ROOT_CERT }); - const redis = new Redis(envConfig.REDIS_URL); + const redis = new Redis(cfg.REDIS_URL); await redis.flushdb("SYNC"); try { @@ -42,7 +42,6 @@ export default { }, true ); - await db.migrate.latest({ directory: path.join(__dirname, "../src/db/migrations"), extension: "ts", @@ -53,24 +52,14 @@ export default { directory: path.join(__dirname, "../src/db/seeds"), extension: "ts" }); - const smtp = mockSmtpServer(); - const queue = queueServiceFactory(envConfig.REDIS_URL, { dbConnectionUrl: envConfig.DB_CONNECTION_URI }); - const keyStore = keyStoreFactory(envConfig.REDIS_URL); + const queue = queueServiceFactory(cfg.REDIS_URL, { dbConnectionUrl: cfg.DB_CONNECTION_URI }); + const keyStore = keyStoreFactory(cfg.REDIS_URL); - const hsmModule = initializeHsmModule(envConfig); + const hsmModule = initializeHsmModule(); hsmModule.initialize(); - const server = await main({ - db, - smtp, - logger, - queue, - keyStore, - hsmModule: hsmModule.getModule(), - redis, - envConfig - }); + const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: hsmModule.getModule(), redis }); // @ts-expect-error type globalThis.testServer = server; @@ -84,8 +73,8 @@ export default { organizationId: seedData1.organization.id, accessVersion: 1 }, - envConfig.AUTH_SECRET, - { expiresIn: envConfig.JWT_AUTH_LIFETIME } + cfg.AUTH_SECRET, + { expiresIn: cfg.JWT_AUTH_LIFETIME } ); } catch (error) { // eslint-disable-next-line @@ -120,4 +109,3 @@ export default { }; } }; - diff --git a/backend/package.json b/backend/package.json index 4f8647eab6..43409e619d 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.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", + "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", "migration:new": "tsx ./scripts/create-migration.ts", - "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", + "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", "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 c023470385..f3298625e6 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -93,12 +93,6 @@ 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 deleted file mode 100644 index 88f6dea692..0000000000 --- a/backend/src/auto-start-migrations.ts +++ /dev/null @@ -1,105 +0,0 @@ -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 5a8dd3d059..d4a2a5b2ca 100644 --- a/backend/src/db/instance.ts +++ b/backend/src/db/instance.ts @@ -49,9 +49,6 @@ export const initDbConnection = ({ ca: Buffer.from(dbRootCert, "base64").toString("ascii") } : false - }, - migrations: { - tableName: "infisical_migrations" } }); @@ -67,9 +64,6 @@ export const initDbConnection = ({ ca: Buffer.from(replicaDbCertificate, "base64").toString("ascii") } : false - }, - migrations: { - tableName: "infisical_migrations" } }); }); @@ -104,9 +98,6 @@ 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 8cf80b7445..8af2b59ab2 100644 --- a/backend/src/db/knexfile.ts +++ b/backend/src/db/knexfile.ts @@ -38,8 +38,7 @@ export default { directory: "./seeds" }, migrations: { - tableName: "infisical_migrations", - loadExtensions: [".mjs"] + tableName: "infisical_migrations" } }, production: { @@ -63,8 +62,7 @@ export default { max: 10 }, migrations: { - tableName: "infisical_migrations", - loadExtensions: [".mjs"] + tableName: "infisical_migrations" } } } 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 deleted file mode 100644 index c9b8e7fec5..0000000000 --- a/backend/src/db/migrations/20250210101840_webhook-to-kms.ts +++ /dev/null @@ -1,127 +0,0 @@ -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 deleted file mode 100644 index 41dc6ba9f1..0000000000 --- a/backend/src/db/migrations/20250210101841_dynamic-secret-root-to-kms.ts +++ /dev/null @@ -1,108 +0,0 @@ -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 deleted file mode 100644 index 567cace991..0000000000 --- a/backend/src/db/migrations/20250210101841_secret-rotation-to-kms.ts +++ /dev/null @@ -1,100 +0,0 @@ -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 deleted file mode 100644 index 3d62ab04fb..0000000000 --- a/backend/src/db/migrations/20250210101842_identity-k8-auth-to-kms.ts +++ /dev/null @@ -1,190 +0,0 @@ -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 deleted file mode 100644 index dc87726a47..0000000000 --- a/backend/src/db/migrations/20250210101842_identity-oidc-auth-to-kms.ts +++ /dev/null @@ -1,138 +0,0 @@ -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 deleted file mode 100644 index 05db409584..0000000000 --- a/backend/src/db/migrations/20250210101845_directory-config-to-kms.ts +++ /dev/null @@ -1,484 +0,0 @@ -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 deleted file mode 100644 index 05ccae97b5..0000000000 --- a/backend/src/db/migrations/utils/env-config.ts +++ /dev/null @@ -1,53 +0,0 @@ -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 new file mode 100644 index 0000000000..9ed0909783 --- /dev/null +++ b/backend/src/db/migrations/utils/kms.ts @@ -0,0 +1,105 @@ +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 deleted file mode 100644 index 8e5c586621..0000000000 --- a/backend/src/db/migrations/utils/ring-buffer.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 731f703e2c..0000000000 --- a/backend/src/db/migrations/utils/services.ts +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index d09b5097df..0000000000 --- a/backend/src/db/rename-migrations-to-mjs.ts +++ /dev/null @@ -1,56 +0,0 @@ -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 eaddea8fed..b27da396c6 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const DynamicSecretsSchema = z.object({ @@ -16,17 +14,16 @@ export const DynamicSecretsSchema = z.object({ type: z.string(), defaultTTL: z.string(), maxTTL: z.string().nullable().optional(), - inputIV: z.string().nullable().optional(), - inputCiphertext: z.string().nullable().optional(), - inputTag: z.string().nullable().optional(), + inputIV: z.string(), + inputCiphertext: z.string(), + inputTag: z.string(), 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(), - encryptedInput: zodBuffer + updatedAt: z.date() }); 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 85f210ff1b..ed99dec86b 100644 --- a/backend/src/db/schemas/identity-kubernetes-auths.ts +++ b/backend/src/db/schemas/identity-kubernetes-auths.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const IdentityKubernetesAuthsSchema = z.object({ @@ -19,17 +17,15 @@ export const IdentityKubernetesAuthsSchema = z.object({ updatedAt: z.date(), identityId: z.string().uuid(), kubernetesHost: 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(), + encryptedCaCert: z.string(), + caCertIV: z.string(), + caCertTag: z.string(), + encryptedTokenReviewerJwt: z.string(), + tokenReviewerJwtIV: z.string(), + tokenReviewerJwtTag: z.string(), allowedNamespaces: z.string(), allowedNames: z.string(), - allowedAudience: z.string(), - encryptedKubernetesTokenReviewerJwt: zodBuffer, - encryptedKubernetesCaCertificate: zodBuffer.nullable().optional() + allowedAudience: z.string() }); 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 ebde5e7dcf..3d7d38c41a 100644 --- a/backend/src/db/schemas/identity-oidc-auths.ts +++ b/backend/src/db/schemas/identity-oidc-auths.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const IdentityOidcAuthsSchema = z.object({ @@ -17,16 +15,15 @@ export const IdentityOidcAuthsSchema = z.object({ accessTokenTrustedIps: z.unknown(), identityId: z.string().uuid(), oidcDiscoveryUrl: z.string(), - encryptedCaCert: z.string().nullable().optional(), - caCertIV: z.string().nullable().optional(), - caCertTag: z.string().nullable().optional(), + encryptedCaCert: z.string(), + caCertIV: z.string(), + caCertTag: z.string(), boundIssuer: z.string(), boundAudiences: z.string(), boundClaims: z.unknown(), boundSubject: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date(), - encryptedCaCertificate: zodBuffer.nullable().optional() + updatedAt: z.date() }); export type TIdentityOidcAuths = z.infer; diff --git a/backend/src/db/schemas/ldap-configs.ts b/backend/src/db/schemas/ldap-configs.ts index 778e7be6e4..460c2cff66 100644 --- a/backend/src/db/schemas/ldap-configs.ts +++ b/backend/src/db/schemas/ldap-configs.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const LdapConfigsSchema = z.object({ @@ -14,25 +12,22 @@ export const LdapConfigsSchema = z.object({ orgId: z.string().uuid(), isActive: z.boolean(), url: 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(), + encryptedBindDN: z.string(), + bindDNIV: z.string(), + bindDNTag: z.string(), + encryptedBindPass: z.string(), + bindPassIV: z.string(), + bindPassTag: z.string(), searchBase: z.string(), - encryptedCACert: z.string().nullable().optional(), - caCertIV: z.string().nullable().optional(), - caCertTag: z.string().nullable().optional(), + encryptedCACert: z.string(), + caCertIV: z.string(), + caCertTag: z.string(), createdAt: z.date(), updatedAt: z.date(), groupSearchBase: z.string().default(""), groupSearchFilter: z.string().default(""), searchFilter: z.string().default(""), - uniqueUserAttribute: z.string().default(""), - encryptedLdapBindDN: zodBuffer, - encryptedLdapBindPass: zodBuffer, - encryptedLdapCaCertificate: zodBuffer.nullable().optional() + uniqueUserAttribute: z.string().default("") }); export type TLdapConfigs = z.infer; diff --git a/backend/src/db/schemas/oidc-configs.ts b/backend/src/db/schemas/oidc-configs.ts index 76923aee82..d7bf2f00f7 100644 --- a/backend/src/db/schemas/oidc-configs.ts +++ b/backend/src/db/schemas/oidc-configs.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const OidcConfigsSchema = z.object({ @@ -17,22 +15,20 @@ export const OidcConfigsSchema = z.object({ jwksUri: z.string().nullable().optional(), tokenEndpoint: z.string().nullable().optional(), userinfoEndpoint: z.string().nullable().optional(), - encryptedClientId: z.string().nullable().optional(), + encryptedClientId: z.string(), configurationType: 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(), + clientIdIV: z.string(), + clientIdTag: z.string(), + encryptedClientSecret: z.string(), + clientSecretIV: z.string(), + clientSecretTag: z.string(), 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), - encryptedOidcClientId: zodBuffer, - encryptedOidcClientSecret: zodBuffer + manageGroupMemberships: z.boolean().default(false) }); export type TOidcConfigs = z.infer; diff --git a/backend/src/db/schemas/saml-configs.ts b/backend/src/db/schemas/saml-configs.ts index 350e84492c..67171469a8 100644 --- a/backend/src/db/schemas/saml-configs.ts +++ b/backend/src/db/schemas/saml-configs.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const SamlConfigsSchema = z.object({ @@ -25,10 +23,7 @@ export const SamlConfigsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), orgId: z.string().uuid(), - lastUsed: z.date().nullable().optional(), - encryptedSamlEntryPoint: zodBuffer, - encryptedSamlIssuer: zodBuffer, - encryptedSamlCertificate: zodBuffer + lastUsed: z.date().nullable().optional() }); export type TSamlConfigs = z.infer; diff --git a/backend/src/db/schemas/secret-rotations.ts b/backend/src/db/schemas/secret-rotations.ts index a3cd04ebb0..b491edc469 100644 --- a/backend/src/db/schemas/secret-rotations.ts +++ b/backend/src/db/schemas/secret-rotations.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const SecretRotationsSchema = z.object({ @@ -24,8 +22,7 @@ export const SecretRotationsSchema = z.object({ keyEncoding: z.string().nullable().optional(), envId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date(), - encryptedRotationData: zodBuffer + updatedAt: z.date() }); export type TSecretRotations = z.infer; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index 60f031ffff..a7aac29339 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -5,14 +5,12 @@ 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().nullable().optional(), + url: z.string(), lastStatus: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(), isDisabled: z.boolean().default(false), @@ -27,9 +25,7 @@ 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(), - encryptedPassKey: zodBuffer.nullable().optional(), - encryptedUrl: zodBuffer + type: z.string().default("general").nullable().optional() }); 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 2057677cf7..735ba632c0 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 { LdapGroupMapsSchema } from "@app/db/schemas"; +import { LdapConfigsSchema, 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,7 +22,6 @@ 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) => { @@ -188,7 +187,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { caCert: z.string().trim().default("") }), response: { - 200: SanitizedLdapConfigSchema + 200: LdapConfigsSchema } }, handler: async (req) => { @@ -229,7 +228,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ organizationId: z.string() })), response: { - 200: SanitizedLdapConfigSchema + 200: LdapConfigsSchema } }, handler: async (req) => { diff --git a/backend/src/ee/routes/v1/oidc-router.ts b/backend/src/ee/routes/v1/oidc-router.ts index df5c61fe49..71daa3446b 100644 --- a/backend/src/ee/routes/v1/oidc-router.ts +++ b/backend/src/ee/routes/v1/oidc-router.ts @@ -11,28 +11,13 @@ import fastifySession from "@fastify/session"; import RedisStore from "connect-redis"; import { z } from "zod"; -import { OidcConfigsSchema } from "@app/db/schemas"; +import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs"; 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" }); @@ -157,7 +142,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { orgSlug: z.string().trim() }), response: { - 200: SanitizedOidcConfigSchema.pick({ + 200: OidcConfigsSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, @@ -229,7 +214,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ orgSlug: z.string() })), response: { - 200: SanitizedOidcConfigSchema.pick({ + 200: OidcConfigsSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, @@ -342,7 +327,20 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => { } }), response: { - 200: SanitizedOidcConfigSchema + 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 + }) } }, diff --git a/backend/src/ee/routes/v1/project-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index cabf653373..60f93d65dc 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/sanitizedSchema/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/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 71facb22af..933015a663 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: SanitizedSamlConfigSchema + 200: SamlConfigsSchema } }, handler: async (req) => { @@ -333,7 +333,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { .partial() .merge(z.object({ organizationId: z.string() })), response: { - 200: SanitizedSamlConfigSchema + 200: SamlConfigsSchema } }, 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 de37a4cded..bb3e179dd8 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/sanitizedSchema/user-additional-privilege"; +import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/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 d9c3a05b56..7934c3f904 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/sanitizedSchema/identitiy-additional-privilege"; +import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/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 e9f00f4015..8106280309 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,7 +37,11 @@ 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("encryptedInput").withSchema(TableName.DynamicSecret).as("dynEncryptedInput"), + 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("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"), db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"), db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"), @@ -55,7 +59,11 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { type: doc.dynType, defaultTTL: doc.dynDefaultTTL, maxTTL: doc.dynMaxTTL, - encryptedInput: doc.dynEncryptedInput, + inputIV: doc.dynInputIV, + inputTag: doc.dynInputTag, + inputCiphertext: doc.dynInputCiphertext, + algorithm: doc.dynAlgorithm, + keyEncoding: doc.dynKeyEncoding, 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 fa1a80ac3d..9bdb1c24e5 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,10 +1,8 @@ +import { SecretKeyEncoding } from "@app/db/schemas"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; -import { NotFoundError } from "@app/lib/errors"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; 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"; @@ -16,8 +14,6 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = { dynamicSecretLeaseDAL: Pick; dynamicSecretDAL: Pick; dynamicSecretProviders: Record; - kmsService: Pick; - folderDAL: Pick; }; export type TDynamicSecretLeaseQueueServiceFactory = ReturnType; @@ -26,9 +22,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ queueService, dynamicSecretDAL, dynamicSecretProviders, - dynamicSecretLeaseDAL, - kmsService, - folderDAL + dynamicSecretLeaseDAL }: TDynamicSecretLeaseQueueServiceFactoryDep) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { await queueService.queue( @@ -82,21 +76,15 @@ 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( - secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) ) as object; await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); @@ -112,22 +100,16 @@ 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( - secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) ) 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 39e8ae7e2c..830c1aa57e 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 } from "@app/db/schemas"; +import { ActionProjectType, SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { @@ -9,10 +9,9 @@ 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"; @@ -38,7 +37,6 @@ type TDynamicSecretLeaseServiceFactoryDep = { folderDAL: Pick; permissionService: Pick; projectDAL: Pick; - kmsService: Pick; }; export type TDynamicSecretLeaseServiceFactory = ReturnType; @@ -51,8 +49,7 @@ export const dynamicSecretLeaseServiceFactory = ({ permissionService, dynamicSecretQueueService, projectDAL, - licenseService, - kmsService + licenseService }: TDynamicSecretLeaseServiceFactoryDep) => { const create = async ({ environmentSlug, @@ -107,14 +104,13 @@ 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( - secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) ) as object; const selectedTTL = ttl || dynamicSecretCfg.defaultTTL; @@ -164,11 +160,6 @@ 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({ @@ -190,7 +181,12 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) ) as object; const selectedTTL = ttl || dynamicSecretCfg.defaultTTL; @@ -244,11 +240,6 @@ 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({ @@ -262,7 +253,12 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( - secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) ) 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 eac5e2ecff..631d5b6ba3 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,16 +1,15 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, SecretKeyEncoding } 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"; @@ -43,7 +42,6 @@ type TDynamicSecretServiceFactoryDep = { folderDAL: Pick; projectDAL: Pick; permissionService: Pick; - kmsService: Pick; }; export type TDynamicSecretServiceFactory = ReturnType; @@ -56,8 +54,7 @@ export const dynamicSecretServiceFactory = ({ dynamicSecretProviders, permissionService, dynamicSecretQueueService, - projectDAL, - kmsService + projectDAL }: TDynamicSecretServiceFactoryDep) => { const create = async ({ path, @@ -111,15 +108,16 @@ export const dynamicSecretServiceFactory = ({ const isConnected = await selectedProvider.validateConnection(provider.inputs); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); + const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); const dynamicSecretCfg = await dynamicSecretDAL.create({ type: provider.type, version: 1, - encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob, + inputIV: encryptedInput.iv, + inputTag: encryptedInput.tag, + inputCiphertext: encryptedInput.ciphertext, + algorithm: encryptedInput.algorithm, + keyEncoding: encryptedInput.encoding, maxTTL, defaultTTL, folderId: folder.id, @@ -182,15 +180,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( - secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) ) as object; const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const updatedInput = await selectedProvider.validateProviderInputs(newInput); @@ -198,8 +196,13 @@ 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, { - encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob, + inputIV: encryptedInput.iv, + inputTag: encryptedInput.tag, + inputCiphertext: encryptedInput.ciphertext, + algorithm: encryptedInput.algorithm, + keyEncoding: encryptedInput.encoding, maxTTL, defaultTTL, name: newName ?? name, @@ -312,13 +315,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( - secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString() + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) ) 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 ef975a371c..3124e1012d 100644 --- a/backend/src/ee/services/hsm/hsm-fns.ts +++ b/backend/src/ee/services/hsm/hsm-fns.ts @@ -1,23 +1,25 @@ import * as pkcs11js from "pkcs11js"; -import { TEnvConfig } from "@app/lib/config/env"; +import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { HsmModule } from "./hsm-types"; -export const initializeHsmModule = (envConfig: Pick) => { +export const initializeHsmModule = () => { + const appCfg = getConfig(); + // Create a new instance of PKCS11 module const pkcs11 = new pkcs11js.PKCS11(); let isInitialized = false; const initialize = () => { - if (!envConfig.isHsmConfigured) { + if (!appCfg.isHsmConfigured) { return; } try { // Load the PKCS#11 module - pkcs11.load(envConfig.HSM_LIB_PATH!); + pkcs11.load(appCfg.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 d35d17a244..a1a0773fc4 100644 --- a/backend/src/ee/services/hsm/hsm-service.ts +++ b/backend/src/ee/services/hsm/hsm-service.ts @@ -1,13 +1,12 @@ import pkcs11js from "pkcs11js"; -import { TEnvConfig } from "@app/lib/config/env"; +import { getConfig } 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; @@ -16,7 +15,9 @@ type SyncOrAsync = T | Promise; type SessionCallback = (session: pkcs11js.Handle) => SyncOrAsync; // eslint-disable-next-line no-empty-pattern -export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envConfig }: THsmServiceFactoryDep) => { +export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsmServiceFactoryDep) => { + const appCfg = getConfig(); + // Constants for buffer structures const IV_LENGTH = 16; // Luna HSM typically expects 16-byte IV for cbc const BLOCK_SIZE = 16; @@ -62,11 +63,11 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon throw new Error("No slots available"); } - if (envConfig.HSM_SLOT >= slots.length) { - throw new Error(`HSM slot ${envConfig.HSM_SLOT} not found or not initialized`); + if (appCfg.HSM_SLOT >= slots.length) { + throw new Error(`HSM slot ${appCfg.HSM_SLOT} not found or not initialized`); } - const slotId = slots[envConfig.HSM_SLOT]; + const slotId = slots[appCfg.HSM_SLOT]; const startTime = Date.now(); while (Date.now() - startTime < MAX_TIMEOUT) { @@ -77,7 +78,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon // Login try { - pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, envConfig.HSM_PIN); + pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, appCfg.HSM_PIN); logger.info("HSM: Successfully authenticated"); break; } catch (error) { @@ -85,7 +86,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon 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 ${envConfig.HSM_SLOT}`); + logger.error(error, `HSM: Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`); throw new Error("HSM: Incorrect HSM Pin detected. Please check the HSM configuration."); } if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) { @@ -132,7 +133,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon }; const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => { - const label = type === HsmKeyType.HMAC ? `${envConfig.HSM_KEY_LABEL}_HMAC` : envConfig.HSM_KEY_LABEL; + const label = type === HsmKeyType.HMAC ? `${appCfg.HSM_KEY_LABEL}_HMAC` : appCfg.HSM_KEY_LABEL; const keyType = type === HsmKeyType.HMAC ? pkcs11js.CKK_GENERIC_SECRET : pkcs11js.CKK_AES; const template = [ @@ -359,7 +360,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon }; const isActive = async () => { - if (!isInitialized || !envConfig.isHsmConfigured) { + if (!isInitialized || !appCfg.isHsmConfigured) { return false; } @@ -371,11 +372,11 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon logger.error(err, "HSM: Error testing PKCS#11 module"); } - return envConfig.isHsmConfigured && isInitialized && pkcs11TestPassed; + return appCfg.isHsmConfigured && isInitialized && pkcs11TestPassed; }; const startService = async () => { - if (!envConfig.isHsmConfigured || !pkcs11 || !isInitialized) return; + if (!appCfg.isHsmConfigured || !pkcs11 || !isInitialized) return; try { await $withSession(async (sessionHandle) => { @@ -394,7 +395,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon { 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: envConfig.HSM_KEY_LABEL! }, + { type: pkcs11js.CKA_LABEL, value: appCfg.HSM_KEY_LABEL! }, { type: pkcs11js.CKA_ENCRYPT, value: true }, // Allow encryption { type: pkcs11js.CKA_DECRYPT, value: true }, // Allow decryption ...genericAttributes @@ -409,7 +410,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon keyTemplate ); - logger.info(`HSM: Master key created successfully with label: ${envConfig.HSM_KEY_LABEL}`); + logger.info(`HSM: Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); } // Check if HMAC key exists, create if not @@ -418,7 +419,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon { 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: `${envConfig.HSM_KEY_LABEL!}_HMAC` }, + { type: pkcs11js.CKA_LABEL, value: `${appCfg.HSM_KEY_LABEL!}_HMAC` }, { type: pkcs11js.CKA_SIGN, value: true }, // Allow signing { type: pkcs11js.CKA_VERIFY, value: true }, // Allow verification ...genericAttributes @@ -433,7 +434,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon hmacKeyTemplate ); - logger.info(`HSM: HMAC key created successfully with label: ${envConfig.HSM_KEY_LABEL}_HMAC`); + logger.info(`HSM: HMAC key created successfully with label: ${appCfg.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 eb9c66c1c1..3a38c0d65c 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/sanitizedSchema/permission"; +import { unpackPermissions } from "@app/server/routes/santizedSchemas/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 d74f9c504f..16c0cc2127 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/sanitizedSchema/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/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 e22b18e1b9..cafc7abf04 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,18 +1,25 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; -import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; +import { OrgMembershipStatus, SecretKeyEncoding, 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 { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; 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,6 +59,7 @@ type TLdapConfigServiceFactoryDep = { TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; + orgBotDAL: Pick; groupDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; @@ -76,7 +84,6 @@ type TLdapConfigServiceFactoryDep = { licenseService: Pick; tokenService: Pick; smtpService: Pick; - kmsService: Pick; }; export type TLdapConfigServiceFactory = ReturnType; @@ -86,6 +93,7 @@ export const ldapConfigServiceFactory = ({ ldapGroupMapDAL, orgDAL, orgMembershipDAL, + orgBotDAL, groupDAL, groupProjectDAL, projectKeyDAL, @@ -97,8 +105,7 @@ export const ldapConfigServiceFactory = ({ permissionService, licenseService, tokenService, - smtpService, - kmsService + smtpService }: TLdapConfigServiceFactoryDep) => { const createLdapCfg = async ({ actor, @@ -126,23 +133,77 @@ export const ldapConfigServiceFactory = ({ message: "Failed to create LDAP configuration due to plan restriction. Upgrade plan to create LDAP configuration." }); - const { encryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId + + 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 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, - encryptedLdapCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob, - encryptedLdapBindDN: encryptor({ plainText: Buffer.from(bindDN) }).cipherTextBlob, - encryptedLdapBindPass: encryptor({ plainText: Buffer.from(bindPass) }).cipherTextBlob + encryptedCACert, + caCertIV, + caCertTag }); return ldapConfig; @@ -185,21 +246,38 @@ export const ldapConfigServiceFactory = ({ uniqueUserAttribute }; - const { encryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId + 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 }); if (bindDN !== undefined) { - updateQuery.encryptedLdapBindDN = encryptor({ plainText: Buffer.from(bindDN) }).cipherTextBlob; + const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); + updateQuery.encryptedBindDN = encryptedBindDN; + updateQuery.bindDNIV = bindDNIV; + updateQuery.bindDNTag = bindDNTag; } if (bindPass !== undefined) { - updateQuery.encryptedLdapBindPass = encryptor({ plainText: Buffer.from(bindPass) }).cipherTextBlob; + const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); + updateQuery.encryptedBindPass = encryptedBindPass; + updateQuery.bindPassIV = bindPassIV; + updateQuery.bindPassTag = bindPassTag; } if (caCert !== undefined) { - updateQuery.encryptedLdapCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; + const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); + updateQuery.encryptedCACert = encryptedCACert; + updateQuery.caCertIV = caCertIV; + updateQuery.caCertTag = caCertTag; } const [ldapConfig] = await ldapConfigDAL.update({ orgId }, updateQuery); @@ -215,24 +293,61 @@ export const ldapConfigServiceFactory = ({ }); } - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: ldapConfig.orgId + 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 { + encryptedBindDN, + bindDNIV, + bindDNTag, + encryptedBindPass, + bindPassIV, + bindPassTag, + encryptedCACert, + caCertIV, + caCertTag + } = ldapConfig; + let bindDN = ""; - if (ldapConfig.encryptedLdapBindDN) { - bindDN = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindDN }).toString(); + if (encryptedBindDN && bindDNIV && bindDNTag) { + bindDN = decryptSymmetric({ + ciphertext: encryptedBindDN, + key, + tag: bindDNTag, + iv: bindDNIV + }); } let bindPass = ""; - if (ldapConfig.encryptedLdapBindPass) { - bindPass = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindPass }).toString(); + if (encryptedBindPass && bindPassIV && bindPassTag) { + bindPass = decryptSymmetric({ + ciphertext: encryptedBindPass, + key, + tag: bindPassTag, + iv: bindPassIV + }); } let caCert = ""; - if (ldapConfig.encryptedLdapCaCertificate) { - caCert = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapCaCertificate }).toString(); + if (encryptedCACert && caCertIV && caCertTag) { + caCert = decryptSymmetric({ + ciphertext: encryptedCACert, + key, + tag: caCertTag, + iv: caCertIV + }); } return { diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 52c8dd5977..0c037a2d3f 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, TableName, TUsers } from "@app/db/schemas"; +import { OrgMembershipStatus, SecretKeyEncoding, 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,14 +14,21 @@ 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 { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; 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"; @@ -63,6 +70,7 @@ type TOidcConfigServiceFactoryDep = { "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; orgMembershipDAL: Pick; + orgBotDAL: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; @@ -83,7 +91,6 @@ type TOidcConfigServiceFactoryDep = { projectDAL: Pick; projectBotDAL: Pick; auditLogService: Pick; - kmsService: Pick; }; export type TOidcConfigServiceFactory = ReturnType; @@ -96,6 +103,7 @@ export const oidcConfigServiceFactory = ({ licenseService, permissionService, tokenService, + orgBotDAL, smtpService, oidcConfigDAL, userGroupMembershipDAL, @@ -104,8 +112,7 @@ export const oidcConfigServiceFactory = ({ projectKeyDAL, projectDAL, projectBotDAL, - auditLogService, - kmsService + auditLogService }: TOidcConfigServiceFactoryDep) => { const getOidc = async (dto: TGetOidcCfgDTO) => { const org = await orgDAL.findOne({ slug: dto.orgSlug }); @@ -136,19 +143,43 @@ export const oidcConfigServiceFactory = ({ }); } - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: oidcCfg.orgId + // 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 { encryptedClientId, clientIdIV, clientIdTag, encryptedClientSecret, clientSecretIV, clientSecretTag } = + oidcCfg; + let clientId = ""; - if (oidcCfg.encryptedOidcClientId) { - clientId = decryptor({ cipherTextBlob: oidcCfg.encryptedOidcClientId }).toString(); + if (encryptedClientId && clientIdIV && clientIdTag) { + clientId = decryptSymmetric({ + ciphertext: encryptedClientId, + key, + tag: clientIdTag, + iv: clientIdIV + }); } let clientSecret = ""; - if (oidcCfg.encryptedOidcClientSecret) { - clientSecret = decryptor({ cipherTextBlob: oidcCfg.encryptedOidcClientSecret }).toString(); + if (encryptedClientSecret && clientSecretIV && clientSecretTag) { + clientSecret = decryptSymmetric({ + key, + tag: clientSecretTag, + iv: clientSecretIV, + ciphertext: encryptedClientSecret + }); } return { @@ -509,10 +540,12 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); - const { encryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: org.id - }); + 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 serverCfg = await getServerCfg(); if (isActive && !serverCfg.trustOidcEmails) { @@ -525,6 +558,13 @@ 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, @@ -540,11 +580,22 @@ export const oidcConfigServiceFactory = ({ }; if (clientId !== undefined) { - updateQuery.encryptedOidcClientId = encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob; + const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key); + updateQuery.encryptedClientId = encryptedClientId; + updateQuery.clientIdIV = clientIdIV; + updateQuery.clientIdTag = clientIdTag; } if (clientSecret !== undefined) { - updateQuery.encryptedOidcClientSecret = encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob; + const { + ciphertext: encryptedClientSecret, + iv: clientSecretIV, + tag: clientSecretTag + } = encryptSymmetric(clientSecret, key); + + updateQuery.encryptedClientSecret = encryptedClientSecret; + updateQuery.clientSecretIV = clientSecretIV; + updateQuery.clientSecretTag = clientSecretTag; } const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery); @@ -596,11 +647,61 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); - const { encryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: org.id + 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 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, @@ -612,9 +713,13 @@ export const oidcConfigServiceFactory = ({ tokenEndpoint, userinfoEndpoint, orgId: org.id, - manageGroupMemberships, - encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob, - encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob + encryptedClientId, + clientIdIV, + clientIdTag, + encryptedClientSecret, + clientSecretIV, + clientSecretTag, + manageGroupMemberships }); return oidcCfg; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 657e9ce3a5..e9ba491278 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/sanitizedSchema/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/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 b2430ac14c..5afa58caf4 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/sanitizedSchema/permission"; +import { unpackPermissions } from "@app/server/routes/santizedSchemas/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 c2764dc531..6b600f3868 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/sanitizedSchema/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/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 6f87663b2d..14586d5e2a 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/sanitizedSchema/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/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 f22e2ad581..068a455202 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -1,15 +1,29 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; -import { OrgMembershipStatus, TableName, TSamlConfigs, TSamlConfigsUpdate, TUsers } from "@app/db/schemas"; +import { + OrgMembershipStatus, + SecretKeyEncoding, + 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 { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; 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"; @@ -38,19 +52,21 @@ 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, @@ -59,8 +75,7 @@ export const samlConfigServiceFactory = ({ licenseService, tokenService, smtpService, - identityMetadataDAL, - kmsService + identityMetadataDAL }: TSamlConfigServiceFactoryDep) => { const createSamlCfg = async ({ cert, @@ -84,18 +99,70 @@ export const samlConfigServiceFactory = ({ "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to create SSO configuration." }); - const { encryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId + 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 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, - encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob, - encryptedSamlEntryPoint: encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob, - encryptedSamlCertificate: encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob + encryptedEntryPoint, + entryPointIV, + entryPointTag, + encryptedIssuer, + issuerIV, + issuerTag, + encryptedCert, + certIV, + certTag }); return samlConfig; @@ -123,21 +190,40 @@ export const samlConfigServiceFactory = ({ }); const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null }; - const { encryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId + 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 }); if (entryPoint !== undefined) { - updateQuery.encryptedSamlEntryPoint = encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob; + const { + ciphertext: encryptedEntryPoint, + iv: entryPointIV, + tag: entryPointTag + } = encryptSymmetric(entryPoint, key); + updateQuery.encryptedEntryPoint = encryptedEntryPoint; + updateQuery.entryPointIV = entryPointIV; + updateQuery.entryPointTag = entryPointTag; } - if (issuer !== undefined) { - updateQuery.encryptedSamlIssuer = encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob; + const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); + updateQuery.encryptedIssuer = encryptedIssuer; + updateQuery.issuerIV = issuerIV; + updateQuery.issuerTag = issuerTag; } - if (cert !== undefined) { - updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob; + const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); + updateQuery.encryptedCert = encryptedCert; + updateQuery.certIV = certIV; + updateQuery.certTag = certTag; } const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery); @@ -147,14 +233,14 @@ export const samlConfigServiceFactory = ({ }; const getSaml = async (dto: TGetSamlCfgDTO) => { - let samlConfig: TSamlConfigs | undefined; + let ssoConfig: TSamlConfigs | undefined; if (dto.type === "org") { - samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); - if (!samlConfig) return; + ssoConfig = await samlConfigDAL.findOne({ orgId: dto.orgId }); + if (!ssoConfig) return; } else if (dto.type === "orgSlug") { const org = await orgDAL.findOne({ slug: dto.orgSlug }); if (!org) return; - samlConfig = await samlConfigDAL.findOne({ orgId: org.id }); + ssoConfig = 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 @@ -173,51 +259,81 @@ export const samlConfigServiceFactory = ({ const id = UUIDToMongoId[dto.id] ?? dto.id; - samlConfig = await samlConfigDAL.findById(id); + ssoConfig = await samlConfigDAL.findById(id); } - if (!samlConfig) throw new NotFoundError({ message: `Failed to find SSO data` }); + if (!ssoConfig) 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, - samlConfig.orgId, + ssoConfig.orgId, dto.actorAuthMethod, dto.actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: samlConfig.orgId + 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 }); let entryPoint = ""; - if (samlConfig.encryptedSamlEntryPoint) { - entryPoint = decryptor({ cipherTextBlob: samlConfig.encryptedSamlEntryPoint }).toString(); + if (encryptedEntryPoint && entryPointIV && entryPointTag) { + entryPoint = decryptSymmetric({ + ciphertext: encryptedEntryPoint, + key, + tag: entryPointTag, + iv: entryPointIV + }); } let issuer = ""; - if (samlConfig.encryptedSamlIssuer) { - issuer = decryptor({ cipherTextBlob: samlConfig.encryptedSamlIssuer }).toString(); + if (encryptedIssuer && issuerTag && issuerIV) { + issuer = decryptSymmetric({ + key, + tag: issuerTag, + iv: issuerIV, + ciphertext: encryptedIssuer + }); } let cert = ""; - if (samlConfig.encryptedSamlCertificate) { - cert = decryptor({ cipherTextBlob: samlConfig.encryptedSamlCertificate }).toString(); + if (encryptedCert && certTag && certIV) { + cert = decryptSymmetric({ key, tag: certTag, iv: certIV, ciphertext: encryptedCert }); } return { - id: samlConfig.id, - organization: samlConfig.orgId, - orgId: samlConfig.orgId, - authProvider: samlConfig.authProvider, - isActive: samlConfig.isActive, + id: ssoConfig.id, + organization: ssoConfig.orgId, + orgId: ssoConfig.orgId, + authProvider: ssoConfig.authProvider, + isActive: ssoConfig.isActive, entryPoint, issuer, cert, - lastUsed: samlConfig.lastUsed + lastUsed: ssoConfig.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 fdc493b9fd..355507ecfe 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,9 +5,13 @@ import { IAMClient } from "@aws-sdk/client-iam"; -import { SecretType } from "@app/db/schemas"; +import { SecretKeyEncoding, SecretType } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption"; +import { + encryptSymmetric128BitHexKeyUTF8, + infisicalSymmetricDecrypt, + infisicalSymmetricEncypt +} from "@app/lib/crypto/encryption"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -131,15 +135,20 @@ 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 - }); - const decryptedData = secretManagerDecryptor({ - cipherTextBlob: secretRotation.encryptedRotationData - }).toString(); + // 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 variables = JSON.parse(decryptedData) as TSecretRotationEncData; // rotation set cycle @@ -294,9 +303,11 @@ export const secretRotationQueueFactory = ({ outputs: newCredential.outputs, internal: newCredential.internal }); - const encryptedRotationData = secretManagerEncryptor({ - plainText: Buffer.from(JSON.stringify(variables)) - }).cipherTextBlob; + const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables)); + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: secretRotation.projectId + }); const numberOfSecretsRotated = rotationOutputs.length; if (shouldUseSecretV2Bridge) { @@ -312,7 +323,11 @@ export const secretRotationQueueFactory = ({ await secretRotationDAL.updateById( rotationId, { - encryptedRotationData, + encryptedData: encVarData.ciphertext, + encryptedDataIV: encVarData.iv, + encryptedDataTag: encVarData.tag, + keyEncoding: encVarData.encoding, + algorithm: encVarData.algorithm, lastRotatedAt: new Date(), statusMessage: "Rotated successfull", status: "success" @@ -356,7 +371,11 @@ export const secretRotationQueueFactory = ({ await secretRotationDAL.updateById( rotationId, { - encryptedRotationData, + encryptedData: encVarData.ciphertext, + encryptedDataIV: encVarData.iv, + encryptedDataTag: encVarData.tag, + keyEncoding: encVarData.encoding, + algorithm: encVarData.algorithm, 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 02da4b7eaf..c456e15815 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -2,11 +2,9 @@ import { ForbiddenError, subject } from "@casl/ability"; import Ajv from "ajv"; import { ActionProjectType, ProjectVersion, TableName } from "@app/db/schemas"; -import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto/encryption"; +import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } 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"; @@ -32,7 +30,6 @@ type TSecretRotationServiceFactoryDep = { permissionService: Pick; secretRotationQueue: TSecretRotationQueueFactory; projectBotService: Pick; - kmsService: Pick; }; export type TSecretRotationServiceFactory = ReturnType; @@ -47,8 +44,7 @@ export const secretRotationServiceFactory = ({ folderDAL, secretDAL, projectBotService, - secretV2BridgeDAL, - kmsService + secretV2BridgeDAL }: TSecretRotationServiceFactoryDep) => { const getProviderTemplates = async ({ actor, @@ -160,11 +156,7 @@ export const secretRotationServiceFactory = ({ inputs: formattedInputs, creds: [] }; - const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - + const encData = infisicalSymmetricEncypt(JSON.stringify(unencryptedData)); const secretRotation = await secretRotationDAL.transaction(async (tx) => { const doc = await secretRotationDAL.create( { @@ -172,8 +164,11 @@ export const secretRotationServiceFactory = ({ secretPath, interval, envId: folder.envId, - encryptedRotationData: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(unencryptedData)) }) - .cipherTextBlob + encryptedDataTag: encData.tag, + encryptedDataIV: encData.iv, + encryptedData: encData.ciphertext, + algorithm: encData.algorithm, + keyEncoding: encData.encoding }, 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 1c34f6b3d4..2e4ed0f931 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -1,5 +1,3 @@ -/* 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 d8240f27e1..8a9eeab8ce 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,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ +/* eslint-disable no-await-in-loop */ import { Knex } from "knex"; import { z } from "zod"; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index a5f3c24c0e..dbfdfd063c 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -2,12 +2,6 @@ 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 deleted file mode 100644 index 1fe78cf7ed..0000000000 --- a/backend/src/keystore/memory.ts +++ /dev/null @@ -1,38 +0,0 @@ -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 801e937a2d..7f0f317283 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -258,8 +258,7 @@ const envSchema = z SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") })); -export type TEnvConfig = Readonly>; -let envCfg: TEnvConfig; +let envCfg: Readonly>; 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 170a0285fe..9676496f72 100644 --- a/backend/src/lib/logger/logger.ts +++ b/backend/src/lib/logger/logger.ts @@ -98,7 +98,7 @@ const extractReqId = () => { } }; -export const initLogger = () => { +export const initLogger = async () => { 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 461601fc0e..850298f89a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -2,13 +2,14 @@ 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 } from "./lib/config/env"; +import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env"; +import { isMigrationMode } from "./lib/fn"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; @@ -18,53 +19,58 @@ import { smtpServiceFactory } from "./services/smtp/smtp-service"; dotenv.config(); const run = async () => { - const logger = initLogger(); - const envConfig = initEnvConfig(logger); + const logger = await initLogger(); + const appCfg = initEnvConfig(logger); const db = initDbConnection({ - dbConnectionUri: envConfig.DB_CONNECTION_URI, - dbRootCert: envConfig.DB_ROOT_CERT, - readReplicas: envConfig.DB_READ_REPLICAS?.map((el) => ({ + dbConnectionUri: appCfg.DB_CONNECTION_URI, + dbRootCert: appCfg.DB_ROOT_CERT, + readReplicas: appCfg.DB_READ_REPLICAS?.map((el) => ({ dbRootCert: el.DB_ROOT_CERT, dbConnectionUri: el.DB_CONNECTION_URI })) }); - const auditLogDb = envConfig.AUDIT_LOGS_DB_CONNECTION_URI + const auditLogDb = appCfg.AUDIT_LOGS_DB_CONNECTION_URI ? initAuditLogDbConnection({ - dbConnectionUri: envConfig.AUDIT_LOGS_DB_CONNECTION_URI, - dbRootCert: envConfig.AUDIT_LOGS_DB_ROOT_CERT + dbConnectionUri: appCfg.AUDIT_LOGS_DB_CONNECTION_URI, + dbRootCert: appCfg.AUDIT_LOGS_DB_ROOT_CERT }) : undefined; - await runMigrations({ applicationDb: db, auditLogDb, logger }); + // 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); + } const smtp = smtpServiceFactory(formatSmtpConfig()); - const queue = queueServiceFactory(envConfig.REDIS_URL, { - dbConnectionUrl: envConfig.DB_CONNECTION_URI, - dbRootCert: envConfig.DB_ROOT_CERT + const queue = queueServiceFactory(appCfg.REDIS_URL, { + dbConnectionUrl: appCfg.DB_CONNECTION_URI, + dbRootCert: appCfg.DB_ROOT_CERT }); await queue.initialize(); - const keyStore = keyStoreFactory(envConfig.REDIS_URL); - const redis = new Redis(envConfig.REDIS_URL); + const keyStore = keyStoreFactory(appCfg.REDIS_URL); + const redis = new Redis(appCfg.REDIS_URL); - const hsmModule = initializeHsmModule(envConfig); + const hsmModule = initializeHsmModule(); hsmModule.initialize(); - const server = await main({ - db, - auditLogDb, - hsmModule: hsmModule.getModule(), - smtp, - logger, - queue, - keyStore, - redis, - envConfig - }); + const server = await main({ db, auditLogDb, hsmModule: hsmModule.getModule(), smtp, logger, queue, keyStore, redis }); const bootstrap = await bootstrapCheck({ db }); // eslint-disable-next-line @@ -84,8 +90,8 @@ const run = async () => { }); await server.listen({ - port: envConfig.PORT, - host: envConfig.HOST, + port: appCfg.PORT, + host: appCfg.HOST, listenTextResolver: (address) => { void bootstrap(); return address; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 26f556508e..ce1be4a04e 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, TEnvConfig } from "@app/lib/config/env"; +import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; import { CustomLogger } from "@app/lib/logger/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TQueueServiceFactory } from "@app/queue"; @@ -43,11 +43,10 @@ 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, envConfig }: TMain) => { +export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore, redis }: TMain) => { const appCfg = getConfig(); const server = fastify({ @@ -128,7 +127,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key }) }); - await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule, envConfig }); + await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule }); 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 10da81bf57..8cebbdebdb 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, TEnvConfig } from "@app/lib/config/env"; +import { getConfig } 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,8 +244,7 @@ export const registerRoutes = async ( hsmModule, smtp: smtpService, queue: queueService, - keyStore, - envConfig + keyStore }: { auditLogDb?: Knex; db: Knex; @@ -253,7 +252,6 @@ export const registerRoutes = async ( smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory; - envConfig: TEnvConfig; } ) => { const appCfg = getConfig(); @@ -393,8 +391,7 @@ export const registerRoutes = async ( const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); const hsmService = hsmServiceFactory({ - hsmModule, - envConfig + hsmModule }); const kmsService = kmsServiceFactory({ @@ -404,8 +401,7 @@ export const registerRoutes = async ( internalKmsDAL, orgDAL, projectDAL, - hsmService, - envConfig + hsmService }); const externalKmsService = externalKmsServiceFactory({ @@ -451,6 +447,7 @@ export const registerRoutes = async ( const samlService = samlConfigServiceFactory({ identityMetadataDAL, permissionService, + orgBotDAL, orgDAL, orgMembershipDAL, userDAL, @@ -458,8 +455,7 @@ export const registerRoutes = async ( samlConfigDAL, licenseService, tokenService, - smtpService, - kmsService + smtpService }); const groupService = groupServiceFactory({ userDAL, @@ -510,6 +506,7 @@ export const registerRoutes = async ( ldapGroupMapDAL, orgDAL, orgMembershipDAL, + orgBotDAL, groupDAL, groupProjectDAL, projectKeyDAL, @@ -521,8 +518,7 @@ export const registerRoutes = async ( permissionService, licenseService, tokenService, - smtpService, - kmsService + smtpService }); const telemetryService = telemetryServiceFactory({ @@ -973,8 +969,7 @@ export const registerRoutes = async ( permissionService, webhookDAL, projectEnvDAL, - projectDAL, - kmsService + projectDAL }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); @@ -1154,8 +1149,7 @@ export const registerRoutes = async ( secretDAL, folderDAL, projectBotService, - secretV2BridgeDAL, - kmsService + secretV2BridgeDAL }); const integrationService = integrationServiceFactory({ @@ -1244,9 +1238,9 @@ export const registerRoutes = async ( identityKubernetesAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, + orgBotDAL, permissionService, - licenseService, - kmsService + licenseService }); const identityGcpAuthService = identityGcpAuthServiceFactory({ identityGcpAuthDAL, @@ -1278,7 +1272,7 @@ export const registerRoutes = async ( identityAccessTokenDAL, permissionService, licenseService, - kmsService + orgBotDAL }); const identityJwtAuthService = identityJwtAuthServiceFactory({ @@ -1295,9 +1289,7 @@ export const registerRoutes = async ( queueService, dynamicSecretLeaseDAL, dynamicSecretProviders, - dynamicSecretDAL, - folderDAL, - kmsService + dynamicSecretDAL }); const dynamicSecretService = dynamicSecretServiceFactory({ projectDAL, @@ -1307,8 +1299,7 @@ export const registerRoutes = async ( dynamicSecretProviders, folderDAL, permissionService, - licenseService, - kmsService + licenseService }); const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ projectDAL, @@ -1318,8 +1309,7 @@ export const registerRoutes = async ( dynamicSecretLeaseDAL, dynamicSecretProviders, folderDAL, - licenseService, - kmsService + licenseService }); const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, @@ -1347,7 +1337,7 @@ export const registerRoutes = async ( licenseService, tokenService, smtpService, - kmsService, + orgBotDAL, permissionService, oidcConfigDAL, projectBotDAL, diff --git a/backend/src/server/routes/sanitizedSchema/directory-config.ts b/backend/src/server/routes/sanitizedSchema/directory-config.ts deleted file mode 100644 index 61be4d9cf6..0000000000 --- a/backend/src/server/routes/sanitizedSchema/directory-config.ts +++ /dev/null @@ -1,42 +0,0 @@ -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/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 4d645ac4be..67f01c9ba0 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 "./sanitizedSchema/permission"; +import { UnpackedPermissionSchema } from "./santizedSchemas/permission"; // sometimes the return data must be santizied to avoid leaking important values // always prefer pick over omit in zod @@ -201,11 +201,10 @@ 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/sanitizedSchema/identitiy-additional-privilege.ts b/backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts similarity index 100% rename from backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts rename to backend/src/server/routes/santizedSchemas/identitiy-additional-privilege.ts diff --git a/backend/src/server/routes/sanitizedSchema/permission.ts b/backend/src/server/routes/santizedSchemas/permission.ts similarity index 100% rename from backend/src/server/routes/sanitizedSchema/permission.ts rename to backend/src/server/routes/santizedSchemas/permission.ts diff --git a/backend/src/server/routes/sanitizedSchema/user-additional-privilege.ts b/backend/src/server/routes/santizedSchemas/user-additional-privilege.ts similarity index 100% rename from backend/src/server/routes/sanitizedSchema/user-additional-privilege.ts rename to backend/src/server/routes/santizedSchemas/user-additional-privilege.ts 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 263fa478e8..3b30251794 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -8,19 +8,13 @@ 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.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 +const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.omit({ + encryptedCaCert: true, + caCertIV: true, + caCertTag: true, + encryptedTokenReviewerJwt: true, + tokenReviewerJwtIV: true, + tokenReviewerJwtTag: 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 7ce0b05b75..431ed3f4f4 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -12,20 +12,10 @@ import { validateOidcBoundClaimsField } from "@app/services/identity-oidc-auth/identity-oidc-auth-validators"; -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 +const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.omit({ + encryptedCaCert: true, + caCertIV: true, + caCertTag: 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 a5677894d2..4508a255da 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,21 +3,28 @@ import axios, { AxiosError } from "axios"; import https from "https"; import jwt from "jsonwebtoken"; -import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { IdentityAuthMethod, SecretKeyEncoding, 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 { @@ -36,9 +43,9 @@ type TIdentityKubernetesAuthServiceFactoryDep = { >; identityAccessTokenDAL: Pick; identityOrgMembershipDAL: Pick; + orgBotDAL: Pick; permissionService: Pick; licenseService: Pick; - kmsService: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; @@ -47,9 +54,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityKubernetesAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, + orgBotDAL, permissionService, - licenseService, - kmsService + licenseService }: TIdentityKubernetesAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); @@ -68,21 +75,42 @@ export const identityKubernetesAuthServiceFactory = ({ }); } - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + 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 { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } = + identityKubernetesAuth; + let caCert = ""; - if (identityKubernetesAuth.encryptedKubernetesCaCertificate) { - caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); + if (encryptedCaCert && caCertIV && caCertTag) { + caCert = decryptSymmetric({ + ciphertext: encryptedCaCert, + iv: caCertIV, + tag: caCertTag, + key + }); } let tokenReviewerJwt = ""; - if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { - tokenReviewerJwt = decryptor({ - cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt - }).toString(); + if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) { + tokenReviewerJwt = decryptSymmetric({ + ciphertext: encryptedTokenReviewerJwt, + iv: tokenReviewerJwtIV, + tag: tokenReviewerJwtTag, + key + }); } const { data } = await axios @@ -269,25 +297,79 @@ export const identityKubernetesAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); - const { encryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + 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 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), - encryptedKubernetesTokenReviewerJwt: encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob, - encryptedKubernetesCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) }, tx ); @@ -373,34 +455,61 @@ export const identityKubernetesAuthServiceFactory = ({ : undefined }; - const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + 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 }); if (caCert !== undefined) { - updateQuery.encryptedKubernetesCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; + const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); + updateQuery.encryptedCaCert = encryptedCACert; + updateQuery.caCertIV = caCertIV; + updateQuery.caCertTag = caCertTag; } if (tokenReviewerJwt !== undefined) { - updateQuery.encryptedKubernetesTokenReviewerJwt = encryptor({ - plainText: Buffer.from(tokenReviewerJwt) - }).cipherTextBlob; + const { + ciphertext: encryptedTokenReviewerJwt, + iv: tokenReviewerJwtIV, + tag: tokenReviewerJwtTag + } = encryptSymmetric(tokenReviewerJwt, key); + updateQuery.encryptedTokenReviewerJwt = encryptedTokenReviewerJwt; + updateQuery.tokenReviewerJwtIV = tokenReviewerJwtIV; + updateQuery.tokenReviewerJwtTag = tokenReviewerJwtTag; } const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery); - const updatedCACert = updatedKubernetesAuth.encryptedKubernetesCaCertificate - ? decryptor({ - cipherTextBlob: updatedKubernetesAuth.encryptedKubernetesCaCertificate - }).toString() - : ""; - - const updatedTokenReviewerJwt = updatedKubernetesAuth.encryptedKubernetesTokenReviewerJwt - ? decryptor({ - cipherTextBlob: updatedKubernetesAuth.encryptedKubernetesTokenReviewerJwt - }).toString() - : ""; + const updatedCACert = + updatedKubernetesAuth.encryptedCaCert && updatedKubernetesAuth.caCertIV && updatedKubernetesAuth.caCertTag + ? decryptSymmetric({ + ciphertext: updatedKubernetesAuth.encryptedCaCert, + iv: updatedKubernetesAuth.caCertIV, + tag: updatedKubernetesAuth.caCertTag, + key + }) + : ""; + + const updatedTokenReviewerJwt = + updatedKubernetesAuth.encryptedTokenReviewerJwt && + updatedKubernetesAuth.tokenReviewerJwtIV && + updatedKubernetesAuth.tokenReviewerJwtTag + ? decryptSymmetric({ + ciphertext: updatedKubernetesAuth.encryptedTokenReviewerJwt, + iv: updatedKubernetesAuth.tokenReviewerJwtIV, + tag: updatedKubernetesAuth.tokenReviewerJwtTag, + key + }) + : ""; return { ...updatedKubernetesAuth, @@ -436,21 +545,41 @@ export const identityKubernetesAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + 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 { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } = + identityKubernetesAuth; + let caCert = ""; - if (identityKubernetesAuth.encryptedKubernetesCaCertificate) { - caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); + if (encryptedCaCert && caCertIV && caCertTag) { + caCert = decryptSymmetric({ + ciphertext: encryptedCaCert, + iv: caCertIV, + tag: caCertTag, + key + }); } let tokenReviewerJwt = ""; - if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { - tokenReviewerJwt = decryptor({ - cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt - }).toString(); + if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) { + tokenReviewerJwt = decryptSymmetric({ + ciphertext: encryptedTokenReviewerJwt, + iv: tokenReviewerJwtIV, + tag: tokenReviewerJwtTag, + key + }); } 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 ff7256a9cb..a1dbed46b3 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,12 +4,20 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; +import { IdentityAuthMethod, SecretKeyEncoding, 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"; @@ -17,8 +25,7 @@ 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 { TOrgBotDALFactory } from "../org/org-bot-dal"; import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; import { @@ -35,7 +42,7 @@ type TIdentityOidcAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; - kmsService: Pick; + orgBotDAL: Pick; }; export type TIdentityOidcAuthServiceFactory = ReturnType; @@ -46,7 +53,7 @@ export const identityOidcAuthServiceFactory = ({ permissionService, licenseService, identityAccessTokenDAL, - kmsService + orgBotDAL }: TIdentityOidcAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -63,14 +70,31 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + 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 { encryptedCaCert, caCertIV, caCertTag } = identityOidcAuth; + let caCert = ""; - if (identityOidcAuth.encryptedCaCertificate) { - caCert = decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString(); + if (encryptedCaCert && caCertIV && caCertTag) { + caCert = decryptSymmetric({ + ciphertext: encryptedCaCert, + iv: caCertIV, + tag: caCertTag, + key + }); } const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert }); @@ -240,17 +264,64 @@ export const identityOidcAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); - const { encryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + 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 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, - encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob, + encryptedCaCert, + caCertIV, + caCertTag, boundIssuer, boundAudiences, boundClaims, @@ -344,19 +415,38 @@ export const identityOidcAuthServiceFactory = ({ : undefined }; - const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + 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 }); if (caCert !== undefined) { - updateQuery.encryptedCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob; + const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); + updateQuery.encryptedCaCert = encryptedCACert; + updateQuery.caCertIV = caCertIV; + updateQuery.caCertTag = caCertTag; } const updatedOidcAuth = await identityOidcAuthDAL.updateById(identityOidcAuth.id, updateQuery); - const updatedCACert = updatedOidcAuth.encryptedCaCertificate - ? decryptor({ cipherTextBlob: updatedOidcAuth.encryptedCaCertificate }).toString() - : ""; + const updatedCACert = + updatedOidcAuth.encryptedCaCert && updatedOidcAuth.caCertIV && updatedOidcAuth.caCertTag + ? decryptSymmetric({ + ciphertext: updatedOidcAuth.encryptedCaCert, + iv: updatedOidcAuth.caCertIV, + tag: updatedOidcAuth.caCertTag, + key + }) + : ""; return { ...updatedOidcAuth, @@ -386,14 +476,27 @@ export const identityOidcAuthServiceFactory = ({ const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + 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 caCert = identityOidcAuth.encryptedCaCertificate - ? decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString() - : ""; + const caCert = decryptSymmetric({ + ciphertext: identityOidcAuth.encryptedCaCert, + iv: identityOidcAuth.caCertIV, + tag: identityOidcAuth.caCertTag, + key + }); 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 8745d286e8..f448e2df86 100644 --- a/backend/src/services/kms/kms-root-config-dal.ts +++ b/backend/src/services/kms/kms-root-config-dal.ts @@ -1,24 +1,10 @@ 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); - - 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 }; + return kmsOrm; }; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index babba4cb22..c417838603 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, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; -import { TEnvConfig } from "@app/lib/config/env"; +import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; +import { getConfig } 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,22 +44,23 @@ 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, @@ -472,8 +473,7 @@ export const kmsServiceFactory = ({ } const kmsDecryptor = await decryptWithKmsKey({ - kmsId: kmsKeyId, - tx: trx + kmsId: kmsKeyId }); return kmsDecryptor({ @@ -635,8 +635,10 @@ export const kmsServiceFactory = ({ }; const $getBasicEncryptionKey = () => { - const encryptionKey = envConfig.ENCRYPTION_KEY || envConfig.ROOT_ENCRYPTION_KEY; - const isBase64 = !envConfig.ENCRYPTION_KEY; + const appCfg = getConfig(); + + const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY; + const isBase64 = !appCfg.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?" @@ -872,33 +874,54 @@ export const kmsServiceFactory = ({ return { id, name, orgId, isExternal }; }; + // akhilmhdh: a copy of this is made in migrations/utils/kms const startService = async () => { - 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; + 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 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; + // 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 decryptedRootKey = await $decryptRootKey(kmsRootConfig); + 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 + }); - logger.info("KMS: Loading ROOT Key into Memory."); - ROOT_ENCRYPTION_KEY = decryptedRootKey; + // 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; }; 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 1d695a5ff1..09bc6460df 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/sanitizedSchema/permission"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/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 83a59b6af4..2c6b8e2dae 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -4,17 +4,8 @@ import { ProjectType, TProjectKeys } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; - -enum KmsType { - External = "external", - Internal = "internal" -} - -enum CaStatus { - ACTIVE = "active", - DISABLED = "disabled", - PENDING_CERTIFICATE = "pending-certificate" -} +import { CaStatus } from "../certificate-authority/certificate-authority-types"; +import { KmsType } from "../kms/kms-types"; export enum ProjectFilterType { ID = "id", diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 00b0e7da84..dc973c0b1f 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1488,18 +1488,7 @@ export const secretQueueFactory = ({ }); queueService.start(QueueName.SecretWebhook, async (job) => { - 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() - }); + await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL }); }); return { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 9a60754235..b0fdd9c5cb 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 { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; +import { 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,21 +87,17 @@ export const superAdminServiceFactory = ({ // reset on initialized await keyStore.deleteItem(ADMIN_CONFIG_KEY); - 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 - }); - return newCfg; + const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (serverCfg) return; + + 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 serverCfg; + return newCfg; }; const updateServerCfg = async ( diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index e46f9db2a2..58f51f8808 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -3,8 +3,9 @@ import crypto from "node:crypto"; import { AxiosError } from "axios"; import picomatch from "picomatch"; -import { TWebhooks } from "@app/db/schemas"; +import { SecretKeyEncoding, 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"; @@ -15,14 +16,28 @@ import { WebhookType } from "./webhook-types"; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; -export const decryptWebhookDetails = (webhook: TWebhooks, decryptor: (value: Buffer) => string) => { - const { encryptedPassKey, encryptedUrl } = webhook; - - const decryptedUrl = decryptor(encryptedUrl); +export const decryptWebhookDetails = (webhook: TWebhooks) => { + const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook; let decryptedSecretKey = ""; - if (encryptedPassKey) { - decryptedSecretKey = decryptor(encryptedPassKey); + 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 + }); } return { @@ -31,14 +46,10 @@ export const decryptWebhookDetails = (webhook: TWebhooks, decryptor: (value: Buf }; }; -export const triggerWebhookRequest = async ( - webhook: TWebhooks, - decryptor: (value: Buffer) => string, - data: Record -) => { +export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record) => { const headers: Record = {}; const payload = { ...data, timestamp: Date.now() }; - const { secretKey, url } = decryptWebhookDetails(webhook, decryptor); + const { secretKey, url } = decryptWebhookDetails(webhook); if (secretKey) { const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex"); @@ -113,7 +124,6 @@ export type TFnTriggerWebhookDTO = { webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; - secretManagerDecryptor: (value: Buffer) => string; }; // this is reusable function @@ -124,8 +134,7 @@ export const fnTriggerWebhook = async ({ projectId, webhookDAL, projectEnvDAL, - projectDAL, - secretManagerDecryptor + projectDAL }: TFnTriggerWebhookDTO) => { const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const toBeTriggeredHooks = webhooks.filter( @@ -139,7 +148,6 @@ 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 bb078e0f1f..26136aaf61 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -3,10 +3,9 @@ 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"; @@ -24,7 +23,6 @@ type TWebhookServiceFactoryDep = { projectEnvDAL: TProjectEnvDALFactory; projectDAL: Pick; permissionService: Pick; - kmsService: Pick; }; export type TWebhookServiceFactory = ReturnType; @@ -33,8 +31,7 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionService, - projectDAL, - kmsService + projectDAL }: TWebhookServiceFactoryDep) => { const createWebhook = async ({ actor, @@ -63,20 +60,30 @@ 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, - encryptedUrl: secretManagerEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob + type }; if (webhookSecretKey) { - insertDoc.encryptedPassKey = secretManagerEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob; + 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; } const webhook = await webhookDAL.create(insertDoc); @@ -133,17 +140,12 @@ 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, @@ -183,13 +185,8 @@ 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, (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()); + const { url } = decryptWebhookDetails(w); return { ...w, url diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 90165acbe2..fcf5089223 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,8 +1,7 @@ { "ts-node": { // Do not forget to `npm i -D tsconfig-paths` - "require": ["tsconfig-paths/register"], - "files": true + "require": ["tsconfig-paths/register"] }, "compilerOptions": { "target": "esnext", @@ -20,7 +19,6 @@ "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 680a655d83..40d17c1b01 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -56,6 +56,20 @@ 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: @@ -66,6 +80,8 @@ services: condition: service_started redis: condition: service_started + db-migration: + condition: service_completed_successfully env_file: - .env ports: @@ -176,7 +192,7 @@ services: depends_on: - openldap profiles: [ldap] - + keycloak: image: quay.io/keycloak/keycloak:26.1.0 restart: always @@ -186,7 +202,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 d3526d8416..77a1e04ab7 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,6 +1,18 @@ 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 @@ -9,6 +21,8 @@ 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 @@ -55,5 +69,4 @@ volumes: driver: local networks: - infisical: - + infisical: \ No newline at end of file