feat(kms): sign & verify data

This commit is contained in:
Daniel Hougaard
2025-04-03 07:17:29 +04:00
parent ebcf4761b6
commit c6ca668db9
36 changed files with 1873 additions and 162 deletions
@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { KmsKeyIntent } 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("type").notNullable().defaultTo(KmsKeyIntent.ENCRYPT_DECRYPT);
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.KmsKey, (t) => {
t.dropColumn("type");
});
}
+2 -1
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(),
type: z.string().default("encrypt-decrypt")
});
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
+3 -2
View File
@@ -23,10 +23,11 @@ export const OrganizationsSchema = z.object({
defaultMembershipRole: z.string().default("member"),
enforceMfa: z.boolean().default(false),
selectedMfaMethod: z.string().nullable().optional(),
secretShareSendToAnyone: z.boolean().default(true).nullable().optional(),
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
shouldUseNewPrivilegeSystem: z.boolean().default(true),
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
privilegeUpgradeInitiatedAt: z.date().nullable().optional()
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
+3 -3
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 { SymmetricKeyEncryptDecrypt } 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(SymmetricKeyEncryptDecrypt)
}),
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(SymmetricKeyEncryptDecrypt)
}),
response: {
200: z.object({
@@ -4,7 +4,8 @@ import {
} from "@app/ee/services/project-template/project-template-types";
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
import { AsymmetricKeySignVerify } 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";
@@ -1897,7 +1898,7 @@ interface CreateCmekEvent {
keyId: string;
name: string;
description?: string;
encryptionAlgorithm: SymmetricEncryption;
encryptionAlgorithm: SymmetricKeyEncryptDecrypt | AsymmetricKeySignVerify;
};
}
@@ -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, KmsKeyIntent } 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,
type: KmsKeyIntent.ENCRYPT_DECRYPT,
name: kmsName,
orgId: actorOrgId
},
@@ -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 {
+3 -3
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");
@@ -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 { KmsKeyIntent } 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,
type: KmsKeyIntent.ENCRYPT_DECRYPT,
orgId: project.orgId
});
+3 -3
View File
@@ -1,4 +1,4 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyEncryptDecrypt } 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: SymmetricKeyEncryptDecrypt;
} & KmipOperationBaseDTO;
export type TKmipGetDTO = {
@@ -77,7 +77,7 @@ export type TKmipLocateDTO = KmipOperationBaseDTO;
export type TKmipRegisterDTO = {
name: string;
key: string;
algorithm: SymmetricEncryption;
algorithm: SymmetricKeyEncryptDecrypt;
} & KmipOperationBaseDTO;
export type TSetupOrgKmipDTO = {
@@ -32,7 +32,9 @@ export enum ProjectPermissionCmekActions {
Edit = "edit",
Delete = "delete",
Encrypt = "encrypt",
Decrypt = "decrypt"
Decrypt = "decrypt",
Sign = "sign",
Verify = "verify"
}
export enum ProjectPermissionDynamicSecretActions {
@@ -650,7 +652,9 @@ const buildAdminPermissionRules = () => {
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
@@ -839,7 +843,9 @@ const buildMemberPermissionRules = () => {
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
+22 -2
View File
@@ -633,7 +633,8 @@ export const FOLDERS = {
path: "The path to list folders from.",
directory: "The directory to list folders from. (Deprecated in favor of path)",
recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories.",
lastSecretModified: "The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)"
lastSecretModified:
"The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)"
},
GET_BY_ID: {
folderId: "The ID of the folder to get details."
@@ -1590,7 +1591,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.",
@@ -1623,6 +1625,24 @@ 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)."
},
VERIFY: {
keyId: "The ID of the key to verify the data with.",
data: "The data in string format to be verified (base64 encoded).",
signature: "The signature to be verified (base64 encoded)."
}
};
+4 -2
View File
@@ -1,6 +1,6 @@
import crypto from "crypto";
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
import { SymmetricKeyEncryptDecrypt, 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: SymmetricKeyEncryptDecrypt.AES_GCM_128 | SymmetricKeyEncryptDecrypt.AES_GCM_256
): TSymmetricEncryptionFns => {
const IV_LENGTH = getIvLength();
const TAG_LENGTH = getTagLength();
+1 -1
View File
@@ -1,2 +1,2 @@
export { symmetricCipherService } from "./cipher";
export { SymmetricEncryption } from "./types";
export { AllowedEncryptionKeyAlgorithms, SymmetricKeyEncryptDecrypt } from "./types";
+14 -1
View File
@@ -1,7 +1,20 @@
export enum SymmetricEncryption {
import { z } from "zod";
import { AsymmetricKeySignVerify } from "../sign/types";
// Supported symmetric encrypt/decrypt algorithms
export enum SymmetricKeyEncryptDecrypt {
AES_GCM_256 = "aes-256-gcm",
AES_GCM_128 = "aes-128-gcm"
}
export const SymmetricKeyEncryptDecryptEnum = z.enum(
Object.values(SymmetricKeyEncryptDecrypt) as [string, ...string[]]
).options;
export const AllowedEncryptionKeyAlgorithms = z.enum([
...Object.values(SymmetricKeyEncryptDecrypt),
...Object.values(AsymmetricKeySignVerify)
] as [string, ...string[]]).options;
export type TSymmetricEncryptionFns = {
encrypt: (text: Buffer, key: Buffer) => Buffer;
+2
View File
@@ -0,0 +1,2 @@
export { signingService } from "./signing";
export { AsymmetricKeySignVerify, SigningAlgorithm } from "./types";
+287
View File
@@ -0,0 +1,287 @@
import crypto from "crypto";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { AsymmetricKeySignVerify, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types";
// Map of signing algorithms to their parameters
interface SigningParams {
hashAlgorithm: string;
padding?: number; // Will use crypto.constants values
saltLength?: number;
}
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 signing algorithm to use
* @returns Object with sign and verify functions
*/
export const signingService = (algorithm: AsymmetricKeySignVerify): TAsymmetricSignVerifyFns => {
const $getSigningParams = (signingAlgorithm: SigningAlgorithm): SigningParams => {
switch (signingAlgorithm) {
// RSA PSS
case SigningAlgorithm.RSASSA_PSS_SHA_256:
return {
hashAlgorithm: "sha256",
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: SHA256_DIGEST_LENGTH
};
case SigningAlgorithm.RSASSA_PSS_SHA_384:
return {
hashAlgorithm: "sha384",
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: SHA384_DIGEST_LENGTH
};
case SigningAlgorithm.RSASSA_PSS_SHA_512:
return {
hashAlgorithm: "sha512",
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: SHA512_DIGEST_LENGTH
};
// RSA PKCS#1 v1.5
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256:
return {
hashAlgorithm: "sha256",
padding: crypto.constants.RSA_PKCS1_PADDING
};
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384:
return {
hashAlgorithm: "sha384",
padding: crypto.constants.RSA_PKCS1_PADDING
};
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512:
return {
hashAlgorithm: "sha512",
padding: crypto.constants.RSA_PKCS1_PADDING
};
// ECDSA
case SigningAlgorithm.ECDSA_SHA_256:
return { hashAlgorithm: "sha256" };
case SigningAlgorithm.ECDSA_SHA_384:
return { hashAlgorithm: "sha384" };
case SigningAlgorithm.ECDSA_SHA_512:
return { hashAlgorithm: "sha512" };
default:
throw new Error(`Unsupported signing algorithm: ${signingAlgorithm as string}`);
}
};
// For ECC key generation, nodejs has some strange and hardly documented curve naming conventions
const $getEcCurveName = (keyAlgorithm: AsymmetricKeySignVerify): string => {
// We will support more in the future
switch (keyAlgorithm) {
case AsymmetricKeySignVerify.ECC_NIST_P256:
return "prime256v1";
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 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 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) => {
if (algorithm.startsWith("rsa")) {
// For RSA keys in PEM format
const privateKeyObj = crypto.createPrivateKey({
key: privateKey,
format: "pem",
type: "pkcs8"
});
const publicKey = crypto.createPublicKey(privateKeyObj).export({
type: "spki",
format: "pem"
});
if (Buffer.isBuffer(publicKey)) {
return publicKey;
}
return Buffer.from(publicKey);
}
const privateKeyObj = crypto.createPrivateKey({
key: privateKey,
format: "pem",
type: "pkcs8"
});
// Return public key in PEM format for both RSA and EC
const publicKey = crypto.createPublicKey(privateKeyObj).export({
type: "spki",
format: "pem"
});
if (Buffer.isBuffer(publicKey)) {
return publicKey;
}
return Buffer.from(publicKey);
};
const sign = (data: Buffer, privateKey: Buffer, signingAlgorithm: SigningAlgorithm): Buffer => {
$validateAlgorithmWithKeyType(signingAlgorithm);
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
const privateKeyObject = crypto.createPrivateKey({
key: privateKey,
format: "pem",
type: "pkcs8"
});
// For RSA signatures
if (signingAlgorithm.startsWith("RSASSA")) {
const signer = crypto.createSign(hashAlgorithm);
signer.update(data);
if (signingAlgorithm.includes("PSS")) {
// For PSS padding
return signer.sign({
key: privateKeyObject,
padding,
saltLength
});
}
// For PKCS1 v1.5 padding
return signer.sign({
key: privateKeyObject
});
}
if (signingAlgorithm.startsWith("ECDSA")) {
// For ECDSA signatures
const signer = crypto.createSign(hashAlgorithm);
signer.update(data);
return signer.sign({
key: privateKeyObject,
dsaEncoding: "ieee-p1363" // Based on AWS KMS implementation, where ECDSA signatures follow the ANSI X9.62-2005 format, which is equivalent to the IEEE-P1363 format
});
}
throw new BadRequestError({
message: `Signing algorithm ${signingAlgorithm} not implemented`
});
};
const verify = (data: Buffer, signature: Buffer, publicKey: Buffer, signingAlgorithm: SigningAlgorithm): boolean => {
try {
$validateAlgorithmWithKeyType(signingAlgorithm);
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
// For RSA signatures
if (signingAlgorithm.startsWith("RSASSA")) {
const verifier = crypto.createVerify(hashAlgorithm);
verifier.update(data);
if (signingAlgorithm.includes("PSS")) {
// For PSS padding
return verifier.verify(
{
key: publicKey.toString(),
padding,
saltLength
},
signature
);
}
// For PKCS1 v1.5 padding
return verifier.verify(
{
key: publicKey.toString()
},
signature
);
}
// For ECDSA signatures
if (signingAlgorithm.startsWith("ECDSA")) {
const verifier = crypto.createVerify(hashAlgorithm);
verifier.update(data);
return verifier.verify(
{
key: publicKey.toString(),
dsaEncoding: "ieee-p1363"
},
signature
);
}
throw new BadRequestError({
message: `Verification for algorithm ${signingAlgorithm} not implemented`
});
} catch (error) {
logger.error(error, "KMS: Failed to verify signature");
return false;
}
};
return {
sign,
verify,
generateAsymmetricPrivateKey,
getPublicKeyFromPrivateKey
};
};
+39
View File
@@ -0,0 +1,39 @@
import { z } from "zod";
export type TAsymmetricSignVerifyFns = {
sign: (data: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm) => Buffer;
verify: (data: Buffer, signature: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm) => boolean;
generateAsymmetricPrivateKey: () => Promise<Buffer>;
getPublicKeyFromPrivateKey: (privateKey: Buffer) => Buffer;
};
// Supported asymmetric key types
export enum AsymmetricKeySignVerify {
RSA_4096 = "rsa-4096",
ECC_NIST_P256 = "ecc-nist-p256"
}
export const AsymmetricKeySignVerifyEnum = z.enum(
Object.values(AsymmetricKeySignVerify) 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_256 = "RSASSA_PSS_SHA_256",
RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384",
RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512",
// 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_256 = "RSASSA_PKCS1_V1_5_SHA_256",
RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384",
RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512",
// ECDSA algorithms
// None of these are deterministic and include randomness like RSA PSS.
ECDSA_SHA_256 = "ECDSA_SHA_256",
ECDSA_SHA_384 = "ECDSA_SHA_384",
ECDSA_SHA_512 = "ECDSA_SHA_512"
}
+200 -16
View File
@@ -4,18 +4,22 @@ 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, SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
import { AsymmetricKeySignVerify, 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 { KmsKeyIntent } from "@app/services/kms/kms-types";
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
const keyDescriptionSchema = z.string().trim().max(500).optional();
const CmekSchema = KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).omit({
const CmekSchema = KmsKeysSchema.merge(
InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true, type: true })
).omit({
isReserved: true
});
@@ -45,16 +49,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),
type: z
.nativeEnum(KmsKeyIntent)
.optional()
.default(KmsKeyIntent.ENCRYPT_DECRYPT)
.describe(KMS.CREATE_KEY.type),
encryptionAlgorithm: z
.enum(AllowedEncryptionKeyAlgorithms)
.optional()
.default(SymmetricKeyEncryptDecrypt.AES_GCM_256)
.describe(KMS.CREATE_KEY.encryptionAlgorithm)
})
.superRefine((data, ctx) => {
if (
data.type === KmsKeyIntent.ENCRYPT_DECRYPT &&
!Object.values(SymmetricKeyEncryptDecrypt).includes(data.encryptionAlgorithm as SymmetricKeyEncryptDecrypt)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `encryptionAlgorithm must be a valid symmetric encryption algorithm. Valid options are: ${Object.values(
SymmetricKeyEncryptDecrypt
).join(", ")}`
});
}
if (
data.type === KmsKeyIntent.SIGN_VERIFY &&
!Object.values(AsymmetricKeySignVerify).includes(data.encryptionAlgorithm as AsymmetricKeySignVerify)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `encryptionAlgorithm must be a valid asymmetric sign-verify algorithm. Valid options are: ${Object.values(
AsymmetricKeySignVerify
).join(", ")}`
});
}
}),
response: {
200: z.object({
key: CmekSchema
@@ -64,12 +98,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, type },
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,
type
},
permission
);
@@ -82,7 +123,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
keyId: cmek.id,
name,
description,
encryptionAlgorithm
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm
}
}
});
@@ -366,6 +407,149 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/keys/:keyId/public-key",
config: {
rateLimit: readLimit
},
schema: {
description: "Get public key for a KMS key",
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 = await server.services.cmek.getPublicKey({ keyId }, permission);
return publicKey;
}
});
server.route({
method: "GET",
url: "/keys/:keyId/signing-algorithms",
config: {
rateLimit: readLimit
},
schema: {
description: "List 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))
})
}
},
handler: async (req) => {
const result = await server.services.cmek.listSigningAlgorithms(
{
keyId: req.params.keyId
},
req.permission
);
return result;
}
});
server.route({
method: "POST",
url: "/keys/:keyId/sign",
config: {
rateLimit: writeLimit
},
schema: {
description: "Sign data with KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.SIGN.keyId)
}),
body: z.object({
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
data: z
.string()
.superRefine((data, ctx) => {
if (!isBase64(data)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "data must be base64 encoded"
});
}
})
.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 },
permission
} = req;
const result = await server.services.cmek.cmekSign({ keyId: inputKeyId, data, signingAlgorithm }, permission);
return result;
}
});
server.route({
method: "POST",
url: "/keys/:keyId/verify",
config: {
rateLimit: writeLimit
},
schema: {
description: "Verify data with KMS key",
params: z.object({
keyId: z.string().uuid().describe(KMS.VERIFY.keyId)
}),
body: z.object({
data: z.string().describe(KMS.VERIFY.data),
signature: z.string().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 },
permission
} = req;
const result = await server.services.cmek.cmekVerify({ keyId, data, signature, signingAlgorithm }, permission);
return result;
}
});
server.route({
method: "POST",
url: "/keys/:keyId/decrypt",
+148 -1
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 { KmsKeyIntent } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
type TCmekServiceFactoryDep = {
@@ -224,6 +231,142 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
return cipherTextBlob.toString("base64");
};
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.type !== KmsKeyIntent.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 };
};
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 };
};
const cmekSign = async ({ keyId, data, signingAlgorithm }: 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 });
return {
signature: signature.toString("base64"),
keyId: key.id,
signingAlgorithm: algorithm
};
};
const cmekVerify = async ({ keyId, data, signature, signingAlgorithm }: 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({
data: Buffer.from(data, "base64"),
signature: Buffer.from(signature, "base64")
});
return {
signatureValid,
keyId: key.id,
signingAlgorithm: algorithm
};
};
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
const key = await kmsDAL.findById(keyId);
@@ -259,6 +402,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
cmekEncrypt,
cmekDecrypt,
findCmekById,
findCmekByName
findCmekByName,
cmekSign,
cmekVerify,
listSigningAlgorithms,
getPublicKey
};
};
+29 -2
View File
@@ -1,12 +1,18 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
import { AsymmetricKeySignVerify, SigningAlgorithm } from "@app/lib/crypto/sign";
import { OrderByDirection } from "@app/lib/types";
import { KmsKeyIntent } from "../kms/kms-types";
export type TCmekKeyEncryptionAlgorithm = SymmetricKeyEncryptDecrypt | AsymmetricKeySignVerify;
export type TCreateCmekDTO = {
orgId: string;
projectId: string;
name: string;
description?: string;
encryptionAlgorithm: SymmetricEncryption;
encryptionAlgorithm: TCmekKeyEncryptionAlgorithm;
type: KmsKeyIntent;
};
export type TUpdabteCmekByIdDTO = {
@@ -38,3 +44,24 @@ 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;
};
export type TCmekVerifyDTO = {
keyId: string;
data: string;
signature: string;
signingAlgorithm: SigningAlgorithm;
};
+46 -4
View File
@@ -1,13 +1,55 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
import { AsymmetricKeySignVerify } from "@app/lib/crypto/sign";
import { BadRequestError } from "@app/lib/errors";
import { KmsKeyIntent } from "./kms-types";
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
export const getByteLengthForSymmetricEncryptionAlgorithm = (encryptionAlgorithm: SymmetricKeyEncryptDecrypt) => {
switch (encryptionAlgorithm) {
case SymmetricEncryption.AES_GCM_128:
case SymmetricKeyEncryptDecrypt.AES_GCM_128:
return 16;
case SymmetricEncryption.AES_GCM_256:
case SymmetricKeyEncryptDecrypt.AES_GCM_256:
default:
return 32;
}
};
export const verifyKeyTypeAndAlgorithm = (
type: KmsKeyIntent,
algorithm: SymmetricKeyEncryptDecrypt | AsymmetricKeySignVerify,
extra?: {
forceType?: KmsKeyIntent;
}
) => {
if (extra?.forceType && type !== extra.forceType) {
throw new BadRequestError({
message: `Unsupported key type, expected ${extra.forceType} but got ${type}`
});
}
if (type === KmsKeyIntent.ENCRYPT_DECRYPT) {
if (!Object.values(SymmetricKeyEncryptDecrypt).includes(algorithm as SymmetricKeyEncryptDecrypt)) {
throw new BadRequestError({
message: `Unsupported encryption algorithm for encrypt/decrypt key: ${algorithm as string}`
});
}
return true;
}
if (type === KmsKeyIntent.SIGN_VERIFY) {
if (!Object.values(AsymmetricKeySignVerify).includes(algorithm as AsymmetricKeySignVerify)) {
throw new BadRequestError({
message: `Unsupported sign/verify algorithm for sign/verify key: ${algorithm as string}`
});
}
return true;
}
throw new BadRequestError({
message: `Unsupported key type: ${type as string}`
});
};
+147 -22
View File
@@ -1,3 +1,5 @@
import crypto from "node:crypto";
import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { z } from "zod";
@@ -15,12 +17,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, SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
import { generateHash } from "@app/lib/crypto/encryption";
import { AsymmetricKeySignVerify, 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 +36,7 @@ import { TKmsKeyDALFactory } from "./kms-key-dal";
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
import {
KmsDataKey,
KmsKeyIntent,
KmsType,
RootKeyEncryptionStrategy,
TDecryptWithKeyDTO,
@@ -38,8 +46,11 @@ import {
TEncryptWithKmsDTO,
TGenerateKMSDTO,
TGetKeyMaterialDTO,
TGetPublicKeyDTO,
TImportKeyMaterialDTO,
TUpdateProjectSecretManagerKmsKeyDTO
TSignWithKmsDTO,
TUpdateProjectSecretManagerKmsKeyDTO,
TVerifyWithKmsDTO
} from "./kms-types";
type TKmsServiceFactoryDep = {
@@ -83,19 +94,42 @@ export const kmsServiceFactory = ({
tx,
name,
projectId,
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
encryptionAlgorithm = SymmetricKeyEncryptDecrypt.AES_GCM_256,
type = KmsKeyIntent.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(type, encryptionAlgorithm);
const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm));
let kmsKeyMaterial: Buffer | null = null;
if (type === KmsKeyIntent.ENCRYPT_DECRYPT) {
kmsKeyMaterial = randomSecureBytes(
getByteLengthForSymmetricEncryptionAlgorithm(encryptionAlgorithm as SymmetricKeyEncryptDecrypt)
);
} else if (type === KmsKeyIntent.SIGN_VERIFY) {
const { generateAsymmetricPrivateKey, getPublicKeyFromPrivateKey } = signingService(
encryptionAlgorithm as AsymmetricKeySignVerify
);
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 type '${type}' using algorithm '${encryptionAlgorithm}'`
});
}
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.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,
type,
orgId,
isReserved,
projectId,
@@ -115,6 +149,7 @@ export const kmsServiceFactory = ({
);
return kmsDoc;
};
if (tx) return dbQuery(tx);
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
return doc;
@@ -134,7 +169,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(SymmetricKeyEncryptDecrypt.AES_GCM_256);
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
// Buffer#1 encrypted text + Buffer#2 version number
@@ -149,7 +184,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(SymmetricKeyEncryptDecrypt.AES_GCM_256);
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
@@ -227,7 +262,7 @@ export const kmsServiceFactory = ({
};
const encryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
return (plainTextBuffer: Buffer) => {
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
@@ -236,7 +271,7 @@ export const kmsServiceFactory = ({
};
const decryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
return (cipherTextBuffer: Buffer) => {
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
@@ -255,6 +290,11 @@ export const kmsServiceFactory = ({
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
}
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyEncryptDecrypt;
verifyKeyTypeAndAlgorithm(kmsDoc.type as KmsKeyIntent, encryptionAlgorithm, {
forceType: KmsKeyIntent.ENCRYPT_DECRYPT
});
if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns;
@@ -316,8 +356,8 @@ export const kmsServiceFactory = ({
}
// internal KMS
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.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 +385,22 @@ export const kmsServiceFactory = ({
});
}
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.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, type }: TImportKeyMaterialDTO,
tx?: Knex
) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
// daniel: currently we only support imports for encrypt/decrypt keys
verifyKeyTypeAndAlgorithm(type, algorithm, { forceType: KmsKeyIntent.ENCRYPT_DECRYPT });
const expectedByteLength = getByteLengthForAlgorithm(algorithm);
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
const expectedByteLength = getByteLengthForSymmetricEncryptionAlgorithm(algorithm as SymmetricKeyEncryptDecrypt);
if (key.byteLength !== expectedByteLength) {
throw new BadRequestError({
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
@@ -370,6 +413,7 @@ export const kmsServiceFactory = ({
const kmsDoc = await kmsDAL.create(
{
name: sanitizedName,
type: KmsKeyIntent.ENCRYPT_DECRYPT,
orgId,
isReserved,
projectId
@@ -393,12 +437,89 @@ 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 AsymmetricKeySignVerify;
verifyKeyTypeAndAlgorithm(kmsDoc.type as KmsKeyIntent, encryptionAlgorithm, {
forceType: KmsKeyIntent.SIGN_VERIFY
});
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
const publicKeyBuffer = signingService(encryptionAlgorithm).getPublicKeyFromPrivateKey(kmsKey);
return crypto
.createPublicKey({
key: publicKeyBuffer,
format: "pem",
type: "spki"
})
.export({ type: "spki", format: "pem" }) as string; // format 'pem' makes it a string
};
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 AsymmetricKeySignVerify;
verifyKeyTypeAndAlgorithm(kmsDoc.type as KmsKeyIntent, encryptionAlgorithm, {
forceType: KmsKeyIntent.SIGN_VERIFY
});
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
const { sign } = signingService(encryptionAlgorithm);
return ({ data, signingAlgorithm }: Pick<TSignWithKmsDTO, "data" | "signingAlgorithm">) => {
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
const signature = sign(data, kmsKey, signingAlgorithm);
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 AsymmetricKeySignVerify;
verifyKeyTypeAndAlgorithm(kmsDoc.type as KmsKeyIntent, encryptionAlgorithm, {
forceType: KmsKeyIntent.SIGN_VERIFY
});
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
const { verify, getPublicKeyFromPrivateKey } = signingService(encryptionAlgorithm);
return ({ data, signature }: Pick<TVerifyWithKmsDTO, "data" | "signature">) => {
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
const publicKey = getPublicKeyFromPrivateKey(kmsKey);
const signatureValid = verify(data, signature, publicKey, signingAlgorithm);
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 SymmetricKeyEncryptDecrypt;
verifyKeyTypeAndAlgorithm(kmsDoc.type as KmsKeyIntent, encryptionAlgorithm, {
forceType: KmsKeyIntent.ENCRYPT_DECRYPT
});
if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns;
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
@@ -454,8 +575,8 @@ export const kmsServiceFactory = ({
}
// internal KMS
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.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 +850,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(SymmetricKeyEncryptDecrypt.AES_GCM_256);
const encryptionKeyBuffer = $getBasicEncryptionKey();
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
@@ -749,7 +870,7 @@ export const kmsServiceFactory = ({
}
if (strategy === RootKeyEncryptionStrategy.Software) {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
const encryptionKeyBuffer = $getBasicEncryptionKey();
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
@@ -765,7 +886,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(SymmetricKeyEncryptDecrypt.AES_GCM_256);
return {
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
@@ -966,6 +1087,7 @@ export const kmsServiceFactory = ({
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
logger.info("KMS: Loading ROOT Key into Memory.");
ROOT_ENCRYPTION_KEY = decryptedRootKey;
};
@@ -1014,6 +1136,9 @@ export const kmsServiceFactory = ({
getKmsById,
createCipherPairWithDataKey,
getKeyMaterial,
importKeyMaterial
importKeyMaterial,
signWithKmsKey,
verifyWithKmsKey,
getPublicKey
};
};
+28 -3
View File
@@ -1,6 +1,7 @@
import { Knex } from "knex";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
import { AsymmetricKeySignVerify, SigningAlgorithm } from "@app/lib/crypto/sign/types";
export enum KmsDataKey {
Organization,
@@ -13,6 +14,11 @@ export enum KmsType {
Internal = "internal"
}
export enum KmsKeyIntent {
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?: SymmetricKeyEncryptDecrypt | AsymmetricKeySignVerify;
type?: KmsKeyIntent;
isReserved?: boolean;
name?: string;
description?: string;
@@ -37,6 +44,23 @@ export type TEncryptWithKmsDTO = {
plainText: Buffer;
};
export type TGetPublicKeyDTO = {
kmsId: string;
};
export type TSignWithKmsDTO = {
kmsId: string;
data: Buffer;
signingAlgorithm: SigningAlgorithm;
};
export type TVerifyWithKmsDTO = {
kmsId: string;
data: Buffer;
signature: Buffer;
signingAlgorithm: SigningAlgorithm;
};
export type TEncryptionWithKeyDTO = {
key: Buffer;
plainText: Buffer;
@@ -67,9 +91,10 @@ export type TGetKeyMaterialDTO = {
export type TImportKeyMaterialDTO = {
key: Buffer;
algorithm: SymmetricEncryption;
algorithm: SymmetricKeyEncryptDecrypt;
name?: string;
isReserved: boolean;
projectId: string;
orgId: string;
type: KmsKeyIntent;
};