mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 21:26:40 +00:00
feat: Hardware security modules
This commit is contained in:
@@ -1,5 +1,44 @@
|
|||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# ? Setup a test SoftHSM module. In production a real HSM is used.
|
||||||
|
|
||||||
|
ARG SOFTHSM2_VERSION=2.5.0
|
||||||
|
|
||||||
|
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
|
||||||
|
SOFTHSM2_SOURCES=/tmp/softhsm2
|
||||||
|
|
||||||
|
# install build dependencies including python3
|
||||||
|
RUN apk --update add \
|
||||||
|
alpine-sdk \
|
||||||
|
autoconf \
|
||||||
|
automake \
|
||||||
|
git \
|
||||||
|
libtool \
|
||||||
|
openssl-dev \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++
|
||||||
|
|
||||||
|
# build and install SoftHSM2
|
||||||
|
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
|
||||||
|
WORKDIR ${SOFTHSM2_SOURCES}
|
||||||
|
|
||||||
|
RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \
|
||||||
|
&& sh autogen.sh \
|
||||||
|
&& ./configure --prefix=/usr/local --disable-gost \
|
||||||
|
&& make \
|
||||||
|
&& make install
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
|
RUN rm -fr ${SOFTHSM2_SOURCES}
|
||||||
|
|
||||||
|
# install pkcs11-tool
|
||||||
|
RUN apk --update add opensc
|
||||||
|
|
||||||
|
RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
||||||
|
|
||||||
|
# ? App setup
|
||||||
|
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||||
&& apk add infisical=0.8.1 && apk add --no-cache git
|
&& apk add infisical=0.8.1 && apk add --no-cache git
|
||||||
|
|||||||
Generated
+36
-3
@@ -55,6 +55,7 @@
|
|||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
"google-auth-library": "^9.9.0",
|
"google-auth-library": "^9.9.0",
|
||||||
"googleapis": "^137.1.0",
|
"googleapis": "^137.1.0",
|
||||||
|
"graphene-pk11": "^2.3.6",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"hdb": "^0.19.10",
|
"hdb": "^0.19.10",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
@@ -13556,6 +13557,23 @@
|
|||||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/graphene-pk11": {
|
||||||
|
"version": "2.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphene-pk11/-/graphene-pk11-2.3.6.tgz",
|
||||||
|
"integrity": "sha512-ol9Pf7XDv5UTjh1DPqtmQVZQqUheiXBzQVXQWRCLWq78+brKQB0Kum/s0NGEcsd/5NQQG8MFA2U/KNujEoC1fQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pkcs11js": "^2.1.6",
|
||||||
|
"tslib": "^2.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/PeculiarVentures"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graphql": {
|
"node_modules/graphql": {
|
||||||
"version": "16.9.0",
|
"version": "16.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
|
||||||
@@ -17066,6 +17084,20 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pkcs11js": {
|
||||||
|
"version": "2.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkcs11js/-/pkcs11js-2.1.6.tgz",
|
||||||
|
"integrity": "sha512-+t5jxzB749q8GaEd1yNx3l98xYuaVK6WW/Vjg1Mk1Iy5bMu/A5W4O/9wZGrpOknWF6lFQSb12FXX+eSNxdriwA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/PeculiarVentures"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pkg-conf": {
|
"node_modules/pkg-conf": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz",
|
||||||
@@ -19761,9 +19793,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.6.3",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
|
||||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==",
|
||||||
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tsup": {
|
"node_modules/tsup": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
"google-auth-library": "^9.9.0",
|
"google-auth-library": "^9.9.0",
|
||||||
"googleapis": "^137.1.0",
|
"googleapis": "^137.1.0",
|
||||||
|
"graphene-pk11": "^2.3.6",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"hdb": "^0.19.10",
|
"hdb": "^0.19.10",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
|
|||||||
Vendored
+2
@@ -44,6 +44,7 @@ import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
|
|||||||
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||||
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
|
import { THsmServiceFactory } from "@app/services/hsm/hsm-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||||
@@ -184,6 +185,7 @@ declare module "fastify" {
|
|||||||
rateLimit: TRateLimitServiceFactory;
|
rateLimit: TRateLimitServiceFactory;
|
||||||
userEngagement: TUserEngagementServiceFactory;
|
userEngagement: TUserEngagementServiceFactory;
|
||||||
externalKms: TExternalKmsServiceFactory;
|
externalKms: TExternalKmsServiceFactory;
|
||||||
|
hsm: THsmServiceFactory;
|
||||||
orgAdmin: TOrgAdminServiceFactory;
|
orgAdmin: TOrgAdminServiceFactory;
|
||||||
slack: TSlackServiceFactory;
|
slack: TSlackServiceFactory;
|
||||||
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasIsEncryptedByHsmCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "isEncryptedByHsm");
|
||||||
|
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
|
||||||
|
if (!hasIsEncryptedByHsmCol) t.boolean("isEncryptedByHsm").defaultTo(false).notNullable();
|
||||||
|
if (!hasTimestampsCol) t.timestamps(true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasIsEncryptedByHsmCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "isEncryptedByHsm");
|
||||||
|
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
|
||||||
|
if (hasIsEncryptedByHsmCol) t.dropColumn("isEncryptedByHsm");
|
||||||
|
if (hasTimestampsCol) t.dropTimestamps(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -11,7 +11,10 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
|
|
||||||
export const KmsRootConfigSchema = z.object({
|
export const KmsRootConfigSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedRootKey: zodBuffer
|
encryptedRootKey: zodBuffer,
|
||||||
|
isEncryptedByHsm: z.boolean().default(false),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;
|
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;
|
||||||
|
|||||||
@@ -163,7 +163,38 @@ const envSchema = z
|
|||||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
|
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
|
||||||
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true")
|
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
|
||||||
|
|
||||||
|
// HSM
|
||||||
|
HSM_LIB_PATH: zpStr(
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => {
|
||||||
|
if (process.env.NODE_ENV === "development") return "/usr/local/lib/softhsm/libsofthsm2.so";
|
||||||
|
return val;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
HSM_PIN: zpStr(
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => {
|
||||||
|
if (process.env.NODE_ENV === "development") return "1234";
|
||||||
|
return val;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
HSM_KEY_LABEL: zpStr(
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => {
|
||||||
|
if (process.env.NODE_ENV === "development") return "auth-app";
|
||||||
|
return val;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
HSM_SLOT: z.coerce.number().optional().default(0),
|
||||||
|
HSM_MECHANISM: zpStr(z.string().optional().default("AES_GCM"))
|
||||||
})
|
})
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
...data,
|
...data,
|
||||||
@@ -175,10 +206,18 @@ const envSchema = z
|
|||||||
isRedisConfigured: Boolean(data.REDIS_URL),
|
isRedisConfigured: Boolean(data.REDIS_URL),
|
||||||
isDevelopmentMode: data.NODE_ENV === "development",
|
isDevelopmentMode: data.NODE_ENV === "development",
|
||||||
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
|
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
|
||||||
|
|
||||||
isSecretScanningConfigured:
|
isSecretScanningConfigured:
|
||||||
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
||||||
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
||||||
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
|
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
|
||||||
|
isHsmConfigured:
|
||||||
|
Boolean(data.HSM_LIB_PATH) &&
|
||||||
|
Boolean(data.HSM_PIN) &&
|
||||||
|
Boolean(data.HSM_KEY_LABEL) &&
|
||||||
|
Boolean(data.HSM_MECHANISM) &&
|
||||||
|
data.HSM_SLOT !== undefined,
|
||||||
|
|
||||||
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
|
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
|
||||||
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
|
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
|
||||||
}));
|
}));
|
||||||
|
|||||||
+7
-1
@@ -9,6 +9,7 @@ import { initLogger } from "./lib/logger";
|
|||||||
import { queueServiceFactory } from "./queue";
|
import { queueServiceFactory } from "./queue";
|
||||||
import { main } from "./server/app";
|
import { main } from "./server/app";
|
||||||
import { bootstrapCheck } from "./server/boot-strap-check";
|
import { bootstrapCheck } from "./server/boot-strap-check";
|
||||||
|
import { initializePkcs11Module } from "./services/hsm/hsm-fns";
|
||||||
import { smtpServiceFactory } from "./services/smtp/smtp-service";
|
import { smtpServiceFactory } from "./services/smtp/smtp-service";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
@@ -53,13 +54,17 @@ const run = async () => {
|
|||||||
const queue = queueServiceFactory(appCfg.REDIS_URL);
|
const queue = queueServiceFactory(appCfg.REDIS_URL);
|
||||||
const keyStore = keyStoreFactory(appCfg.REDIS_URL);
|
const keyStore = keyStoreFactory(appCfg.REDIS_URL);
|
||||||
|
|
||||||
const server = await main({ db, auditLogDb, smtp, logger, queue, keyStore });
|
const pkcs11Module = initializePkcs11Module();
|
||||||
|
pkcs11Module.initialize();
|
||||||
|
|
||||||
|
const server = await main({ db, auditLogDb, hsmModule: pkcs11Module.getModule(), smtp, logger, queue, keyStore });
|
||||||
const bootstrap = await bootstrapCheck({ db });
|
const bootstrap = await bootstrapCheck({ db });
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
process.on("SIGINT", async () => {
|
process.on("SIGINT", async () => {
|
||||||
await server.close();
|
await server.close();
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
|
pkcs11Module.finalize();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,6 +72,7 @@ const run = async () => {
|
|||||||
process.on("SIGTERM", async () => {
|
process.on("SIGTERM", async () => {
|
||||||
await server.close();
|
await server.close();
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
|
pkcs11Module.finalize();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { Logger } from "pino";
|
|||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
|
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
|
||||||
import { TQueueServiceFactory } from "@app/queue";
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { HsmModule } from "@app/services/hsm/hsm-fns";
|
||||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
||||||
@@ -36,10 +37,11 @@ type TMain = {
|
|||||||
logger?: Logger;
|
logger?: Logger;
|
||||||
queue: TQueueServiceFactory;
|
queue: TQueueServiceFactory;
|
||||||
keyStore: TKeyStoreFactory;
|
keyStore: TKeyStoreFactory;
|
||||||
|
hsmModule: HsmModule;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run the server!
|
// Run the server!
|
||||||
export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TMain) => {
|
export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore }: TMain) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const server = fastify({
|
const server = fastify({
|
||||||
logger: appCfg.NODE_ENV === "test" ? false : logger,
|
logger: appCfg.NODE_ENV === "test" ? false : logger,
|
||||||
@@ -95,7 +97,7 @@ export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TM
|
|||||||
|
|
||||||
await server.register(maintenanceMode);
|
await server.register(maintenanceMode);
|
||||||
|
|
||||||
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore });
|
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
|
||||||
|
|
||||||
if (appCfg.isProductionMode) {
|
if (appCfg.isProductionMode) {
|
||||||
await server.register(registerExternalNextjs, {
|
await server.register(registerExternalNextjs, {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
// import { Redis } from "ioredis";
|
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -108,6 +107,8 @@ import { externalMigrationServiceFactory } from "@app/services/external-migratio
|
|||||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
|
import { HsmModule } from "@app/services/hsm/hsm-fns";
|
||||||
|
import { hsmServiceFactory } from "@app/services/hsm/hsm-service";
|
||||||
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
|
import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
|
||||||
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||||
@@ -223,10 +224,18 @@ export const registerRoutes = async (
|
|||||||
{
|
{
|
||||||
auditLogDb,
|
auditLogDb,
|
||||||
db,
|
db,
|
||||||
|
hsmModule,
|
||||||
smtp: smtpService,
|
smtp: smtpService,
|
||||||
queue: queueService,
|
queue: queueService,
|
||||||
keyStore
|
keyStore
|
||||||
}: { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
|
}: {
|
||||||
|
auditLogDb?: Knex;
|
||||||
|
db: Knex;
|
||||||
|
hsmModule: HsmModule;
|
||||||
|
smtp: TSmtpService;
|
||||||
|
queue: TQueueServiceFactory;
|
||||||
|
keyStore: TKeyStoreFactory;
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
|
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
|
||||||
@@ -352,14 +361,21 @@ export const registerRoutes = async (
|
|||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
||||||
|
|
||||||
|
const hsmService = hsmServiceFactory({
|
||||||
|
pkcs11Module: hsmModule
|
||||||
|
});
|
||||||
|
|
||||||
const kmsService = kmsServiceFactory({
|
const kmsService = kmsServiceFactory({
|
||||||
kmsRootConfigDAL,
|
kmsRootConfigDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
kmsDAL,
|
kmsDAL,
|
||||||
internalKmsDAL,
|
internalKmsDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
hsmService
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalKmsService = externalKmsServiceFactory({
|
const externalKmsService = externalKmsServiceFactory({
|
||||||
kmsDAL,
|
kmsDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
@@ -1265,6 +1281,7 @@ export const registerRoutes = async (
|
|||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
await licenseService.init();
|
await licenseService.init();
|
||||||
|
|
||||||
|
hsmService.startService();
|
||||||
await telemetryQueue.startTelemetryCheck();
|
await telemetryQueue.startTelemetryCheck();
|
||||||
await dailyResourceCleanUp.startCleanUp();
|
await dailyResourceCleanUp.startCleanUp();
|
||||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||||
@@ -1342,6 +1359,7 @@ export const registerRoutes = async (
|
|||||||
secretSharing: secretSharingService,
|
secretSharing: secretSharingService,
|
||||||
userEngagement: userEngagementService,
|
userEngagement: userEngagementService,
|
||||||
externalKms: externalKmsService,
|
externalKms: externalKmsService,
|
||||||
|
hsm: hsmService,
|
||||||
cmek: cmekService,
|
cmek: cmekService,
|
||||||
orgAdmin: orgAdminService,
|
orgAdmin: orgAdminService,
|
||||||
slack: slackService,
|
slack: slackService,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import * as grapheneLib from "graphene-pk11";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
export type HsmModule = {
|
||||||
|
module: grapheneLib.Module | null;
|
||||||
|
graphene: typeof grapheneLib;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializePkcs11Module = () => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
let module: grapheneLib.Module | null = null;
|
||||||
|
|
||||||
|
const initialize = () => {
|
||||||
|
if (!appCfg.isHsmConfigured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
module = grapheneLib.Module.load(appCfg.HSM_LIB_PATH!, "SoftHSM");
|
||||||
|
module.initialize();
|
||||||
|
logger.info("PKCS#11 module initialized");
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalize = () => {
|
||||||
|
if (module) {
|
||||||
|
module.finalize();
|
||||||
|
logger.info("PKCS#11 module finalized");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModule = (): HsmModule => ({ module, graphene: grapheneLib });
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialize,
|
||||||
|
finalize,
|
||||||
|
getModule
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import grapheneLib from "graphene-pk11";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { HsmModule } from "./hsm-fns";
|
||||||
|
|
||||||
|
type THsmServiceFactoryDep = {
|
||||||
|
pkcs11Module: HsmModule;
|
||||||
|
};
|
||||||
|
const SESSION_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
||||||
|
const USER_ALREADY_LOGGED_IN_ERROR = "CKR_USER_ALREADY_LOGGED_IN";
|
||||||
|
|
||||||
|
export type THsmServiceFactory = ReturnType<typeof hsmServiceFactory>;
|
||||||
|
|
||||||
|
class HsmSessionManager {
|
||||||
|
private session: grapheneLib.Session | null = null;
|
||||||
|
|
||||||
|
private lastUsed: number = 0;
|
||||||
|
|
||||||
|
private module: grapheneLib.Module;
|
||||||
|
|
||||||
|
private graphene: typeof grapheneLib;
|
||||||
|
|
||||||
|
private sessionCheckInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
private startSessionMonitoring() {
|
||||||
|
// Check session health every minute
|
||||||
|
this.sessionCheckInterval = setInterval(() => {
|
||||||
|
this.checkAndRefreshSession();
|
||||||
|
}, 60 * 1000); // 1 minute
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkAndRefreshSession() {
|
||||||
|
if (!this.session) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - this.lastUsed > SESSION_TIMEOUT) {
|
||||||
|
logger.info("Session expired, cleaning up...");
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanup() {
|
||||||
|
if (this.session) {
|
||||||
|
try {
|
||||||
|
this.session.logout();
|
||||||
|
this.session.close();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error during session cleanup:", error);
|
||||||
|
}
|
||||||
|
this.session = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sessionCheckInterval) {
|
||||||
|
clearInterval(this.sessionCheckInterval);
|
||||||
|
this.sessionCheckInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSession(): grapheneLib.Session {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
// If we have a valid session, update its last used time and return it
|
||||||
|
if (this.session) {
|
||||||
|
try {
|
||||||
|
// Try a simple operation to verify session is still valid
|
||||||
|
this.session.generateRandom(16);
|
||||||
|
this.lastUsed = Date.now();
|
||||||
|
return this.session;
|
||||||
|
} catch (error) {
|
||||||
|
logger.info("HSM Session validation failed, creating new session...");
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new session
|
||||||
|
const slot = this.module.getSlots(appCfg.HSM_SLOT);
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
if (!(slot.flags & this.graphene.SlotFlag.TOKEN_PRESENT)) {
|
||||||
|
throw new Error("Slot is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
const session = slot.open(this.graphene.SessionFlag.RW_SESSION | this.graphene.SessionFlag.SERIAL_SESSION);
|
||||||
|
|
||||||
|
try {
|
||||||
|
session.login(appCfg.HSM_PIN!);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- The error is of type `Pkcs11Error`, but this error is not exported by graphene. And we don't want to install another library just for an error assertion.
|
||||||
|
if (error.message !== USER_ALREADY_LOGGED_IN_ERROR) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.session = session;
|
||||||
|
this.lastUsed = Date.now();
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(module: grapheneLib.Module, graphene: typeof grapheneLib) {
|
||||||
|
this.module = module;
|
||||||
|
this.graphene = graphene;
|
||||||
|
|
||||||
|
this.startSessionMonitoring();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-empty-pattern
|
||||||
|
export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmServiceFactoryDep) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
// Constants for buffer structure
|
||||||
|
const IV_LENGTH = 16;
|
||||||
|
const TAG_LENGTH = 16;
|
||||||
|
|
||||||
|
let sessionManager: HsmSessionManager | null = null;
|
||||||
|
|
||||||
|
const $findKey = (session: grapheneLib.Session) => {
|
||||||
|
// Find the existing AES key
|
||||||
|
const template = {
|
||||||
|
class: graphene.ObjectClass.SECRET_KEY,
|
||||||
|
keyType: graphene.KeyType.AES,
|
||||||
|
label: appCfg.HSM_KEY_LABEL
|
||||||
|
} as grapheneLib.ITemplate;
|
||||||
|
|
||||||
|
const key = session.find(template).items(0);
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new Error("Failed to encrypt data, AES key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $keyExists = (session: grapheneLib.Session): boolean => {
|
||||||
|
try {
|
||||||
|
const key = $findKey(session);
|
||||||
|
// items(0) will throw an error if no items are found
|
||||||
|
// Return true only if we got a valid object with handle
|
||||||
|
return key && typeof key.handle !== "undefined";
|
||||||
|
} catch (error) {
|
||||||
|
// If items(0) throws, it means no key was found
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call
|
||||||
|
if ((error as any).message?.includes("CKR_OBJECT_HANDLE_INVALID")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
logger.error(error, "Error checking for HSM key presence");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isActive = async () => {
|
||||||
|
if (!module || !appCfg.isHsmConfigured || !sessionManager) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return appCfg.isHsmConfigured && module !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startService = () => {
|
||||||
|
if (!appCfg.isHsmConfigured || !module) return;
|
||||||
|
|
||||||
|
sessionManager = new HsmSessionManager(module, graphene);
|
||||||
|
const session = sessionManager.getSession();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if key already exists
|
||||||
|
if ($keyExists(session)) {
|
||||||
|
logger.info("Key already exists, skipping creation");
|
||||||
|
} else {
|
||||||
|
// Generate 256-bit AES key with persistent storage
|
||||||
|
session.generateKey(graphene.KeyGenMechanism.AES, {
|
||||||
|
class: graphene.ObjectClass.SECRET_KEY,
|
||||||
|
token: true, // This ensures the key is stored persistently
|
||||||
|
valueLen: 256 / 8,
|
||||||
|
keyType: graphene.KeyType.AES,
|
||||||
|
label: appCfg.HSM_KEY_LABEL,
|
||||||
|
encrypt: true,
|
||||||
|
decrypt: true,
|
||||||
|
extractable: false, // Prevent key export
|
||||||
|
sensitive: true, // Mark as sensitive data
|
||||||
|
private: true // Require login to access
|
||||||
|
});
|
||||||
|
logger.info(`Key created successfully with label: ${appCfg.HSM_KEY_LABEL}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mechs = session.slot.getMechanisms();
|
||||||
|
let gotAesGcmMechanism = false;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
|
for (let i = 0; i < mechs.length; i++) {
|
||||||
|
const mech = mechs.items(i);
|
||||||
|
if (mech.name === "AES_GCM") {
|
||||||
|
gotAesGcmMechanism = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gotAesGcmMechanism) {
|
||||||
|
throw new Error("Failed to initialize HSM. AES GCM encryption mechanism not supported by the HSM");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Error creating HSM key");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function encrypt(data: Buffer): Buffer {
|
||||||
|
if (!module) {
|
||||||
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionManager) {
|
||||||
|
throw new Error("HSM Session manager is not initialized");
|
||||||
|
}
|
||||||
|
const session = sessionManager.getSession();
|
||||||
|
const key = $findKey(session);
|
||||||
|
|
||||||
|
// Generate IV
|
||||||
|
const iv = session.generateRandom(IV_LENGTH);
|
||||||
|
const alg = {
|
||||||
|
name: appCfg.HSM_MECHANISM,
|
||||||
|
params: new graphene.AesGcm240Params(iv)
|
||||||
|
} as grapheneLib.IAlgorithm;
|
||||||
|
|
||||||
|
const cipher = session.createCipher(alg, new graphene.Key(key).toType());
|
||||||
|
|
||||||
|
// Calculate the output buffer size based on input length
|
||||||
|
// GCM adds a 16-byte auth tag, so we need input length + 16
|
||||||
|
const outputBuffer = Buffer.alloc(data.length + TAG_LENGTH);
|
||||||
|
const encryptedData = cipher.once(data, outputBuffer);
|
||||||
|
|
||||||
|
// Combine IV + encrypted data into a single buffer
|
||||||
|
// Format: [IV (16 bytes)][Encrypted Data][Auth Tag (16 bytes)]
|
||||||
|
return Buffer.concat([iv, encryptedData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrypt(encryptedBlob: Buffer): Buffer {
|
||||||
|
if (!module) {
|
||||||
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionManager) {
|
||||||
|
throw new Error("HSM Session manager is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = sessionManager.getSession();
|
||||||
|
const key = $findKey(session);
|
||||||
|
|
||||||
|
// Extract IV, ciphertext, and tag from the blob
|
||||||
|
const iv = encryptedBlob.subarray(0, IV_LENGTH);
|
||||||
|
const ciphertext = encryptedBlob.subarray(IV_LENGTH, encryptedBlob.length);
|
||||||
|
|
||||||
|
const algo = {
|
||||||
|
name: appCfg.HSM_MECHANISM,
|
||||||
|
params: new graphene.AesGcm240Params(iv) // Pass both IV and tag
|
||||||
|
};
|
||||||
|
|
||||||
|
const decipher = session.createDecipher(algo, new graphene.Key(key).toType());
|
||||||
|
|
||||||
|
// Allocate buffer for decrypted data
|
||||||
|
const outputBuffer = Buffer.alloc(ciphertext.length);
|
||||||
|
|
||||||
|
const decrypted = decipher.once(ciphertext, outputBuffer);
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
encrypt,
|
||||||
|
startService,
|
||||||
|
isActive,
|
||||||
|
decrypt
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -19,6 +19,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { getByteLengthForAlgorithm } from "@app/services/kms/kms-fns";
|
import { getByteLengthForAlgorithm } from "@app/services/kms/kms-fns";
|
||||||
|
|
||||||
|
import { THsmServiceFactory } from "../hsm/hsm-service";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TInternalKmsDALFactory } from "./internal-kms-dal";
|
import { TInternalKmsDALFactory } from "./internal-kms-dal";
|
||||||
@@ -40,9 +41,10 @@ type TKmsServiceFactoryDep = {
|
|||||||
kmsDAL: TKmsKeyDALFactory;
|
kmsDAL: TKmsKeyDALFactory;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "updateById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "updateById" | "transaction">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findById" | "updateById" | "transaction">;
|
orgDAL: Pick<TOrgDALFactory, "findById" | "updateById" | "transaction">;
|
||||||
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">;
|
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create" | "updateById">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
|
||||||
internalKmsDAL: Pick<TInternalKmsDALFactory, "create">;
|
internalKmsDAL: Pick<TInternalKmsDALFactory, "create">;
|
||||||
|
hsmService: THsmServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
|
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
|
||||||
@@ -63,7 +65,8 @@ export const kmsServiceFactory = ({
|
|||||||
keyStore,
|
keyStore,
|
||||||
internalKmsDAL,
|
internalKmsDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
hsmService
|
||||||
}: TKmsServiceFactoryDep) => {
|
}: TKmsServiceFactoryDep) => {
|
||||||
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
|
||||||
|
|
||||||
@@ -801,6 +804,7 @@ export const kmsServiceFactory = ({
|
|||||||
const isBase64 = !appCfg.ENCRYPTION_KEY;
|
const isBase64 = !appCfg.ENCRYPTION_KEY;
|
||||||
if (!encryptionKey) throw new Error("Root encryption key not found for KMS service.");
|
if (!encryptionKey) throw new Error("Root encryption key not found for KMS service.");
|
||||||
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
|
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
|
||||||
|
const hsmEnabled = await hsmService.isActive();
|
||||||
|
|
||||||
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
|
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
|
||||||
if (!lock) {
|
if (!lock) {
|
||||||
@@ -817,8 +821,43 @@ export const kmsServiceFactory = ({
|
|||||||
if (kmsRootConfig) {
|
if (kmsRootConfig) {
|
||||||
if (lock) await lock.release();
|
if (lock) await lock.release();
|
||||||
logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting.");
|
logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting.");
|
||||||
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
|
||||||
// set the flag so that other instancen nodes can start
|
let decryptedRootKey: Buffer | null = null;
|
||||||
|
try {
|
||||||
|
decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
||||||
|
logger.info("KMS: Decrypted ROOT Key with platform key.");
|
||||||
|
} catch (err) {
|
||||||
|
// First we attempt to decrypt with regular root key. If it fails, we check if HSM is enabled, and if it is we attempt to decrypt with HSM.
|
||||||
|
if (hsmEnabled && kmsRootConfig.isEncryptedByHsm) {
|
||||||
|
decryptedRootKey = hsmService.decrypt(kmsRootConfig.encryptedRootKey);
|
||||||
|
logger.info("KMS: Decrypted ROOT Key with HSM.");
|
||||||
|
} else {
|
||||||
|
// If HSM is not enabled we assume it's a general error, and throw.
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decryptedRootKey) {
|
||||||
|
logger.error(
|
||||||
|
{ hsmEnabled, isEncryptedByHsm: kmsRootConfig.isEncryptedByHsm },
|
||||||
|
"KMS: Failed to decrypt ROOT Key"
|
||||||
|
);
|
||||||
|
throw new Error("Failed to decrypt ROOT Key");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the key is not encrypted with HSM, we re-encrypt it with HSM and update the key in the DB.
|
||||||
|
if (!kmsRootConfig.isEncryptedByHsm && hsmEnabled) {
|
||||||
|
const encryptedRootKey = hsmService.encrypt(decryptedRootKey);
|
||||||
|
|
||||||
|
if (!encryptedRootKey) {
|
||||||
|
logger.error("KMS: Failed to encrypt ROOT Key with HSM");
|
||||||
|
throw new Error("Failed to encrypt ROOT Key with HSM");
|
||||||
|
}
|
||||||
|
|
||||||
|
await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, { encryptedRootKey, isEncryptedByHsm: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the flag so that other instance nodes can start
|
||||||
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
||||||
logger.info("KMS: Loading ROOT Key into Memory.");
|
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||||
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||||
@@ -827,9 +866,23 @@ export const kmsServiceFactory = ({
|
|||||||
|
|
||||||
logger.info("KMS: Generating ROOT Key");
|
logger.info("KMS: Generating ROOT Key");
|
||||||
const newRootKey = randomSecureBytes(32);
|
const newRootKey = randomSecureBytes(32);
|
||||||
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
|
|
||||||
|
let encryptedRootKey: Buffer | null = null;
|
||||||
|
let isEncryptedByHsm = false;
|
||||||
|
|
||||||
|
if (hsmEnabled) {
|
||||||
|
encryptedRootKey = hsmService.encrypt(newRootKey);
|
||||||
|
isEncryptedByHsm = true;
|
||||||
|
} else {
|
||||||
|
encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!encryptedRootKey) {
|
||||||
|
logger.error({ hsmEnabled }, "KMS: Failed to encrypt ROOT Key");
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
|
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
|
||||||
await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID });
|
await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID, isEncryptedByHsm });
|
||||||
|
|
||||||
// set the flag so that other instancen nodes can start
|
// set the flag so that other instancen nodes can start
|
||||||
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
||||||
|
|||||||
Reference in New Issue
Block a user