Merge branch 'Infisical:main' into github-username

This commit is contained in:
x032205
2025-04-13 16:01:33 -04:00
committed by GitHub
49 changed files with 2334 additions and 171 deletions

View File

@@ -8,7 +8,8 @@ RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
openssh-client
openssh-client \
openssl
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apt-get install -y \

View File

@@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y \
make \
g++ \
openssh-client \
openssl \
curl \
pkg-config

View File

@@ -132,7 +132,7 @@
"@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.9.5",
"@types/node": "^20.17.30",
"@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14",
@@ -9753,11 +9753,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.9.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
"integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==",
"version": "20.17.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
"undici-types": "~6.19.2"
}
},
"node_modules/@types/node-fetch": {
@@ -20081,11 +20082,6 @@
"undici-types": "~6.19.2"
}
},
"node_modules/scim-patch/node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/scim2-parse-filter": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
@@ -22442,9 +22438,9 @@
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",

View File

@@ -89,7 +89,7 @@
"@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.9.5",
"@types/node": "^20.17.30",
"@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14",

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { KmsKeyUsage } from "@app/services/kms/kms-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasTypeColumn = await knex.schema.hasColumn(TableName.KmsKey, "type");
await knex.schema.alterTable(TableName.KmsKey, (t) => {
if (!hasTypeColumn) t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT);
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.KmsKey, (t) => {
t.dropColumn("keyUsage");
});
}

View File

@@ -16,7 +16,8 @@ export const KmsKeysSchema = z.object({
name: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
projectId: z.string().nullable().optional()
projectId: z.string().nullable().optional(),
keyUsage: z.string().default("encrypt-decrypt")
});
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;

View File

@@ -2,7 +2,7 @@ import z from "zod";
import { KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -74,7 +74,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
schema: {
description: "KMIP endpoint for creating managed objects",
body: z.object({
algorithm: z.nativeEnum(SymmetricEncryption)
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
}),
response: {
200: KmsKeysSchema
@@ -433,7 +433,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
body: z.object({
key: z.string(),
name: z.string(),
algorithm: z.nativeEnum(SymmetricEncryption)
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
}),
response: {
200: z.object({

View File

@@ -12,7 +12,8 @@ import {
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
import { TProjectPermission } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
@@ -255,6 +256,11 @@ export enum EventType {
GET_CMEK = "get-cmek",
CMEK_ENCRYPT = "cmek-encrypt",
CMEK_DECRYPT = "cmek-decrypt",
CMEK_SIGN = "cmek-sign",
CMEK_VERIFY = "cmek-verify",
CMEK_LIST_SIGNING_ALGORITHMS = "cmek-list-signing-algorithms",
CMEK_GET_PUBLIC_KEY = "cmek-get-public-key",
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
GET_PROJECT_TEMPLATES = "get-project-templates",
@@ -1997,7 +2003,7 @@ interface CreateCmekEvent {
keyId: string;
name: string;
description?: string;
encryptionAlgorithm: SymmetricEncryption;
encryptionAlgorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
};
}
@@ -2045,6 +2051,39 @@ interface CmekDecryptEvent {
};
}
interface CmekSignEvent {
type: EventType.CMEK_SIGN;
metadata: {
keyId: string;
signingAlgorithm: SigningAlgorithm;
signature: string;
};
}
interface CmekVerifyEvent {
type: EventType.CMEK_VERIFY;
metadata: {
keyId: string;
signingAlgorithm: SigningAlgorithm;
signature: string;
signatureValid: boolean;
};
}
interface CmekListSigningAlgorithmsEvent {
type: EventType.CMEK_LIST_SIGNING_ALGORITHMS;
metadata: {
keyId: string;
};
}
interface CmekGetPublicKeyEvent {
type: EventType.CMEK_GET_PUBLIC_KEY;
metadata: {
keyId: string;
};
}
interface GetExternalGroupOrgRoleMappingsEvent {
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
metadata?: Record<string, never>; // not needed, based off orgId
@@ -2639,6 +2678,10 @@ export type Event =
| GetCmeksEvent
| CmekEncryptEvent
| CmekDecryptEvent
| CmekSignEvent
| CmekVerifyEvent
| CmekListSigningAlgorithmsEvent
| CmekGetPublicKeyEvent
| GetExternalGroupOrgRoleMappingsEvent
| UpdateExternalGroupOrgRoleMappingsEvent
| GetProjectTemplatesEvent

View File

@@ -7,7 +7,7 @@ import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/er
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { KmsDataKey, KmsKeyUsage } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -115,6 +115,7 @@ export const externalKmsServiceFactory = ({
{
isReserved: false,
description,
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
name: kmsName,
orgId: actorOrgId
},

View File

@@ -92,7 +92,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
plaintext: data
});
if (!encryptedText[0].ciphertext) throw new Error("encryption failed");
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext) };
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext as Uint8Array) };
};
const decrypt = async (encryptedBlob: Buffer) => {
@@ -101,7 +101,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
ciphertext: encryptedBlob
});
if (!decryptedText[0].plaintext) throw new Error("decryption failed");
return { data: Buffer.from(decryptedText[0].plaintext) };
return { data: Buffer.from(decryptedText[0].plaintext as Uint8Array) };
};
return {

View File

@@ -258,7 +258,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
const decrypt: {
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
(encryptedBlob: Buffer): Promise<Buffer>;
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle): Promise<Buffer> => {
if (!pkcs11 || !isInitialized) {
throw new Error("PKCS#11 module is not initialized");
}
@@ -309,10 +309,10 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
const tempBuffer = Buffer.alloc(encryptedData.length);
const tempBuffer: Buffer = Buffer.alloc(encryptedData.length);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
// Create a new buffer from the decrypted data
return Buffer.from(decryptedData);
} catch (error) {
logger.error(error, "HSM: Failed to perform decryption");

View File

@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsKeyUsage } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -403,6 +404,7 @@ export const kmipOperationServiceFactory = ({
algorithm,
isReserved: false,
projectId,
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
orgId: project.orgId
});

View File

@@ -1,4 +1,4 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
@@ -49,7 +49,7 @@ type KmipOperationBaseDTO = {
} & Omit<TOrgPermission, "orgId">;
export type TKmipCreateDTO = {
algorithm: SymmetricEncryption;
algorithm: SymmetricKeyAlgorithm;
} & KmipOperationBaseDTO;
export type TKmipGetDTO = {
@@ -77,7 +77,7 @@ export type TKmipLocateDTO = KmipOperationBaseDTO;
export type TKmipRegisterDTO = {
name: string;
key: string;
algorithm: SymmetricEncryption;
algorithm: SymmetricKeyAlgorithm;
} & KmipOperationBaseDTO;
export type TSetupOrgKmipDTO = {

View File

@@ -32,7 +32,9 @@ export enum ProjectPermissionCmekActions {
Edit = "edit",
Delete = "delete",
Encrypt = "encrypt",
Decrypt = "decrypt"
Decrypt = "decrypt",
Sign = "sign",
Verify = "verify"
}
export enum ProjectPermissionDynamicSecretActions {
@@ -732,7 +734,9 @@ const buildAdminPermissionRules = () => {
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
@@ -935,7 +939,9 @@ const buildMemberPermissionRules = () => {
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);

View File

@@ -1672,7 +1672,8 @@ export const KMS = {
projectId: "The ID of the project to create the key in.",
name: "The name of the key to be created. Must be slug-friendly.",
description: "An optional description of the key.",
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key.",
type: "The type of key to be created, either encrypt-decrypt or sign-verify, based on your intended use for the key."
},
UPDATE_KEY: {
keyId: "The ID of the key to be updated.",
@@ -1705,6 +1706,28 @@ export const KMS = {
DECRYPT: {
keyId: "The ID of the key to decrypt the data with.",
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
},
LIST_SIGNING_ALGORITHMS: {
keyId: "The ID of the key to list the signing algorithms for. The key must be for signing and verifying."
},
GET_PUBLIC_KEY: {
keyId: "The ID of the key to get the public key for. The key must be for signing and verifying."
},
SIGN: {
keyId: "The ID of the key to sign the data with.",
data: "The data in string format to be signed (base64 encoded).",
isDigest:
"Whether the data is already digested or not. Please be aware that if you are passing a digest the algorithm used to create the digest must match the signing algorithm used to sign the digest.",
signingAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
},
VERIFY: {
keyId: "The ID of the key to verify the data with.",
data: "The data in string format to be verified (base64 encoded). For data larger than 4096 bytes you must first create a digest of the data and then pass the digest in the data parameter.",
signature: "The signature to be verified (base64 encoded).",
isDigest: "Whether the data is already digested or not."
}
};

View File

@@ -1,6 +1,6 @@
import crypto from "crypto";
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
import { SymmetricKeyAlgorithm, TSymmetricEncryptionFns } from "./types";
const getIvLength = () => {
return 12;
@@ -10,7 +10,9 @@ const getTagLength = () => {
return 16;
};
export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
export const symmetricCipherService = (
type: SymmetricKeyAlgorithm.AES_GCM_128 | SymmetricKeyAlgorithm.AES_GCM_256
): TSymmetricEncryptionFns => {
const IV_LENGTH = getIvLength();
const TAG_LENGTH = getTagLength();

View File

@@ -1,2 +1,2 @@
export { symmetricCipherService } from "./cipher";
export { SymmetricEncryption } from "./types";
export { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "./types";

View File

@@ -1,7 +1,18 @@
export enum SymmetricEncryption {
import { z } from "zod";
import { AsymmetricKeyAlgorithm } from "../sign/types";
// Supported symmetric encrypt/decrypt algorithms
export enum SymmetricKeyAlgorithm {
AES_GCM_256 = "aes-256-gcm",
AES_GCM_128 = "aes-128-gcm"
}
export const SymmetricKeyAlgorithmEnum = z.enum(Object.values(SymmetricKeyAlgorithm) as [string, ...string[]]).options;
export const AllowedEncryptionKeyAlgorithms = z.enum([
...Object.values(SymmetricKeyAlgorithm),
...Object.values(AsymmetricKeyAlgorithm)
] as [string, ...string[]]).options;
export type TSymmetricEncryptionFns = {
encrypt: (text: Buffer, key: Buffer) => Buffer;

View File

@@ -0,0 +1,2 @@
export { signingService } from "./signing";
export { AsymmetricKeyAlgorithm, SigningAlgorithm } from "./types";

View File

@@ -0,0 +1,539 @@
import { execFile } from "child_process";
import crypto from "crypto";
import fs from "fs/promises";
import path from "path";
import { promisify } from "util";
import { BadRequestError } from "@app/lib/errors";
import { cleanTemporaryDirectory, createTemporaryDirectory, writeToTemporaryFile } from "@app/lib/files";
import { logger } from "@app/lib/logger";
import { AsymmetricKeyAlgorithm, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types";
const execFileAsync = promisify(execFile);
interface SigningParams {
hashAlgorithm: SupportedHashAlgorithm;
padding?: number;
saltLength?: number;
}
enum SupportedHashAlgorithm {
SHA256 = "sha256",
SHA384 = "sha384",
SHA512 = "sha512"
}
const COMMAND_TIMEOUT = 15_000;
const SHA256_DIGEST_LENGTH = 32;
const SHA384_DIGEST_LENGTH = 48;
const SHA512_DIGEST_LENGTH = 64;
/**
* Service for cryptographic signing and verification operations using asymmetric keys
*
* @param algorithm The key algorithm itself. The signing algorithm is supplied in the individual sign/verify functions.
* @returns Object with sign and verify functions
*/
export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSignVerifyFns => {
const $getSigningParams = (signingAlgorithm: SigningAlgorithm): SigningParams => {
switch (signingAlgorithm) {
// RSA PSS
case SigningAlgorithm.RSASSA_PSS_SHA_512:
return {
hashAlgorithm: SupportedHashAlgorithm.SHA512,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: SHA512_DIGEST_LENGTH
};
case SigningAlgorithm.RSASSA_PSS_SHA_256:
return {
hashAlgorithm: SupportedHashAlgorithm.SHA256,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: SHA256_DIGEST_LENGTH
};
case SigningAlgorithm.RSASSA_PSS_SHA_384:
return {
hashAlgorithm: SupportedHashAlgorithm.SHA384,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: SHA384_DIGEST_LENGTH
};
// RSA PKCS#1 v1.5
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512:
return {
hashAlgorithm: SupportedHashAlgorithm.SHA512,
padding: crypto.constants.RSA_PKCS1_PADDING
};
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384:
return {
hashAlgorithm: SupportedHashAlgorithm.SHA384,
padding: crypto.constants.RSA_PKCS1_PADDING
};
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256:
return {
hashAlgorithm: SupportedHashAlgorithm.SHA256,
padding: crypto.constants.RSA_PKCS1_PADDING
};
// ECDSA
case SigningAlgorithm.ECDSA_SHA_256:
return { hashAlgorithm: SupportedHashAlgorithm.SHA256 };
case SigningAlgorithm.ECDSA_SHA_384:
return { hashAlgorithm: SupportedHashAlgorithm.SHA384 };
case SigningAlgorithm.ECDSA_SHA_512:
return { hashAlgorithm: SupportedHashAlgorithm.SHA512 };
default:
throw new Error(`Unsupported signing algorithm: ${signingAlgorithm as string}`);
}
};
const $getEcCurveName = (keyAlgorithm: AsymmetricKeyAlgorithm): { full: string; short: string } => {
// We will support more in the future
switch (keyAlgorithm) {
case AsymmetricKeyAlgorithm.ECC_NIST_P256:
return {
full: "prime256v1",
short: "p256"
};
default:
throw new Error(`Unsupported EC curve: ${keyAlgorithm}`);
}
};
const $validateAlgorithmWithKeyType = (signingAlgorithm: SigningAlgorithm) => {
const isRsaKey = algorithm.startsWith("RSA");
const isEccKey = algorithm.startsWith("ECC");
const isRsaAlgorithm = signingAlgorithm.startsWith("RSASSA");
const isEccAlgorithm = signingAlgorithm.startsWith("ECDSA");
if (isRsaKey && !isRsaAlgorithm) {
throw new BadRequestError({ message: `KMS RSA key cannot be used with ${signingAlgorithm}` });
}
if (isEccKey && !isEccAlgorithm) {
throw new BadRequestError({ message: `KMS ECC key cannot be used with ${signingAlgorithm}` });
}
};
const $signRsaDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
const tempDir = await createTemporaryDirectory("kms-rsa-sign");
const digestPath = path.join(tempDir, "digest.bin");
const sigPath = path.join(tempDir, "signature.bin");
const keyPath = path.join(tempDir, "key.pem");
try {
await writeToTemporaryFile(digestPath, digest);
await writeToTemporaryFile(keyPath, privateKey);
const { stderr } = await execFileAsync(
"openssl",
[
"pkeyutl",
"-sign",
"-in",
digestPath,
"-inkey",
keyPath,
"-pkeyopt",
`digest:${hashAlgorithm}`,
"-out",
sigPath
],
{
maxBuffer: 10 * 1024 * 1024,
timeout: COMMAND_TIMEOUT
}
);
if (stderr) {
logger.error(stderr, "KMS: Failed to sign RSA digest");
throw new BadRequestError({
message: "Failed to sign RSA digest due to signing error"
});
}
const signature = await fs.readFile(sigPath);
if (!signature) {
throw new BadRequestError({
message:
"No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest."
});
}
return signature;
} finally {
await cleanTemporaryDirectory(tempDir);
}
};
const $signEccDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
const tempDir = await createTemporaryDirectory("ecc-sign");
const digestPath = path.join(tempDir, "digest.bin");
const keyPath = path.join(tempDir, "key.pem");
const sigPath = path.join(tempDir, "signature.bin");
try {
await writeToTemporaryFile(digestPath, digest);
await writeToTemporaryFile(keyPath, privateKey);
const { stderr } = await execFileAsync(
"openssl",
[
"pkeyutl",
"-sign",
"-in",
digestPath,
"-inkey",
keyPath,
"-pkeyopt",
`digest:${hashAlgorithm}`,
"-out",
sigPath
],
{
maxBuffer: 10 * 1024 * 1024,
timeout: COMMAND_TIMEOUT
}
);
if (stderr) {
logger.error(stderr, "KMS: Failed to sign ECC digest");
throw new BadRequestError({
message: "Failed to sign ECC digest due to signing error"
});
}
const signature = await fs.readFile(sigPath);
if (!signature) {
throw new BadRequestError({
message:
"No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest."
});
}
return signature;
} finally {
await cleanTemporaryDirectory(tempDir);
}
};
const $verifyEccDigest = async (
digest: Buffer,
signature: Buffer,
publicKey: Buffer,
hashAlgorithm: SupportedHashAlgorithm
) => {
const tempDir = await createTemporaryDirectory("ecc-signature-verification");
const publicKeyFile = path.join(tempDir, "public-key.pem");
const sigFile = path.join(tempDir, "signature.sig");
const digestFile = path.join(tempDir, "digest.bin");
try {
await writeToTemporaryFile(publicKeyFile, publicKey);
await writeToTemporaryFile(sigFile, signature);
await writeToTemporaryFile(digestFile, digest);
await execFileAsync(
"openssl",
[
"pkeyutl",
"-verify",
"-in",
digestFile,
"-inkey",
publicKeyFile,
"-pubin", // Important for EC public keys
"-sigfile",
sigFile,
"-pkeyopt",
`digest:${hashAlgorithm}`
],
{ timeout: COMMAND_TIMEOUT }
);
return true;
} catch (error) {
const err = error as { stderr: string };
if (
!err?.stderr?.toLowerCase()?.includes("signature verification failure") &&
!err?.stderr?.toLowerCase()?.includes("bad signature")
) {
logger.error(error, "KMS: Failed to verify ECC signature");
}
return false;
} finally {
await cleanTemporaryDirectory(tempDir);
}
};
const $verifyRsaDigest = async (
digest: Buffer,
signature: Buffer,
publicKey: Buffer,
hashAlgorithm: SupportedHashAlgorithm
) => {
const tempDir = await createTemporaryDirectory("kms-signature-verification");
const publicKeyFile = path.join(tempDir, "public-key.pub");
const signatureFile = path.join(tempDir, "signature.sig");
const digestFile = path.join(tempDir, "digest.bin");
try {
await writeToTemporaryFile(publicKeyFile, publicKey);
await writeToTemporaryFile(signatureFile, signature);
await writeToTemporaryFile(digestFile, digest);
await execFileAsync(
"openssl",
[
"pkeyutl",
"-verify",
"-in",
digestFile,
"-inkey",
publicKeyFile,
"-pubin",
"-sigfile",
signatureFile,
"-pkeyopt",
`digest:${hashAlgorithm}`
],
{ timeout: COMMAND_TIMEOUT }
);
// it'll throw if the verification was not successful
return true;
} catch (error) {
const err = error as { stdout: string };
if (!err?.stdout?.toLowerCase()?.includes("signature verification failure")) {
logger.error(error, "KMS: Failed to verify signature");
}
return false;
} finally {
await cleanTemporaryDirectory(tempDir);
}
};
const verifyDigestFunctionsMap: Record<
AsymmetricKeyAlgorithm,
(data: Buffer, signature: Buffer, publicKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<boolean>
> = {
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $verifyEccDigest,
[AsymmetricKeyAlgorithm.RSA_4096]: $verifyRsaDigest
};
const signDigestFunctionsMap: Record<
AsymmetricKeyAlgorithm,
(data: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<Buffer>
> = {
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest,
[AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest
};
const sign = async (
data: Buffer,
privateKey: Buffer,
signingAlgorithm: SigningAlgorithm,
isDigest: boolean
): Promise<Buffer> => {
$validateAlgorithmWithKeyType(signingAlgorithm);
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
if (isDigest) {
if (signingAlgorithm.startsWith("RSASSA_PSS")) {
throw new BadRequestError({
message: "RSA PSS does not support digested input"
});
}
const signFunction = signDigestFunctionsMap[algorithm];
if (!signFunction) {
throw new BadRequestError({
message: `Digested input is not supported for key algorithm ${algorithm}`
});
}
const signature = await signFunction(data, privateKey, hashAlgorithm);
return signature;
}
const privateKeyObject = crypto.createPrivateKey({
key: privateKey,
format: "pem",
type: "pkcs8"
});
// For RSA signatures
if (signingAlgorithm.startsWith("RSA")) {
const signer = crypto.createSign(hashAlgorithm);
signer.update(data);
return signer.sign({
key: privateKeyObject,
padding,
...(signingAlgorithm.includes("PSS") ? { saltLength } : {})
});
}
if (signingAlgorithm.startsWith("ECDSA")) {
// For ECDSA signatures
const signer = crypto.createSign(hashAlgorithm);
signer.update(data);
return signer.sign({
key: privateKeyObject,
dsaEncoding: "der"
});
}
throw new BadRequestError({
message: `Signing algorithm ${signingAlgorithm} not implemented`
});
};
const verify = async (
data: Buffer,
signature: Buffer,
publicKey: Buffer,
signingAlgorithm: SigningAlgorithm,
isDigest: boolean
): Promise<boolean> => {
try {
$validateAlgorithmWithKeyType(signingAlgorithm);
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
if (isDigest) {
if (signingAlgorithm.startsWith("RSASSA_PSS")) {
throw new BadRequestError({
message: "RSA PSS does not support digested input"
});
}
const verifyFunction = verifyDigestFunctionsMap[algorithm];
if (!verifyFunction) {
throw new BadRequestError({
message: `Digested input is not supported for key algorithm ${algorithm}`
});
}
const signatureValid = await verifyFunction(data, signature, publicKey, hashAlgorithm);
return signatureValid;
}
const publicKeyObject = crypto.createPublicKey({
key: publicKey,
format: "der",
type: "spki"
});
// For RSA signatures
if (signingAlgorithm.startsWith("RSA")) {
const verifier = crypto.createVerify(hashAlgorithm);
verifier.update(data);
return verifier.verify(
{
key: publicKeyObject,
padding,
...(signingAlgorithm.includes("PSS") ? { saltLength } : {})
},
signature
);
}
// For ECDSA signatures
if (signingAlgorithm.startsWith("ECDSA")) {
const verifier = crypto.createVerify(hashAlgorithm);
verifier.update(data);
return verifier.verify(
{
key: publicKeyObject,
dsaEncoding: "der"
},
signature
);
}
throw new BadRequestError({
message: `Verification for algorithm ${signingAlgorithm} not implemented`
});
} catch (error) {
if (error instanceof BadRequestError) {
throw error;
}
logger.error(error, "KMS: Failed to verify signature");
return false;
}
};
const generateAsymmetricPrivateKey = async () => {
const { privateKey } = await new Promise<{ privateKey: string }>((resolve, reject) => {
if (algorithm.startsWith("RSA")) {
crypto.generateKeyPair(
"rsa",
{
modulusLength: Number(algorithm.split("_")[1]),
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" }
},
(err, _, pk) => {
if (err) {
reject(err);
} else {
resolve({ privateKey: pk });
}
}
);
} else {
const { full: namedCurve } = $getEcCurveName(algorithm);
crypto.generateKeyPair(
"ec",
{
namedCurve,
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" }
},
(err, _, pk) => {
if (err) {
reject(err);
} else {
resolve({
privateKey: pk
});
}
}
);
}
});
return Buffer.from(privateKey);
};
const getPublicKeyFromPrivateKey = (privateKey: Buffer) => {
const privateKeyObj = crypto.createPrivateKey({
key: privateKey,
format: "pem",
type: "pkcs8"
});
const publicKey = crypto.createPublicKey(privateKeyObj).export({
type: "spki",
format: "der"
});
return publicKey;
};
return {
sign,
verify,
generateAsymmetricPrivateKey,
getPublicKeyFromPrivateKey
};
};

View File

@@ -0,0 +1,45 @@
import { z } from "zod";
export type TAsymmetricSignVerifyFns = {
sign: (data: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm, isDigest: boolean) => Promise<Buffer>;
verify: (
data: Buffer,
signature: Buffer,
key: Buffer,
signingAlgorithm: SigningAlgorithm,
isDigest: boolean
) => Promise<boolean>;
generateAsymmetricPrivateKey: () => Promise<Buffer>;
getPublicKeyFromPrivateKey: (privateKey: Buffer) => Buffer;
};
// Supported asymmetric key types
export enum AsymmetricKeyAlgorithm {
RSA_4096 = "RSA_4096",
ECC_NIST_P256 = "ECC_NIST_P256"
}
export const AsymmetricKeyAlgorithmEnum = z.enum(
Object.values(AsymmetricKeyAlgorithm) as [string, ...string[]]
).options;
export enum SigningAlgorithm {
// RSA PSS algorithms
// These are NOT deterministic and include randomness.
// This means that the output signature is different each time for the same input.
RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512",
RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384",
RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256",
// RSA PKCS#1 v1.5 algorithms
// These are deterministic and the output is the same each time for the same input.
RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512",
RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384",
RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256",
// ECDSA algorithms
// None of these are deterministic and include randomness like RSA PSS.
ECDSA_SHA_512 = "ECDSA_SHA_512",
ECDSA_SHA_384 = "ECDSA_SHA_384",
ECDSA_SHA_256 = "ECDSA_SHA_256"
}

View File

@@ -0,0 +1,35 @@
import crypto from "crypto";
import fs from "fs/promises";
import os from "os";
import path from "path";
import { logger } from "@app/lib/logger";
const baseDir = path.join(os.tmpdir(), "infisical");
const randomPath = () => `${crypto.randomBytes(32).toString("hex")}`;
export const createTemporaryDirectory = async (name: string) => {
const tempDirPath = path.join(baseDir, `${name}-${randomPath()}`);
await fs.mkdir(tempDirPath, { recursive: true });
return tempDirPath;
};
export const removeTemporaryBaseDirectory = async () => {
await fs.rm(baseDir, { force: true, recursive: true }).catch((err) => {
logger.error(err, `Failed to remove temporary base directory [path=${baseDir}]`);
});
};
export const cleanTemporaryDirectory = async (dirPath: string) => {
await fs.rm(dirPath, { recursive: true, force: true }).catch((err) => {
logger.error(err, `Failed to cleanup temporary directory [path=${dirPath}]`);
});
};
export const writeToTemporaryFile = async (tempDirPath: string, data: string | Buffer) => {
await fs.writeFile(tempDirPath, data, { mode: 0o600 }).catch((err) => {
logger.error(err, `Failed to write to temporary file [path=${tempDirPath}]`);
throw err;
});
};

View File

@@ -0,0 +1 @@
export * from "./files";

View File

@@ -9,6 +9,7 @@ import { runMigrations } from "./auto-start-migrations";
import { initAuditLogDbConnection, initDbConnection } from "./db";
import { keyStoreFactory } from "./keystore/keystore";
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
import { removeTemporaryBaseDirectory } from "./lib/files";
import { initLogger } from "./lib/logger";
import { queueServiceFactory } from "./queue";
import { main } from "./server/app";
@@ -21,6 +22,8 @@ const run = async () => {
const logger = initLogger();
const envConfig = initEnvConfig(logger);
await removeTemporaryBaseDirectory();
const db = initDbConnection({
dbConnectionUri: envConfig.DB_CONNECTION_URI,
dbRootCert: envConfig.DB_ROOT_CERT,
@@ -71,6 +74,7 @@ const run = async () => {
process.on("SIGINT", async () => {
await server.close();
await db.destroy();
await removeTemporaryBaseDirectory();
hsmModule.finalize();
process.exit(0);
});
@@ -79,6 +83,7 @@ const run = async () => {
process.on("SIGTERM", async () => {
await server.close();
await db.destroy();
await removeTemporaryBaseDirectory();
hsmModule.finalize();
process.exit(0);
});

View File

@@ -4,13 +4,15 @@ import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { KMS } from "@app/lib/api-docs";
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
import { CmekOrderBy, TCmekKeyEncryptionAlgorithm } from "@app/services/cmek/cmek-types";
import { KmsKeyUsage } from "@app/services/kms/kms-types";
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
const keyDescriptionSchema = z.string().trim().max(500).optional();
@@ -45,16 +47,46 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
},
schema: {
description: "Create KMS key",
body: z.object({
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
encryptionAlgorithm: z
.nativeEnum(SymmetricEncryption)
.optional()
.default(SymmetricEncryption.AES_GCM_256)
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others
}),
body: z
.object({
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
keyUsage: z
.nativeEnum(KmsKeyUsage)
.optional()
.default(KmsKeyUsage.ENCRYPT_DECRYPT)
.describe(KMS.CREATE_KEY.type),
encryptionAlgorithm: z
.enum(AllowedEncryptionKeyAlgorithms)
.optional()
.default(SymmetricKeyAlgorithm.AES_GCM_256)
.describe(KMS.CREATE_KEY.encryptionAlgorithm)
})
.superRefine((data, ctx) => {
if (
data.keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT &&
!Object.values(SymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as SymmetricKeyAlgorithm)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `encryptionAlgorithm must be a valid symmetric encryption algorithm. Valid options are: ${Object.values(
SymmetricKeyAlgorithm
).join(", ")}`
});
}
if (
data.keyUsage === KmsKeyUsage.SIGN_VERIFY &&
!Object.values(AsymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as AsymmetricKeyAlgorithm)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `encryptionAlgorithm must be a valid asymmetric sign-verify algorithm. Valid options are: ${Object.values(
AsymmetricKeyAlgorithm
).join(", ")}`
});
}
}),
response: {
200: z.object({
key: CmekSchema
@@ -64,12 +96,19 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
body: { projectId, name, description, encryptionAlgorithm },
body: { projectId, name, description, encryptionAlgorithm, keyUsage },
permission
} = req;
const cmek = await server.services.cmek.createCmek(
{ orgId: permission.orgId, projectId, name, description, encryptionAlgorithm },
{
orgId: permission.orgId,
projectId,
name,
description,
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm,
keyUsage
},
permission
);
@@ -82,7 +121,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
keyId: cmek.id,
name,
description,
encryptionAlgorithm
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm
}
}
});
@@ -126,7 +165,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
projectId: cmek.projectId!,
event: {
type: EventType.UPDATE_CMEK,
metadata: {
@@ -169,7 +208,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
projectId: cmek.projectId!,
event: {
type: EventType.DELETE_CMEK,
metadata: {
@@ -282,7 +321,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
description: "Get KMS key by Name",
description: "Get KMS key by name",
params: z.object({
keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName)
}),
@@ -349,11 +388,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
permission
} = req;
const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
const { ciphertext, projectId } = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
projectId,
event: {
type: EventType.CMEK_ENCRYPT,
metadata: {
@@ -366,6 +405,198 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/keys/:keyId/public-key",
config: {
rateLimit: readLimit
},
schema: {
description:
"Get the public key for a KMS key that is used for signing and verifying data. This endpoint is only available for asymmetric keys.",
params: z.object({
keyId: z.string().uuid().describe(KMS.GET_PUBLIC_KEY.keyId)
}),
response: {
200: z.object({
publicKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId },
permission
} = req;
const { publicKey, projectId } = await server.services.cmek.getPublicKey({ keyId }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.CMEK_GET_PUBLIC_KEY,
metadata: {
keyId
}
}
});
return { publicKey };
}
});
server.route({
method: "GET",
url: "/keys/:keyId/signing-algorithms",
config: {
rateLimit: readLimit
},
schema: {
description: "List all available signing algorithms for a KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.LIST_SIGNING_ALGORITHMS.keyId)
}),
response: {
200: z.object({
signingAlgorithms: z.array(z.nativeEnum(SigningAlgorithm))
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { keyId } = req.params;
const { signingAlgorithms, projectId } = await server.services.cmek.listSigningAlgorithms(
{ keyId },
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.CMEK_LIST_SIGNING_ALGORITHMS,
metadata: {
keyId
}
}
});
return { signingAlgorithms };
}
});
server.route({
method: "POST",
url: "/keys/:keyId/sign",
config: {
rateLimit: writeLimit
},
schema: {
description: "Sign data with a KMS key.",
params: z.object({
keyId: z.string().uuid().describe(KMS.SIGN.keyId)
}),
body: z.object({
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
isDigest: z.boolean().optional().default(false).describe(KMS.SIGN.isDigest),
data: base64Schema.describe(KMS.SIGN.data)
}),
response: {
200: z.object({
signature: z.string(),
keyId: z.string().uuid(),
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId: inputKeyId },
body: { data, signingAlgorithm, isDigest },
permission
} = req;
const { projectId, ...result } = await server.services.cmek.cmekSign(
{ keyId: inputKeyId, data, signingAlgorithm, isDigest },
permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.CMEK_SIGN,
metadata: {
keyId: inputKeyId,
signingAlgorithm,
signature: result.signature
}
}
});
return result;
}
});
server.route({
method: "POST",
url: "/keys/:keyId/verify",
config: {
rateLimit: writeLimit
},
schema: {
description: "Verify data signatures with a KMS key.",
params: z.object({
keyId: z.string().uuid().describe(KMS.VERIFY.keyId)
}),
body: z.object({
isDigest: z.boolean().optional().default(false).describe(KMS.VERIFY.isDigest),
data: base64Schema.describe(KMS.VERIFY.data),
signature: base64Schema.describe(KMS.VERIFY.signature),
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
}),
response: {
200: z.object({
signatureValid: z.boolean(),
keyId: z.string().uuid(),
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
params: { keyId },
body: { data, signature, signingAlgorithm, isDigest },
permission
} = req;
const { projectId, ...result } = await server.services.cmek.cmekVerify(
{ keyId, data, signature, signingAlgorithm, isDigest },
permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.CMEK_VERIFY,
metadata: {
keyId,
signatureValid: result.signatureValid,
signingAlgorithm,
signature
}
}
});
return result;
}
});
server.route({
method: "POST",
url: "/keys/:keyId/decrypt",
@@ -394,11 +625,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
permission
} = req;
const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
const { plaintext, projectId } = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: permission.orgId,
projectId,
event: {
type: EventType.CMEK_DECRYPT,
metadata: {

View File

@@ -3,12 +3,18 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { SigningAlgorithm } from "@app/lib/crypto/sign";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import {
TCmekDecryptDTO,
TCmekEncryptDTO,
TCmekGetPublicKeyDTO,
TCmekKeyEncryptionAlgorithm,
TCmekListSigningAlgorithmsDTO,
TCmekSignDTO,
TCmekVerifyDTO,
TCreateCmekDTO,
TListCmeksByProjectIdDTO,
TUpdabteCmekByIdDTO
@@ -16,6 +22,7 @@ import {
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsKeyUsage } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
type TCmekServiceFactoryDep = {
@@ -221,7 +228,151 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") });
return cipherTextBlob.toString("base64");
return {
ciphertext: cipherTextBlob.toString("base64"),
projectId: key.projectId
};
};
const listSigningAlgorithms = async ({ keyId }: TCmekListSigningAlgorithmsDTO, actor: OrgServiceActor) => {
const key = await kmsDAL.findCmekById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId: key.projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
if (key.keyUsage !== KmsKeyUsage.SIGN_VERIFY) {
throw new BadRequestError({ message: `Key with ID '${keyId}' is not intended for signing` });
}
const encryptionAlgorithm = key.encryptionAlgorithm as TCmekKeyEncryptionAlgorithm;
const algos = [
{
keyAlgorithm: "rsa",
signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) =>
algorithm.toLowerCase().startsWith("rsa")
)
},
{
keyAlgorithm: "ecc",
signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) =>
algorithm.toLowerCase().startsWith("ecdsa")
)
}
];
const selectedAlgorithm = algos.find((algo) => encryptionAlgorithm.toLowerCase().startsWith(algo.keyAlgorithm));
if (!selectedAlgorithm) {
throw new BadRequestError({ message: `Unsupported encryption algorithm: ${encryptionAlgorithm}` });
}
return { signingAlgorithms: selectedAlgorithm.signingAlgorithms, projectId: key.projectId };
};
const getPublicKey = async ({ keyId }: TCmekGetPublicKeyDTO, actor: OrgServiceActor) => {
const key = await kmsDAL.findCmekById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId: key.projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
const publicKey = await kmsService.getPublicKey({ kmsId: keyId });
return { publicKey: publicKey.toString("base64"), projectId: key.projectId };
};
const cmekSign = async ({ keyId, data, signingAlgorithm, isDigest }: TCmekSignDTO, actor: OrgServiceActor) => {
const key = await kmsDAL.findCmekById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId: key.projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Sign, ProjectPermissionSub.Cmek);
const sign = await kmsService.signWithKmsKey({ kmsId: keyId });
const { signature, algorithm } = await sign({ data: Buffer.from(data, "base64"), signingAlgorithm, isDigest });
return {
signature: signature.toString("base64"),
keyId: key.id,
projectId: key.projectId,
signingAlgorithm: algorithm
};
};
const cmekVerify = async (
{ keyId, data, signature, signingAlgorithm, isDigest }: TCmekVerifyDTO,
actor: OrgServiceActor
) => {
const key = await kmsDAL.findCmekById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId: key.projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Verify, ProjectPermissionSub.Cmek);
const verify = await kmsService.verifyWithKmsKey({ kmsId: keyId, signingAlgorithm });
const { signatureValid, algorithm } = await verify({
isDigest,
data: Buffer.from(data, "base64"),
signature: Buffer.from(signature, "base64")
});
return {
signatureValid,
keyId: key.id,
projectId: key.projectId,
signingAlgorithm: algorithm
};
};
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
@@ -248,7 +399,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") });
return plaintextBlob.toString("base64");
return {
plaintext: plaintextBlob.toString("base64"),
projectId: key.projectId
};
};
return {
@@ -259,6 +413,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
cmekEncrypt,
cmekDecrypt,
findCmekById,
findCmekByName
findCmekByName,
cmekSign,
cmekVerify,
listSigningAlgorithms,
getPublicKey
};
};

View File

@@ -1,12 +1,18 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign";
import { OrderByDirection } from "@app/lib/types";
import { KmsKeyUsage } from "../kms/kms-types";
export type TCmekKeyEncryptionAlgorithm = SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
export type TCreateCmekDTO = {
orgId: string;
projectId: string;
name: string;
description?: string;
encryptionAlgorithm: SymmetricEncryption;
encryptionAlgorithm: TCmekKeyEncryptionAlgorithm;
keyUsage: KmsKeyUsage;
};
export type TUpdabteCmekByIdDTO = {
@@ -38,3 +44,26 @@ export type TCmekDecryptDTO = {
export enum CmekOrderBy {
Name = "name"
}
export type TCmekListSigningAlgorithmsDTO = {
keyId: string;
};
export type TCmekGetPublicKeyDTO = {
keyId: string;
};
export type TCmekSignDTO = {
keyId: string;
data: string;
signingAlgorithm: SigningAlgorithm;
isDigest: boolean;
};
export type TCmekVerifyDTO = {
keyId: string;
data: string;
signature: string;
signingAlgorithm: SigningAlgorithm;
isDigest: boolean;
};

View File

@@ -1,13 +1,55 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
import { AsymmetricKeyAlgorithm } from "@app/lib/crypto/sign";
import { BadRequestError } from "@app/lib/errors";
import { KmsKeyUsage } from "./kms-types";
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
export const getByteLengthForSymmetricEncryptionAlgorithm = (encryptionAlgorithm: SymmetricKeyAlgorithm) => {
switch (encryptionAlgorithm) {
case SymmetricEncryption.AES_GCM_128:
case SymmetricKeyAlgorithm.AES_GCM_128:
return 16;
case SymmetricEncryption.AES_GCM_256:
case SymmetricKeyAlgorithm.AES_GCM_256:
default:
return 32;
}
};
export const verifyKeyTypeAndAlgorithm = (
keyUsage: KmsKeyUsage,
algorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm,
extra?: {
forceType?: KmsKeyUsage;
}
) => {
if (extra?.forceType && keyUsage !== extra.forceType) {
throw new BadRequestError({
message: `Unsupported key type, expected ${extra.forceType} but got ${keyUsage}`
});
}
if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
if (!Object.values(SymmetricKeyAlgorithm).includes(algorithm as SymmetricKeyAlgorithm)) {
throw new BadRequestError({
message: `Unsupported encryption algorithm for encrypt/decrypt key: ${algorithm as string}`
});
}
return true;
}
if (keyUsage === KmsKeyUsage.SIGN_VERIFY) {
if (!Object.values(AsymmetricKeyAlgorithm).includes(algorithm as AsymmetricKeyAlgorithm)) {
throw new BadRequestError({
message: `Unsupported sign/verify algorithm for sign/verify key: ${algorithm as string}`
});
}
return true;
}
throw new BadRequestError({
message: `Unsupported key type: ${keyUsage as string}`
});
};

View File

@@ -15,12 +15,17 @@ import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { TEnvConfig } from "@app/lib/config/env";
import { randomSecureBytes } from "@app/lib/crypto";
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
import { symmetricCipherService, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
import { generateHash } from "@app/lib/crypto/encryption";
import { AsymmetricKeyAlgorithm, signingService } from "@app/lib/crypto/sign";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { getByteLengthForAlgorithm, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns";
import {
getByteLengthForSymmetricEncryptionAlgorithm,
KMS_ROOT_CONFIG_UUID,
verifyKeyTypeAndAlgorithm
} from "@app/services/kms/kms-fns";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
@@ -29,6 +34,7 @@ import { TKmsKeyDALFactory } from "./kms-key-dal";
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
import {
KmsDataKey,
KmsKeyUsage,
KmsType,
RootKeyEncryptionStrategy,
TDecryptWithKeyDTO,
@@ -38,8 +44,11 @@ import {
TEncryptWithKmsDTO,
TGenerateKMSDTO,
TGetKeyMaterialDTO,
TGetPublicKeyDTO,
TImportKeyMaterialDTO,
TUpdateProjectSecretManagerKmsKeyDTO
TSignWithKmsDTO,
TUpdateProjectSecretManagerKmsKeyDTO,
TVerifyWithKmsDTO
} from "./kms-types";
type TKmsServiceFactoryDep = {
@@ -83,19 +92,42 @@ export const kmsServiceFactory = ({
tx,
name,
projectId,
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
encryptionAlgorithm = SymmetricKeyAlgorithm.AES_GCM_256,
keyUsage = KmsKeyUsage.ENCRYPT_DECRYPT,
description
}: TGenerateKMSDTO) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
// daniel: ensure that the key type (sign/encrypt) and the encryption algorithm are compatible.
verifyKeyTypeAndAlgorithm(keyUsage, encryptionAlgorithm);
const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm));
let kmsKeyMaterial: Buffer | null = null;
if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
kmsKeyMaterial = randomSecureBytes(
getByteLengthForSymmetricEncryptionAlgorithm(encryptionAlgorithm as SymmetricKeyAlgorithm)
);
} else if (keyUsage === KmsKeyUsage.SIGN_VERIFY) {
const { generateAsymmetricPrivateKey, getPublicKeyFromPrivateKey } = signingService(
encryptionAlgorithm as AsymmetricKeyAlgorithm
);
kmsKeyMaterial = await generateAsymmetricPrivateKey();
// daniel: safety check to ensure we're able to extract the public key from the private key before we proceed to key creation
getPublicKeyFromPrivateKey(kmsKeyMaterial);
}
if (!kmsKeyMaterial) {
throw new BadRequestError({
message: `Invalid KMS key type. No key material was created for key usage '${keyUsage}' using algorithm '${encryptionAlgorithm}'`
});
}
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
const dbQuery = async (db: Knex) => {
const kmsDoc = await kmsDAL.create(
{
name: sanitizedName,
keyUsage,
orgId,
isReserved,
projectId,
@@ -115,6 +147,7 @@ export const kmsServiceFactory = ({
);
return kmsDoc;
};
if (tx) return dbQuery(tx);
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
return doc;
@@ -134,7 +167,7 @@ export const kmsServiceFactory = ({
*/
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
// Buffer#1 encrypted text + Buffer#2 version number
@@ -149,7 +182,7 @@ export const kmsServiceFactory = ({
* This can be even later exposed directly as api for encryption as function
*/
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
@@ -227,7 +260,7 @@ export const kmsServiceFactory = ({
};
const encryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
return (plainTextBuffer: Buffer) => {
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
@@ -236,7 +269,7 @@ export const kmsServiceFactory = ({
};
const decryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
return (cipherTextBuffer: Buffer) => {
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
@@ -255,6 +288,11 @@ export const kmsServiceFactory = ({
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
}
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
});
if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns;
@@ -316,8 +354,8 @@ export const kmsServiceFactory = ({
}
// internal KMS
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const dataCipher = symmetricCipherService(encryptionAlgorithm);
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
@@ -345,19 +383,22 @@ export const kmsServiceFactory = ({
});
}
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
return kmsKey;
};
const importKeyMaterial = async (
{ key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO,
{ key, algorithm, name, isReserved, projectId, orgId, keyUsage }: TImportKeyMaterialDTO,
tx?: Knex
) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
// daniel: currently we only support imports for encrypt/decrypt keys
verifyKeyTypeAndAlgorithm(keyUsage, algorithm, { forceType: KmsKeyUsage.ENCRYPT_DECRYPT });
const expectedByteLength = getByteLengthForAlgorithm(algorithm);
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const expectedByteLength = getByteLengthForSymmetricEncryptionAlgorithm(algorithm as SymmetricKeyAlgorithm);
if (key.byteLength !== expectedByteLength) {
throw new BadRequestError({
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
@@ -370,6 +411,7 @@ export const kmsServiceFactory = ({
const kmsDoc = await kmsDAL.create(
{
name: sanitizedName,
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
orgId,
isReserved,
projectId
@@ -393,12 +435,85 @@ export const kmsServiceFactory = ({
return doc;
};
const getPublicKey = async ({ kmsId }: TGetPublicKeyDTO) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
if (!kmsDoc) {
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
}
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
forceType: KmsKeyUsage.SIGN_VERIFY
});
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
return signingService(encryptionAlgorithm).getPublicKeyFromPrivateKey(kmsKey);
};
const signWithKmsKey = async ({ kmsId }: Pick<TSignWithKmsDTO, "kmsId">) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
if (!kmsDoc) {
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
}
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
forceType: KmsKeyUsage.SIGN_VERIFY
});
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const { sign } = signingService(encryptionAlgorithm);
return async ({
data,
signingAlgorithm,
isDigest
}: Pick<TSignWithKmsDTO, "data" | "signingAlgorithm" | "isDigest">) => {
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
const signature = await sign(data, kmsKey, signingAlgorithm, isDigest);
return Promise.resolve({ signature, algorithm: signingAlgorithm });
};
};
const verifyWithKmsKey = async ({
kmsId,
signingAlgorithm
}: Pick<TVerifyWithKmsDTO, "kmsId" | "signingAlgorithm">) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
if (!kmsDoc) {
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
}
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
forceType: KmsKeyUsage.SIGN_VERIFY
});
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const { verify, getPublicKeyFromPrivateKey } = signingService(encryptionAlgorithm);
return async ({ data, signature, isDigest }: Pick<TVerifyWithKmsDTO, "data" | "signature" | "isDigest">) => {
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
const publicKey = getPublicKeyFromPrivateKey(kmsKey);
const signatureValid = await verify(data, signature, publicKey, signingAlgorithm, isDigest);
return Promise.resolve({ signatureValid, algorithm: signingAlgorithm });
};
};
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
if (!kmsDoc) {
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
}
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
});
if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns;
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
@@ -454,8 +569,8 @@ export const kmsServiceFactory = ({
}
// internal KMS
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const dataCipher = symmetricCipherService(encryptionAlgorithm);
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
@@ -729,7 +844,7 @@ export const kmsServiceFactory = ({
// case 2: root key is encrypted with software encryption
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const encryptionKeyBuffer = $getBasicEncryptionKey();
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
@@ -749,7 +864,7 @@ export const kmsServiceFactory = ({
}
if (strategy === RootKeyEncryptionStrategy.Software) {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const encryptionKeyBuffer = $getBasicEncryptionKey();
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
@@ -765,7 +880,7 @@ export const kmsServiceFactory = ({
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
const dataKey = await $getDataKey(encryptionContext, trx);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
return {
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
@@ -966,6 +1081,7 @@ export const kmsServiceFactory = ({
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
logger.info("KMS: Loading ROOT Key into Memory.");
ROOT_ENCRYPTION_KEY = decryptedRootKey;
};
@@ -1014,6 +1130,9 @@ export const kmsServiceFactory = ({
getKmsById,
createCipherPairWithDataKey,
getKeyMaterial,
importKeyMaterial
importKeyMaterial,
signWithKmsKey,
verifyWithKmsKey,
getPublicKey
};
};

View File

@@ -1,6 +1,7 @@
import { Knex } from "knex";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
export enum KmsDataKey {
Organization,
@@ -13,6 +14,11 @@ export enum KmsType {
Internal = "internal"
}
export enum KmsKeyUsage {
ENCRYPT_DECRYPT = "encrypt-decrypt",
SIGN_VERIFY = "sign-verify"
}
export type TEncryptWithKmsDataKeyDTO =
| { type: KmsDataKey.Organization; orgId: string }
| { type: KmsDataKey.SecretManager; projectId: string };
@@ -25,7 +31,8 @@ export type TEncryptWithKmsDataKeyDTO =
export type TGenerateKMSDTO = {
orgId: string;
projectId?: string;
encryptionAlgorithm?: SymmetricEncryption;
encryptionAlgorithm?: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
keyUsage?: KmsKeyUsage;
isReserved?: boolean;
name?: string;
description?: string;
@@ -37,6 +44,25 @@ export type TEncryptWithKmsDTO = {
plainText: Buffer;
};
export type TGetPublicKeyDTO = {
kmsId: string;
};
export type TSignWithKmsDTO = {
kmsId: string;
data: Buffer;
signingAlgorithm: SigningAlgorithm;
isDigest: boolean;
};
export type TVerifyWithKmsDTO = {
kmsId: string;
data: Buffer;
signature: Buffer;
signingAlgorithm: SigningAlgorithm;
isDigest: boolean;
};
export type TEncryptionWithKeyDTO = {
key: Buffer;
plainText: Buffer;
@@ -67,9 +93,10 @@ export type TGetKeyMaterialDTO = {
export type TImportKeyMaterialDTO = {
key: Buffer;
algorithm: SymmetricEncryption;
algorithm: SymmetricKeyAlgorithm;
name?: string;
isReserved: boolean;
projectId: string;
orgId: string;
keyUsage: KmsKeyUsage;
};