mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 15:27:46 +00:00
requested changes
requested changes temp: team debugging Revert "temp: team debugging" This reverts commit 6533d731f829d79f41bf2f7209e3a636553792b1. feat: hsm support Update hsm-service.ts feat: hsm support
This commit is contained in:
@@ -1,62 +1,115 @@
|
|||||||
name: Release standalone docker image
|
name: Release standalone docker image
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "infisical/v*.*.*-postgres"
|
- "infisical/v*.*.*-postgres"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
infisical-tests:
|
infisical-tests:
|
||||||
name: Run tests before deployment
|
name: Run tests before deployment
|
||||||
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
|
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
|
||||||
uses: ./.github/workflows/run-backend-tests.yml
|
uses: ./.github/workflows/run-backend-tests.yml
|
||||||
infisical-standalone:
|
|
||||||
name: Build infisical standalone image postgres
|
infisical-standalone:
|
||||||
runs-on: ubuntu-latest
|
name: Build infisical standalone image postgres
|
||||||
needs: [infisical-tests]
|
runs-on: ubuntu-latest
|
||||||
steps:
|
needs: [infisical-tests]
|
||||||
- name: Extract version from tag
|
steps:
|
||||||
id: extract_version
|
- name: Extract version from tag
|
||||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
id: extract_version
|
||||||
- name: ☁️ Checkout source
|
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||||
uses: actions/checkout@v3
|
- name: ☁️ Checkout source
|
||||||
with:
|
uses: actions/checkout@v3
|
||||||
fetch-depth: 0
|
with:
|
||||||
- name: 📦 Install dependencies to test all dependencies
|
fetch-depth: 0
|
||||||
run: npm ci --only-production
|
- name: 📦 Install dependencies to test all dependencies
|
||||||
working-directory: backend
|
run: npm ci --only-production
|
||||||
- name: version output
|
working-directory: backend
|
||||||
run: |
|
- name: version output
|
||||||
echo "Output Value: ${{ steps.version.outputs.major }}"
|
run: |
|
||||||
echo "Output Value: ${{ steps.version.outputs.minor }}"
|
echo "Output Value: ${{ steps.version.outputs.major }}"
|
||||||
echo "Output Value: ${{ steps.version.outputs.patch }}"
|
echo "Output Value: ${{ steps.version.outputs.minor }}"
|
||||||
echo "Output Value: ${{ steps.version.outputs.version }}"
|
echo "Output Value: ${{ steps.version.outputs.patch }}"
|
||||||
echo "Output Value: ${{ steps.version.outputs.version_type }}"
|
echo "Output Value: ${{ steps.version.outputs.version }}"
|
||||||
echo "Output Value: ${{ steps.version.outputs.increment }}"
|
echo "Output Value: ${{ steps.version.outputs.version_type }}"
|
||||||
- name: Save commit hashes for tag
|
echo "Output Value: ${{ steps.version.outputs.increment }}"
|
||||||
id: commit
|
- name: Save commit hashes for tag
|
||||||
uses: pr-mpt/actions-commit-hash@v2
|
id: commit
|
||||||
- name: 🔧 Set up Docker Buildx
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
uses: docker/setup-buildx-action@v2
|
- name: 🔧 Set up Docker Buildx
|
||||||
- name: 🐋 Login to Docker Hub
|
uses: docker/setup-buildx-action@v2
|
||||||
uses: docker/login-action@v2
|
- name: 🐋 Login to Docker Hub
|
||||||
with:
|
uses: docker/login-action@v2
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
with:
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
- name: Set up Depot CLI
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
uses: depot/setup-action@v1
|
- name: Set up Depot CLI
|
||||||
- name: 📦 Build backend and export to Docker
|
uses: depot/setup-action@v1
|
||||||
uses: depot/build-push-action@v1
|
- name: 📦 Build backend and export to Docker
|
||||||
with:
|
uses: depot/build-push-action@v1
|
||||||
project: 64mmf0n610
|
with:
|
||||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
project: 64mmf0n610
|
||||||
push: true
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
context: .
|
push: true
|
||||||
tags: |
|
context: .
|
||||||
infisical/infisical:latest-postgres
|
tags: |
|
||||||
infisical/infisical:${{ steps.commit.outputs.short }}
|
infisical/infisical:latest-postgres
|
||||||
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
infisical/infisical:${{ steps.commit.outputs.short }}
|
||||||
platforms: linux/amd64,linux/arm64
|
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
||||||
file: Dockerfile.standalone-infisical
|
platforms: linux/amd64,linux/arm64
|
||||||
build-args: |
|
file: Dockerfile.standalone-infisical
|
||||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
build-args: |
|
||||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||||
|
|
||||||
|
infisical-fips-standalone:
|
||||||
|
name: Build infisical standalone image postgres
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [infisical-tests]
|
||||||
|
steps:
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: extract_version
|
||||||
|
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: 📦 Install dependencies to test all dependencies
|
||||||
|
run: npm ci --only-production
|
||||||
|
working-directory: backend
|
||||||
|
- name: version output
|
||||||
|
run: |
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.major }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.minor }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.patch }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.version }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.version_type }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.increment }}"
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: 🐋 Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
|
- name: 📦 Build backend and export to Docker
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
push: true
|
||||||
|
context: .
|
||||||
|
tags: |
|
||||||
|
infisical/infisical-fips:latest-postgres
|
||||||
|
infisical/infisical-fips:${{ steps.commit.outputs.short }}
|
||||||
|
infisical/infisical-fips:${{ steps.extract_version.outputs.version }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
file: Dockerfile.fips.standalone-infisical
|
||||||
|
build-args: |
|
||||||
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/S
|
|||||||
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
|
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
|
||||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
|
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
|
||||||
docs/mint.json:generic-api-key:651
|
docs/mint.json:generic-api-key:651
|
||||||
|
backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
ARG POSTHOG_HOST=https://app.posthog.com
|
||||||
|
ARG POSTHOG_API_KEY=posthog-api-key
|
||||||
|
ARG INTERCOM_ID=intercom-id
|
||||||
|
ARG CAPTCHA_SITE_KEY=captcha-site-key
|
||||||
|
|
||||||
|
FROM --platform=linux/amd64 node:20-slim AS base
|
||||||
|
|
||||||
|
FROM base AS frontend-dependencies
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --only-production --ignore-scripts
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM --platform=linux/amd64 base AS frontend-builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependencies
|
||||||
|
COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||||
|
# Copy all files
|
||||||
|
COPY /frontend .
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENV NEXT_PUBLIC_ENV production
|
||||||
|
ARG POSTHOG_HOST
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
|
||||||
|
ARG POSTHOG_API_KEY
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||||
|
ARG INTERCOM_ID
|
||||||
|
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||||
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
|
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
|
ARG CAPTCHA_SITE_KEY
|
||||||
|
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
|
# Build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production image
|
||||||
|
FROM --platform=linux/amd64 base AS frontend-runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||||
|
|
||||||
|
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
|
||||||
|
VOLUME /app/.next/cache/images
|
||||||
|
|
||||||
|
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
|
||||||
|
COPY --from=frontend-builder /app/public ./public
|
||||||
|
RUN chown non-root-user:nodejs ./public/data
|
||||||
|
|
||||||
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER non-root-user
|
||||||
|
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
##
|
||||||
|
## BACKEND
|
||||||
|
##
|
||||||
|
FROM --platform=linux/amd64 base AS backend-build
|
||||||
|
|
||||||
|
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||||
|
|
||||||
|
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Required for pkcs11js
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY backend/package*.json ./
|
||||||
|
RUN npm ci --only-production
|
||||||
|
|
||||||
|
COPY /backend .
|
||||||
|
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
|
||||||
|
RUN npm i -D tsconfig-paths
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM --platform=linux/amd64 base AS backend-runner
|
||||||
|
|
||||||
|
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Required for pkcs11js
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY backend/package*.json ./
|
||||||
|
RUN npm ci --only-production
|
||||||
|
|
||||||
|
COPY --from=backend-build /app .
|
||||||
|
|
||||||
|
RUN mkdir frontend-build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM --platform=linux/amd64 base AS production
|
||||||
|
|
||||||
|
# Install necessary packages
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Infisical CLI
|
||||||
|
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
||||||
|
&& apt-get update && apt-get install -y infisical=0.31.1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||||
|
|
||||||
|
# Give non-root-user permission to update SSL certs
|
||||||
|
RUN chown -R non-root-user /etc/ssl/certs
|
||||||
|
RUN chown non-root-user /etc/ssl/certs/ca-certificates.crt
|
||||||
|
RUN chmod -R u+rwx /etc/ssl/certs
|
||||||
|
RUN chmod u+rw /etc/ssl/certs/ca-certificates.crt
|
||||||
|
RUN chown non-root-user /usr/sbin/update-ca-certificates
|
||||||
|
RUN chmod u+rx /usr/sbin/update-ca-certificates
|
||||||
|
|
||||||
|
## set pre baked keys
|
||||||
|
ARG POSTHOG_API_KEY
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||||
|
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||||
|
ARG INTERCOM_ID=intercom-id
|
||||||
|
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||||
|
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||||
|
ARG CAPTCHA_SITE_KEY
|
||||||
|
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
||||||
|
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=backend-runner /app /backend
|
||||||
|
|
||||||
|
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||||
|
|
||||||
|
ENV PORT 8080
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV HTTPS_ENABLED false
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENV STANDALONE_BUILD true
|
||||||
|
ENV STANDALONE_MODE true
|
||||||
|
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||||
|
|
||||||
|
WORKDIR /backend
|
||||||
|
|
||||||
|
ENV TELEMETRY_ENABLED true
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
EXPOSE 443
|
||||||
|
|
||||||
|
USER non-root-user
|
||||||
|
|
||||||
|
CMD ["./standalone-entrypoint.sh"]
|
||||||
Generated
+13
-18
@@ -55,7 +55,6 @@
|
|||||||
"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",
|
||||||
@@ -84,6 +83,7 @@
|
|||||||
"pg-query-stream": "^4.5.3",
|
"pg-query-stream": "^4.5.3",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
|
"pkcs11js": "^2.1.6",
|
||||||
"pkijs": "^3.2.4",
|
"pkijs": "^3.2.4",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.3.8",
|
"probot": "^13.3.8",
|
||||||
@@ -121,6 +121,7 @@
|
|||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
"@types/pg": "^8.10.9",
|
"@types/pg": "^8.10.9",
|
||||||
"@types/picomatch": "^2.3.3",
|
"@types/picomatch": "^2.3.3",
|
||||||
|
"@types/pkcs11js": "^1.0.4",
|
||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
"@types/safe-regex": "^1.1.6",
|
"@types/safe-regex": "^1.1.6",
|
||||||
@@ -8831,6 +8832,17 @@
|
|||||||
"integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==",
|
"integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pkcs11js": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pkcs11js/-/pkcs11js-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-Pkq8VbwZZv7o/6ODFOhxw0s0M8J4ucg4/I4V1dSCn8tUwWgIKIYzuV4Pp2fYuir81DgQXAF5TpGyhBMjJ3FjFw==",
|
||||||
|
"deprecated": "This is a stub types definition for pkcs11js (https://github.com/PeculiarVentures/pkcs11js). pkcs11js provides its own type definitions, so you don't need @types/pkcs11js installed!",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pkcs11js": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/prompt-sync": {
|
"node_modules/@types/prompt-sync": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz",
|
||||||
@@ -13557,23 +13569,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
"@types/pg": "^8.10.9",
|
"@types/pg": "^8.10.9",
|
||||||
"@types/picomatch": "^2.3.3",
|
"@types/picomatch": "^2.3.3",
|
||||||
|
"@types/pkcs11js": "^1.0.4",
|
||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
"@types/safe-regex": "^1.1.6",
|
"@types/safe-regex": "^1.1.6",
|
||||||
@@ -160,7 +161,6 @@
|
|||||||
"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",
|
||||||
@@ -189,6 +189,7 @@
|
|||||||
"pg-query-stream": "^4.5.3",
|
"pg-query-stream": "^4.5.3",
|
||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
|
"pkcs11js": "^2.1.6",
|
||||||
"pkijs": "^3.2.4",
|
"pkijs": "^3.2.4",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.3.8",
|
"probot": "^13.3.8",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as grapheneLib from "graphene-pk11";
|
import * as pkcs11js from "pkcs11js";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
@@ -8,26 +8,47 @@ import { HsmModule } from "./hsm-types";
|
|||||||
export const initializeHsmModule = () => {
|
export const initializeHsmModule = () => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
let module: grapheneLib.Module | null = null;
|
// Create a new instance of PKCS11 module
|
||||||
|
const pkcs11 = new pkcs11js.PKCS11();
|
||||||
|
let isInitialized = false;
|
||||||
|
|
||||||
const initialize = () => {
|
const initialize = () => {
|
||||||
if (!appCfg.isHsmConfigured) {
|
if (!appCfg.isHsmConfigured) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
module = grapheneLib.Module.load(appCfg.HSM_LIB_PATH!, "InfisicalHSM");
|
try {
|
||||||
module.initialize();
|
// Load the PKCS#11 module
|
||||||
logger.info("PKCS#11 module initialized");
|
pkcs11.load(appCfg.HSM_LIB_PATH!);
|
||||||
};
|
|
||||||
|
|
||||||
const finalize = () => {
|
// Initialize the module
|
||||||
if (module) {
|
pkcs11.C_Initialize();
|
||||||
module.finalize();
|
isInitialized = true;
|
||||||
logger.info("PKCS#11 module finalized");
|
|
||||||
|
logger.info("PKCS#11 module initialized");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to initialize PKCS#11 module:", err);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getModule = (): HsmModule => ({ module, graphene: grapheneLib });
|
const finalize = () => {
|
||||||
|
if (isInitialized) {
|
||||||
|
try {
|
||||||
|
pkcs11.C_Finalize();
|
||||||
|
isInitialized = false;
|
||||||
|
logger.info("PKCS#11 module finalized");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to finalize PKCS#11 module:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModule = (): HsmModule => ({
|
||||||
|
pkcs11,
|
||||||
|
isInitialized
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialize,
|
initialize,
|
||||||
|
|||||||
@@ -1,277 +1,339 @@
|
|||||||
import grapheneLib from "graphene-pk11";
|
import pkcs11js from "pkcs11js";
|
||||||
|
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { Lock } from "@app/lib/red-lock";
|
|
||||||
|
|
||||||
import { HsmModule, RequiredMechanisms } from "./hsm-types";
|
import { HsmKeyType, HsmModule } from "./hsm-types";
|
||||||
|
|
||||||
type THsmServiceFactoryDep = {
|
type THsmServiceFactoryDep = {
|
||||||
hsmModule: HsmModule;
|
hsmModule: HsmModule;
|
||||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const HSM_SESSION_WAIT_KEY = "wait_till_hsm_session_ready";
|
|
||||||
|
|
||||||
const USER_ALREADY_LOGGED_IN_ERROR = "CKR_USER_ALREADY_LOGGED_IN";
|
|
||||||
const WRAPPED_KEY_LENGTH = 32 + 8; // AES-256 key + padding
|
|
||||||
|
|
||||||
export type THsmServiceFactory = ReturnType<typeof hsmServiceFactory>;
|
export type THsmServiceFactory = ReturnType<typeof hsmServiceFactory>;
|
||||||
|
|
||||||
type SyncOrAsync<T> = T | Promise<T>;
|
type SyncOrAsync<T> = T | Promise<T>;
|
||||||
type SessionCallback<T> = (session: grapheneLib.Session) => SyncOrAsync<T>;
|
type SessionCallback<T> = (session: pkcs11js.Handle) => SyncOrAsync<T>;
|
||||||
|
|
||||||
// eslint-disable-next-line no-empty-pattern
|
// eslint-disable-next-line no-empty-pattern
|
||||||
export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }: THsmServiceFactoryDep) => {
|
export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsmServiceFactoryDep) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
// Constants for buffer structure
|
// Constants for buffer structures
|
||||||
const IV_LENGTH = 12;
|
const IV_LENGTH = 16; // Luna HSM typically expects 16-byte IV for cbc
|
||||||
const TAG_LENGTH = 16;
|
const BLOCK_SIZE = 16;
|
||||||
|
const HMAC_SIZE = 32;
|
||||||
|
|
||||||
const $withSession = async <T>(callbackWithSession: SessionCallback<T>): Promise<T> => {
|
const $withSession = async <T>(callbackWithSession: SessionCallback<T>): Promise<T> => {
|
||||||
const RETRY_INTERVAL = 300; // 300ms between attempts
|
const RETRY_INTERVAL = 300; // 300ms between attempts
|
||||||
const MAX_TIMEOUT = 30_000; // 30 seconds maximum total time
|
const MAX_TIMEOUT = 30_000; // 30 seconds maximum total time
|
||||||
|
|
||||||
let session: grapheneLib.Session | null = null;
|
let sessionHandle: pkcs11js.Handle | null = null;
|
||||||
let lock: Lock | null = null;
|
|
||||||
|
|
||||||
const removeSession = () => {
|
const removeSession = () => {
|
||||||
if (session) {
|
if (sessionHandle !== null) {
|
||||||
session.logout();
|
try {
|
||||||
session.close();
|
pkcs11.C_Logout(sessionHandle);
|
||||||
session = null;
|
pkcs11.C_CloseSession(sessionHandle);
|
||||||
|
logger.info("HSM: Terminated session successfully");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error during session cleanup:", error);
|
||||||
|
} finally {
|
||||||
|
sessionHandle = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!module) {
|
if (!pkcs11 || !isInitialized) {
|
||||||
throw new Error("PKCS#11 module is not initialized");
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new session
|
// Get slot list
|
||||||
const slot = module.getSlots(appCfg.HSM_SLOT);
|
let slots: pkcs11js.Handle[];
|
||||||
// eslint-disable-next-line no-bitwise
|
try {
|
||||||
if (!(slot.flags & graphene.SlotFlag.TOKEN_PRESENT)) {
|
slots = pkcs11.C_GetSlotList(false); // false to get all slots
|
||||||
throw new Error("Slot is not initialized");
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to get slot list: ${(error as Error)?.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock = await keyStore.acquireLock(["HSM_SESSION_LOCK"], 10_000, { retryCount: 3 }).catch(() => null);
|
if (slots.length === 0) {
|
||||||
|
throw new Error("No slots available");
|
||||||
if (!lock) {
|
|
||||||
await keyStore.waitTillReady({
|
|
||||||
key: HSM_SESSION_WAIT_KEY,
|
|
||||||
keyCheckCb: (val) => val === "true",
|
|
||||||
waitingCb: () => logger.info("HSM Lock: Waiting for session to be available...")
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appCfg.HSM_SLOT >= slots.length) {
|
||||||
|
throw new Error(`HSM slot ${appCfg.HSM_SLOT} not found or not initialized`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotId = slots[appCfg.HSM_SLOT];
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
while (Date.now() - startTime < MAX_TIMEOUT) {
|
while (Date.now() - startTime < MAX_TIMEOUT) {
|
||||||
try {
|
try {
|
||||||
|
// Open session
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
session = slot.open(graphene.SessionFlag.RW_SESSION | graphene.SessionFlag.SERIAL_SESSION);
|
sessionHandle = pkcs11.C_OpenSession(slotId, pkcs11js.CKF_SERIAL_SESSION | pkcs11js.CKF_RW_SESSION);
|
||||||
session.login(appCfg.HSM_PIN!);
|
|
||||||
// session.login("4311");
|
// Login
|
||||||
break;
|
try {
|
||||||
} catch (error) {
|
pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, appCfg.HSM_PIN);
|
||||||
if ((error as Error)?.message !== USER_ALREADY_LOGGED_IN_ERROR) {
|
logger.info("HSM: Successfully authenticated");
|
||||||
throw error;
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof pkcs11js.Pkcs11Error) {
|
||||||
|
// Handle specific error cases
|
||||||
|
if (error.code === pkcs11js.CKR_PIN_INCORRECT) {
|
||||||
|
logger.error(error, `Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`);
|
||||||
|
throw new Error("Incorrect HSM Pin detected. Please check the HSM configuration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) {
|
||||||
|
logger.warn("HSM session already logged in");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error; // Re-throw other errors
|
||||||
}
|
}
|
||||||
logger.warn("HSM session already logged in");
|
} catch (error) {
|
||||||
|
logger.warn(`HSM: Session creation failed. Retrying... Error: ${(error as Error)?.message}`);
|
||||||
|
|
||||||
|
if (sessionHandle !== null) {
|
||||||
|
try {
|
||||||
|
pkcs11.C_CloseSession(sessionHandle);
|
||||||
|
} catch (closeError) {
|
||||||
|
logger.error("Error closing failed session:", closeError);
|
||||||
|
}
|
||||||
|
sessionHandle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, RETRY_INTERVAL);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionHandle === null) {
|
||||||
|
throw new Error("Failed to open session after maximum retries");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute callback with session handle
|
||||||
|
const result = await callbackWithSession(sessionHandle);
|
||||||
|
removeSession();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error in HSM session handling:", error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Ensure cleanup
|
||||||
|
removeSession();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => {
|
||||||
|
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 = [
|
||||||
|
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
|
||||||
|
{ type: pkcs11js.CKA_KEY_TYPE, value: keyType },
|
||||||
|
{ type: pkcs11js.CKA_LABEL, value: label }
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize search
|
||||||
|
pkcs11.C_FindObjectsInit(sessionHandle, template);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find first matching object
|
||||||
|
const handles = pkcs11.C_FindObjects(sessionHandle, 1);
|
||||||
|
|
||||||
|
if (handles.length === 0) {
|
||||||
|
throw new Error("Failed to find master key");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.warn(`HSM: No session available. Waiting for session to be available... [retry=${RETRY_INTERVAL}ms]`);
|
return handles[0]; // Return the key handle
|
||||||
|
} finally {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// Always finalize the search operation
|
||||||
await new Promise((resolve) => {
|
pkcs11.C_FindObjectsFinal(sessionHandle);
|
||||||
setTimeout(resolve, RETRY_INTERVAL);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
if (!session) {
|
logger.error("Error finding master key:", error);
|
||||||
throw new Error("Failed to open session");
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the callback and await its result (works for both sync and async)
|
|
||||||
const result = await callbackWithSession(session);
|
|
||||||
|
|
||||||
if (session) {
|
|
||||||
removeSession();
|
|
||||||
await keyStore.setItemWithExpiry(HSM_SESSION_WAIT_KEY, 10, "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
// Clean up session if it was created
|
|
||||||
try {
|
|
||||||
removeSession();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error, "Error cleaning up HSM session:");
|
|
||||||
}
|
|
||||||
|
|
||||||
await lock?.release();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const $findMasterKey = (session: grapheneLib.Session) => {
|
const $keyExists = (session: pkcs11js.Handle, type: HsmKeyType): boolean => {
|
||||||
// Find the master key (root 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 find master key");
|
|
||||||
}
|
|
||||||
|
|
||||||
return key;
|
|
||||||
};
|
|
||||||
|
|
||||||
const $generateAndWrapKey = (session: grapheneLib.Session) => {
|
|
||||||
const masterKey = $findMasterKey(session);
|
|
||||||
|
|
||||||
// Generate a new session key for encryption
|
|
||||||
const sessionKey = session.generateKey(graphene.KeyGenMechanism.AES, {
|
|
||||||
class: graphene.ObjectClass.SECRET_KEY,
|
|
||||||
keyType: graphene.KeyType.AES,
|
|
||||||
token: false, // Session-only key
|
|
||||||
sensitive: true,
|
|
||||||
extractable: true, // Must be true to allow wrapping
|
|
||||||
encrypt: true,
|
|
||||||
decrypt: true,
|
|
||||||
valueLen: 32 // 256-bit key
|
|
||||||
} as grapheneLib.ITemplate);
|
|
||||||
|
|
||||||
// Wrap the session key with master key
|
|
||||||
const wrappingMech = { name: "AES_KEY_WRAP", params: null };
|
|
||||||
const wrappedKey = session.wrapKey(
|
|
||||||
wrappingMech,
|
|
||||||
new graphene.Key(masterKey).toType(),
|
|
||||||
new graphene.Key(sessionKey).toType()
|
|
||||||
);
|
|
||||||
|
|
||||||
return { wrappedKey, sessionKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
const $unwrapKey = (session: grapheneLib.Session, wrappedKey: Buffer) => {
|
|
||||||
const masterKey = $findMasterKey(session);
|
|
||||||
|
|
||||||
// Absolute minimal template - let HSM set most attributes
|
|
||||||
const unwrapTemplate = {
|
|
||||||
class: graphene.ObjectClass.SECRET_KEY,
|
|
||||||
keyType: graphene.KeyType.AES
|
|
||||||
} as grapheneLib.ITemplate;
|
|
||||||
|
|
||||||
const unwrappingMech = {
|
|
||||||
name: "AES_KEY_WRAP",
|
|
||||||
params: null
|
|
||||||
} as grapheneLib.MechanismType;
|
|
||||||
|
|
||||||
return session.unwrapKey(unwrappingMech, new graphene.Key(masterKey).toType(), wrappedKey, unwrapTemplate);
|
|
||||||
};
|
|
||||||
|
|
||||||
const $keyExists = (session: grapheneLib.Session): boolean => {
|
|
||||||
try {
|
try {
|
||||||
const key = $findMasterKey(session);
|
const key = $findKey(session, type);
|
||||||
// items(0) will throw an error if no items are found
|
// items(0) will throw an error if no items are found
|
||||||
// Return true only if we got a valid object with handle
|
// Return true only if we got a valid object with handle
|
||||||
return key && typeof key.handle !== "undefined";
|
return !!key && key.length > 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If items(0) throws, it means no key was found
|
// 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
|
// 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");
|
logger.error(error, "Error checking for HSM key presence");
|
||||||
|
|
||||||
|
if (error instanceof pkcs11js.Pkcs11Error) {
|
||||||
|
if (error.code === pkcs11js.CKR_OBJECT_HANDLE_INVALID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const encrypt: {
|
const encrypt: {
|
||||||
(data: Buffer, providedSession: grapheneLib.Session): Promise<Buffer>;
|
(data: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
||||||
(data: Buffer): Promise<Buffer>;
|
(data: Buffer): Promise<Buffer>;
|
||||||
} = async (data: Buffer, providedSession?: grapheneLib.Session) => {
|
} = async (data: Buffer, providedSession?: pkcs11js.Handle) => {
|
||||||
if (!module) {
|
if (!pkcs11 || !isInitialized) {
|
||||||
throw new Error("PKCS#11 module is not initialized");
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
const $performEncryption = (s: grapheneLib.Session) => {
|
const $performEncryption = (sessionHandle: pkcs11js.Handle) => {
|
||||||
// Generate IV for encryption
|
try {
|
||||||
const iv = s.generateRandom(IV_LENGTH);
|
const aesKey = $findKey(sessionHandle, HsmKeyType.AES);
|
||||||
|
if (!aesKey) {
|
||||||
|
throw new Error("AES key not found");
|
||||||
|
}
|
||||||
|
|
||||||
// Generate and wrap a new session key
|
const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC);
|
||||||
const { wrappedKey, sessionKey } = $generateAndWrapKey(s);
|
if (!hmacKey) {
|
||||||
|
throw new Error("HMAC key not found");
|
||||||
|
}
|
||||||
|
|
||||||
const alg = {
|
const iv = Buffer.alloc(IV_LENGTH);
|
||||||
name: appCfg.HSM_MECHANISM,
|
pkcs11.C_GenerateRandom(sessionHandle, iv);
|
||||||
params: new graphene.AesGcm240Params(iv)
|
|
||||||
} as grapheneLib.IAlgorithm;
|
|
||||||
|
|
||||||
const cipher = s.createCipher(alg, new graphene.Key(sessionKey).toType());
|
const encryptMechanism = {
|
||||||
|
mechanism: pkcs11js.CKM_AES_CBC_PAD,
|
||||||
|
parameter: iv
|
||||||
|
};
|
||||||
|
|
||||||
// Calculate the output buffer size based on input length
|
pkcs11.C_EncryptInit(sessionHandle, encryptMechanism, aesKey);
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Format: [Wrapped Key (40)][IV (16)][Encrypted Data + Tag]
|
// Calculate max buffer size (input length + potential full block of padding)
|
||||||
return Buffer.concat([wrappedKey, iv, encryptedData]);
|
const maxEncryptedLength = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE;
|
||||||
|
const tempBuffer = Buffer.alloc(maxEncryptedLength);
|
||||||
|
|
||||||
|
// First call to get the actual length
|
||||||
|
const encryptedLength = pkcs11.C_Encrypt(sessionHandle, data, tempBuffer);
|
||||||
|
|
||||||
|
// Create a copy of the encrypted data using the actual length
|
||||||
|
const encryptedData = Buffer.from(tempBuffer.slice(0, encryptedLength.length || 16));
|
||||||
|
|
||||||
|
// Initialize HMAC
|
||||||
|
const hmacMechanism = {
|
||||||
|
mechanism: pkcs11js.CKM_SHA256_HMAC
|
||||||
|
};
|
||||||
|
|
||||||
|
pkcs11.C_SignInit(sessionHandle, hmacMechanism, hmacKey);
|
||||||
|
|
||||||
|
// Sign the IV and encrypted data
|
||||||
|
pkcs11.C_SignUpdate(sessionHandle, iv);
|
||||||
|
pkcs11.C_SignUpdate(sessionHandle, encryptedData);
|
||||||
|
|
||||||
|
// Get the HMAC
|
||||||
|
const hmac = Buffer.alloc(HMAC_SIZE);
|
||||||
|
pkcs11.C_SignFinal(sessionHandle, hmac);
|
||||||
|
|
||||||
|
// Combine encrypted data and HMAC [Encrypted Data | HMAC]
|
||||||
|
const finalBuffer = Buffer.alloc(encryptedData.length + hmac.length);
|
||||||
|
encryptedData.copy(finalBuffer);
|
||||||
|
hmac.copy(finalBuffer, encryptedData.length);
|
||||||
|
|
||||||
|
return Buffer.concat([iv, finalBuffer]);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Encryption error:", error);
|
||||||
|
throw new Error(`Encryption failed: ${(error as Error)?.message}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (providedSession) {
|
if (providedSession) {
|
||||||
return $performEncryption(providedSession);
|
return $performEncryption(providedSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encrypted = await $withSession($performEncryption);
|
const result = await $withSession($performEncryption);
|
||||||
|
return result;
|
||||||
return encrypted;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const decrypt: {
|
const decrypt: {
|
||||||
(encryptedBlob: Buffer, providedSession: grapheneLib.Session): Promise<Buffer>;
|
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
||||||
(encryptedBlob: Buffer): Promise<Buffer>;
|
(encryptedBlob: Buffer): Promise<Buffer>;
|
||||||
} = async (encryptedBlob: Buffer, providedSession?: grapheneLib.Session) => {
|
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
|
||||||
if (!module) {
|
if (!isInitialized) {
|
||||||
throw new Error("HSM service not initialized");
|
throw new Error("HSM service not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
const $performDecryption = (s: grapheneLib.Session) => {
|
const $performDecryption = (sessionHandle: pkcs11js.Handle) => {
|
||||||
const wrappedKey = encryptedBlob.subarray(0, WRAPPED_KEY_LENGTH);
|
try {
|
||||||
const iv = encryptedBlob.subarray(WRAPPED_KEY_LENGTH, WRAPPED_KEY_LENGTH + IV_LENGTH);
|
// structure is: [IV (16 bytes) | Encrypted Data (N bytes) | HMAC (32 bytes)]
|
||||||
const ciphertext = encryptedBlob.subarray(WRAPPED_KEY_LENGTH + IV_LENGTH);
|
const iv = encryptedBlob.subarray(0, IV_LENGTH);
|
||||||
|
const encryptedDataWithHmac = encryptedBlob.subarray(IV_LENGTH);
|
||||||
|
|
||||||
// Unwrap the session key
|
// Split encrypted data and HMAC
|
||||||
const sessionKey = $unwrapKey(s, wrappedKey);
|
const hmac = encryptedDataWithHmac.subarray(-HMAC_SIZE); // Last 32 bytes are HMAC
|
||||||
|
|
||||||
const algo = {
|
const encryptedData = encryptedDataWithHmac.slice(0, -HMAC_SIZE); // Everything except last 32 bytes
|
||||||
name: appCfg.HSM_MECHANISM,
|
|
||||||
params: new graphene.AesGcm240Params(iv)
|
|
||||||
};
|
|
||||||
|
|
||||||
const decipher = s.createDecipher(algo, new graphene.Key(sessionKey).toType());
|
// Find the keys
|
||||||
const outputBuffer = Buffer.alloc(ciphertext.length);
|
const aesKey = $findKey(sessionHandle, HsmKeyType.AES);
|
||||||
|
if (!aesKey) {
|
||||||
|
throw new Error("AES key not found");
|
||||||
|
}
|
||||||
|
|
||||||
// Extract wrapped key, IV, and ciphertext
|
const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC);
|
||||||
return decipher.once(ciphertext, outputBuffer);
|
if (!hmacKey) {
|
||||||
|
throw new Error("HMAC key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify HMAC first
|
||||||
|
const hmacMechanism = {
|
||||||
|
mechanism: pkcs11js.CKM_SHA256_HMAC
|
||||||
|
};
|
||||||
|
|
||||||
|
pkcs11.C_VerifyInit(sessionHandle, hmacMechanism, hmacKey);
|
||||||
|
pkcs11.C_VerifyUpdate(sessionHandle, iv);
|
||||||
|
pkcs11.C_VerifyUpdate(sessionHandle, encryptedData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
pkcs11.C_VerifyFinal(sessionHandle, hmac);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("Decryption failed"); // Generic error for failed verification
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only decrypt if verification passed
|
||||||
|
const decryptMechanism = {
|
||||||
|
mechanism: pkcs11js.CKM_AES_CBC_PAD,
|
||||||
|
parameter: iv
|
||||||
|
};
|
||||||
|
|
||||||
|
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
|
||||||
|
|
||||||
|
const tempBuffer = Buffer.alloc(encryptedData.length);
|
||||||
|
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
|
||||||
|
|
||||||
|
// Create a new buffer from the decrypted data
|
||||||
|
return Buffer.from(decryptedData);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Decryption error:", error);
|
||||||
|
throw new Error(`Decryption failed: ${(error as Error)?.message}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (providedSession) {
|
if (providedSession) {
|
||||||
return $performDecryption(providedSession);
|
return $performDecryption(providedSession);
|
||||||
}
|
}
|
||||||
const decrypted = await $withSession($performDecryption);
|
|
||||||
|
|
||||||
return decrypted;
|
const result = await $withSession($performDecryption);
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device.
|
// We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device.
|
||||||
const $testPkcs11Module = async (session: grapheneLib.Session) => {
|
const $testPkcs11Module = async (session: pkcs11js.Handle) => {
|
||||||
try {
|
try {
|
||||||
if (!module) {
|
if (!isInitialized) {
|
||||||
throw new Error("HSM service not initialized");
|
throw new Error("HSM service not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,11 +341,15 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }:
|
|||||||
throw new Error("Session not initialized");
|
throw new Error("Session not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomData = session.generateRandom(256);
|
const randomData = pkcs11.C_GenerateRandom(session, Buffer.alloc(500));
|
||||||
const encryptedData = await encrypt(Buffer.from(randomData), session);
|
|
||||||
|
const encryptedData = await encrypt(randomData, session);
|
||||||
const decryptedData = await decrypt(encryptedData, session);
|
const decryptedData = await decrypt(encryptedData, session);
|
||||||
|
|
||||||
if (Buffer.from(randomData).toString("hex") !== Buffer.from(decryptedData).toString("hex")) {
|
const randomDataHex = randomData.toString("hex");
|
||||||
|
const decryptedDataHex = decryptedData.toString("hex");
|
||||||
|
|
||||||
|
if (randomDataHex !== decryptedDataHex && Buffer.compare(randomData, decryptedData)) {
|
||||||
throw new Error("Decrypted data does not match original data");
|
throw new Error("Decrypted data does not match original data");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +361,7 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isActive = async () => {
|
const isActive = async () => {
|
||||||
if (!module || !appCfg.isHsmConfigured) {
|
if (!isInitialized || !appCfg.isHsmConfigured) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,62 +370,94 @@ export const hsmServiceFactory = ({ hsmModule: { module, graphene }, keyStore }:
|
|||||||
try {
|
try {
|
||||||
pkcs11TestPassed = await $withSession($testPkcs11Module);
|
pkcs11TestPassed = await $withSession($testPkcs11Module);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err, "isActive: Error testing PKCS#11 module");
|
logger.error(err, "HSM: Error testing PKCS#11 module");
|
||||||
}
|
}
|
||||||
|
|
||||||
return appCfg.isHsmConfigured && module !== null && pkcs11TestPassed;
|
return appCfg.isHsmConfigured && isInitialized && pkcs11TestPassed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const startService = async () => {
|
const startService = async () => {
|
||||||
if (!appCfg.isHsmConfigured || !module) return;
|
if (!appCfg.isHsmConfigured || !pkcs11 || !isInitialized) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await $withSession(async (session) => {
|
await $withSession(async (sessionHandle) => {
|
||||||
// Check if master key exists, create if not
|
// Check if master key exists, create if not
|
||||||
if (!$keyExists(session)) {
|
|
||||||
// Generate 256-bit AES master key with persistent storage
|
const genericAttributes = [
|
||||||
session.generateKey(graphene.KeyGenMechanism.AES, {
|
{ type: pkcs11js.CKA_TOKEN, value: true }, // Persistent storage
|
||||||
class: graphene.ObjectClass.SECRET_KEY,
|
{ type: pkcs11js.CKA_EXTRACTABLE, value: false }, // Cannot be extracted
|
||||||
token: true,
|
{ type: pkcs11js.CKA_SENSITIVE, value: true }, // Sensitive value
|
||||||
valueLen: 256 / 8,
|
{ type: pkcs11js.CKA_PRIVATE, value: true } // Requires authentication
|
||||||
keyType: graphene.KeyType.AES,
|
];
|
||||||
label: appCfg.HSM_KEY_LABEL,
|
|
||||||
derive: true, // Enable key derivation
|
if (!$keyExists(sessionHandle, HsmKeyType.AES)) {
|
||||||
extractable: false,
|
// Template for generating 256-bit AES master key
|
||||||
sensitive: true,
|
const keyTemplate = [
|
||||||
private: true
|
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
|
||||||
});
|
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_AES },
|
||||||
|
{ type: pkcs11js.CKA_VALUE_LEN, value: 256 / 8 },
|
||||||
|
{ 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
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate the key
|
||||||
|
pkcs11.C_GenerateKey(
|
||||||
|
sessionHandle,
|
||||||
|
{
|
||||||
|
mechanism: pkcs11js.CKM_AES_KEY_GEN
|
||||||
|
},
|
||||||
|
keyTemplate
|
||||||
|
);
|
||||||
|
|
||||||
logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`);
|
logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify HSM supports required mechanisms
|
// Check if HMAC key exists, create if not
|
||||||
const mechs = session.slot.getMechanisms();
|
if (!$keyExists(sessionHandle, HsmKeyType.HMAC)) {
|
||||||
const mechNames: string[] = [];
|
const hmacKeyTemplate = [
|
||||||
|
{ 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: 256 / 8 }, // 256-bit key
|
||||||
|
{ 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
|
||||||
|
];
|
||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
// Generate the HMAC key
|
||||||
for (let i = 0; i < mechs.length; i++) {
|
pkcs11.C_GenerateKey(
|
||||||
mechNames.push(mechs.items(i).name);
|
sessionHandle,
|
||||||
|
{
|
||||||
|
mechanism: pkcs11js.CKM_GENERIC_SECRET_KEY_GEN
|
||||||
|
},
|
||||||
|
hmacKeyTemplate
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAesGcm = mechNames.includes(RequiredMechanisms.AesGcm);
|
// Get slot info to check supported mechanisms
|
||||||
const hasAesKeyWrap = mechNames.includes(RequiredMechanisms.AesKeyWrap);
|
const slotId = pkcs11.C_GetSessionInfo(sessionHandle).slotID;
|
||||||
|
const mechanisms = pkcs11.C_GetMechanismList(slotId);
|
||||||
|
|
||||||
if (!hasAesGcm) {
|
// Check for AES CBC PAD support
|
||||||
throw new Error(`Required mechanism ${RequiredMechanisms.AesGcm} not supported by HSM`);
|
const hasAesCbc = mechanisms.includes(pkcs11js.CKM_AES_CBC_PAD);
|
||||||
}
|
|
||||||
if (!hasAesKeyWrap) {
|
if (!hasAesCbc) {
|
||||||
throw new Error(`Required mechanism ${RequiredMechanisms.AesKeyWrap} not supported by HSM`);
|
throw new Error(`Required mechanism CKM_AEC_CBC_PAD not supported by HSM`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const testPassed = await $testPkcs11Module(session);
|
// Run test encryption/decryption
|
||||||
|
const testPassed = await $testPkcs11Module(sessionHandle);
|
||||||
|
|
||||||
// Run a test to verify module is working
|
|
||||||
if (!testPassed) {
|
if (!testPassed) {
|
||||||
throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured.");
|
throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error, "Error initializing HSM service");
|
logger.error("Error initializing HSM service:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as grapheneLib from "graphene-pk11";
|
import pkcs11js from "pkcs11js";
|
||||||
|
|
||||||
export type HsmModule = {
|
export type HsmModule = {
|
||||||
module: grapheneLib.Module | null;
|
pkcs11: pkcs11js.PKCS11;
|
||||||
graphene: typeof grapheneLib;
|
isInitialized: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum RequiredMechanisms {
|
export enum HsmKeyType {
|
||||||
AesGcm = "AES_GCM",
|
AES = "AES",
|
||||||
AesKeyWrap = "AES_KEY_WRAP"
|
HMAC = "hmac"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,35 +166,10 @@ const envSchema = z
|
|||||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
|
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
|
||||||
|
|
||||||
// HSM
|
// HSM
|
||||||
HSM_LIB_PATH: zpStr(
|
HSM_LIB_PATH: zpStr(z.string().optional()),
|
||||||
z
|
HSM_PIN: zpStr(z.string().optional()),
|
||||||
.string()
|
HSM_KEY_LABEL: zpStr(z.string().optional()),
|
||||||
.optional()
|
HSM_SLOT: z.coerce.number().optional().default(0)
|
||||||
.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"))
|
|
||||||
})
|
})
|
||||||
// To ensure that basic encryption is always possible.
|
// To ensure that basic encryption is always possible.
|
||||||
.refine(
|
.refine(
|
||||||
@@ -218,11 +193,7 @@ const envSchema = z
|
|||||||
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:
|
isHsmConfigured:
|
||||||
Boolean(data.HSM_LIB_PATH) &&
|
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
|
||||||
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(",")
|
||||||
|
|||||||
@@ -363,8 +363,7 @@ export const registerRoutes = async (
|
|||||||
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
||||||
|
|
||||||
const hsmService = hsmServiceFactory({
|
const hsmService = hsmServiceFactory({
|
||||||
hsmModule,
|
hsmModule
|
||||||
keyStore
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const kmsService = kmsServiceFactory({
|
const kmsService = kmsServiceFactory({
|
||||||
|
|||||||
Reference in New Issue
Block a user