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
+2 -1
View File
@@ -8,7 +8,8 @@ RUN apt-get update && apt-get install -y \
python3 \ python3 \
make \ make \
g++ \ g++ \
openssh-client openssh-client \
openssl
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets) # Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apt-get install -y \ RUN apt-get install -y \
+1
View File
@@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y \
make \ make \
g++ \ g++ \
openssh-client \ openssh-client \
openssl \
curl \ curl \
pkg-config pkg-config
+9 -13
View File
@@ -132,7 +132,7 @@
"@types/jsrp": "^0.2.6", "@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13", "@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.9.5", "@types/node": "^20.17.30",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12", "@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
@@ -9753,11 +9753,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.9.5", "version": "20.17.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
"integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==", "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
"license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~6.19.2"
} }
}, },
"node_modules/@types/node-fetch": { "node_modules/@types/node-fetch": {
@@ -20081,11 +20082,6 @@
"undici-types": "~6.19.2" "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": { "node_modules/scim2-parse-filter": {
"version": "0.2.10", "version": "0.2.10",
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz", "resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
@@ -22442,9 +22438,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "5.26.5", "version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
}, },
"node_modules/unicode-canonical-property-names-ecmascript": { "node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0", "version": "2.0.0",
+1 -1
View File
@@ -89,7 +89,7 @@
"@types/jsrp": "^0.2.6", "@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13", "@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.9.5", "@types/node": "^20.17.30",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12", "@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
@@ -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");
});
}
+2 -1
View File
@@ -16,7 +16,8 @@ export const KmsKeysSchema = z.object({
name: z.string(), name: z.string(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: 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>; export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
+3 -3
View File
@@ -2,7 +2,7 @@ import z from "zod";
import { KmsKeysSchema } from "@app/db/schemas"; import { KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; 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 { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -74,7 +74,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "KMIP endpoint for creating managed objects", description: "KMIP endpoint for creating managed objects",
body: z.object({ body: z.object({
algorithm: z.nativeEnum(SymmetricEncryption) algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
}), }),
response: { response: {
200: KmsKeysSchema 200: KmsKeysSchema
@@ -433,7 +433,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
key: z.string(), key: z.string(),
name: z.string(), name: z.string(),
algorithm: z.nativeEnum(SymmetricEncryption) algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -12,7 +12,8 @@ import {
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types"; import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-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 { 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 { TProjectPermission } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types"; import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
@@ -255,6 +256,11 @@ export enum EventType {
GET_CMEK = "get-cmek", GET_CMEK = "get-cmek",
CMEK_ENCRYPT = "cmek-encrypt", CMEK_ENCRYPT = "cmek-encrypt",
CMEK_DECRYPT = "cmek-decrypt", 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", 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_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
GET_PROJECT_TEMPLATES = "get-project-templates", GET_PROJECT_TEMPLATES = "get-project-templates",
@@ -1997,7 +2003,7 @@ interface CreateCmekEvent {
keyId: string; keyId: string;
name: string; name: string;
description?: 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 { interface GetExternalGroupOrgRoleMappingsEvent {
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS; type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
metadata?: Record<string, never>; // not needed, based off orgId metadata?: Record<string, never>; // not needed, based off orgId
@@ -2639,6 +2678,10 @@ export type Event =
| GetCmeksEvent | GetCmeksEvent
| CmekEncryptEvent | CmekEncryptEvent
| CmekDecryptEvent | CmekDecryptEvent
| CmekSignEvent
| CmekVerifyEvent
| CmekListSigningAlgorithmsEvent
| CmekGetPublicKeyEvent
| GetExternalGroupOrgRoleMappingsEvent | GetExternalGroupOrgRoleMappingsEvent
| UpdateExternalGroupOrgRoleMappingsEvent | UpdateExternalGroupOrgRoleMappingsEvent
| GetProjectTemplatesEvent | GetProjectTemplatesEvent
@@ -7,7 +7,7 @@ import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/er
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; 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 { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -115,6 +115,7 @@ export const externalKmsServiceFactory = ({
{ {
isReserved: false, isReserved: false,
description, description,
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
name: kmsName, name: kmsName,
orgId: actorOrgId orgId: actorOrgId
}, },
@@ -92,7 +92,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
plaintext: data plaintext: data
}); });
if (!encryptedText[0].ciphertext) throw new Error("encryption failed"); 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) => { const decrypt = async (encryptedBlob: Buffer) => {
@@ -101,7 +101,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
ciphertext: encryptedBlob ciphertext: encryptedBlob
}); });
if (!decryptedText[0].plaintext) throw new Error("decryption failed"); 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 { return {
+3 -3
View File
@@ -258,7 +258,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
const decrypt: { const decrypt: {
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>; (encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
(encryptedBlob: Buffer): Promise<Buffer>; (encryptedBlob: Buffer): Promise<Buffer>;
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => { } = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle): Promise<Buffer> => {
if (!pkcs11 || !isInitialized) { if (!pkcs11 || !isInitialized) {
throw new Error("PKCS#11 module is not initialized"); 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); 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); const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
// Create a new buffer from the decrypted data
return Buffer.from(decryptedData); return Buffer.from(decryptedData);
} catch (error) { } catch (error) {
logger.error(error, "HSM: Failed to perform decryption"); 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 { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; 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 { TProjectDALFactory } from "@app/services/project/project-dal";
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -403,6 +404,7 @@ export const kmipOperationServiceFactory = ({
algorithm, algorithm,
isReserved: false, isReserved: false,
projectId, projectId,
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
orgId: project.orgId orgId: project.orgId
}); });
+3 -3
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 { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
@@ -49,7 +49,7 @@ type KmipOperationBaseDTO = {
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
export type TKmipCreateDTO = { export type TKmipCreateDTO = {
algorithm: SymmetricEncryption; algorithm: SymmetricKeyAlgorithm;
} & KmipOperationBaseDTO; } & KmipOperationBaseDTO;
export type TKmipGetDTO = { export type TKmipGetDTO = {
@@ -77,7 +77,7 @@ export type TKmipLocateDTO = KmipOperationBaseDTO;
export type TKmipRegisterDTO = { export type TKmipRegisterDTO = {
name: string; name: string;
key: string; key: string;
algorithm: SymmetricEncryption; algorithm: SymmetricKeyAlgorithm;
} & KmipOperationBaseDTO; } & KmipOperationBaseDTO;
export type TSetupOrgKmipDTO = { export type TSetupOrgKmipDTO = {
@@ -32,7 +32,9 @@ export enum ProjectPermissionCmekActions {
Edit = "edit", Edit = "edit",
Delete = "delete", Delete = "delete",
Encrypt = "encrypt", Encrypt = "encrypt",
Decrypt = "decrypt" Decrypt = "decrypt",
Sign = "sign",
Verify = "verify"
} }
export enum ProjectPermissionDynamicSecretActions { export enum ProjectPermissionDynamicSecretActions {
@@ -732,7 +734,9 @@ const buildAdminPermissionRules = () => {
ProjectPermissionCmekActions.Delete, ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read, ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt, ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
], ],
ProjectPermissionSub.Cmek ProjectPermissionSub.Cmek
); );
@@ -935,7 +939,9 @@ const buildMemberPermissionRules = () => {
ProjectPermissionCmekActions.Delete, ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read, ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt, ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
], ],
ProjectPermissionSub.Cmek ProjectPermissionSub.Cmek
); );
+24 -1
View File
@@ -1672,7 +1672,8 @@ export const KMS = {
projectId: "The ID of the project to create the key in.", projectId: "The ID of the project to create the key in.",
name: "The name of the key to be created. Must be slug-friendly.", name: "The name of the key to be created. Must be slug-friendly.",
description: "An optional description of the key.", 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: { UPDATE_KEY: {
keyId: "The ID of the key to be updated.", keyId: "The ID of the key to be updated.",
@@ -1705,6 +1706,28 @@ export const KMS = {
DECRYPT: { DECRYPT: {
keyId: "The ID of the key to decrypt the data with.", keyId: "The ID of the key to decrypt the data with.",
ciphertext: "The ciphertext to be decrypted (base64 encoded)." 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."
} }
}; };
+4 -2
View File
@@ -1,6 +1,6 @@
import crypto from "crypto"; import crypto from "crypto";
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types"; import { SymmetricKeyAlgorithm, TSymmetricEncryptionFns } from "./types";
const getIvLength = () => { const getIvLength = () => {
return 12; return 12;
@@ -10,7 +10,9 @@ const getTagLength = () => {
return 16; 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 IV_LENGTH = getIvLength();
const TAG_LENGTH = getTagLength(); const TAG_LENGTH = getTagLength();
+1 -1
View File
@@ -1,2 +1,2 @@
export { symmetricCipherService } from "./cipher"; export { symmetricCipherService } from "./cipher";
export { SymmetricEncryption } from "./types"; export { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "./types";
+12 -1
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_256 = "aes-256-gcm",
AES_GCM_128 = "aes-128-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 = { export type TSymmetricEncryptionFns = {
encrypt: (text: Buffer, key: Buffer) => Buffer; encrypt: (text: Buffer, key: Buffer) => Buffer;
+2
View File
@@ -0,0 +1,2 @@
export { signingService } from "./signing";
export { AsymmetricKeyAlgorithm, SigningAlgorithm } from "./types";
+539
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
};
};
+45
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"
}
+35
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;
});
};
+1
View File
@@ -0,0 +1 @@
export * from "./files";
+5
View File
@@ -9,6 +9,7 @@ import { runMigrations } from "./auto-start-migrations";
import { initAuditLogDbConnection, initDbConnection } from "./db"; import { initAuditLogDbConnection, initDbConnection } from "./db";
import { keyStoreFactory } from "./keystore/keystore"; import { keyStoreFactory } from "./keystore/keystore";
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
import { removeTemporaryBaseDirectory } from "./lib/files";
import { initLogger } from "./lib/logger"; import { initLogger } from "./lib/logger";
import { queueServiceFactory } from "./queue"; import { queueServiceFactory } from "./queue";
import { main } from "./server/app"; import { main } from "./server/app";
@@ -21,6 +22,8 @@ const run = async () => {
const logger = initLogger(); const logger = initLogger();
const envConfig = initEnvConfig(logger); const envConfig = initEnvConfig(logger);
await removeTemporaryBaseDirectory();
const db = initDbConnection({ const db = initDbConnection({
dbConnectionUri: envConfig.DB_CONNECTION_URI, dbConnectionUri: envConfig.DB_CONNECTION_URI,
dbRootCert: envConfig.DB_ROOT_CERT, dbRootCert: envConfig.DB_ROOT_CERT,
@@ -71,6 +74,7 @@ const run = async () => {
process.on("SIGINT", async () => { process.on("SIGINT", async () => {
await server.close(); await server.close();
await db.destroy(); await db.destroy();
await removeTemporaryBaseDirectory();
hsmModule.finalize(); hsmModule.finalize();
process.exit(0); process.exit(0);
}); });
@@ -79,6 +83,7 @@ const run = async () => {
process.on("SIGTERM", async () => { process.on("SIGTERM", async () => {
await server.close(); await server.close();
await db.destroy(); await db.destroy();
await removeTemporaryBaseDirectory();
hsmModule.finalize(); hsmModule.finalize();
process.exit(0); process.exit(0);
}); });
+253 -22
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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { KMS } from "@app/lib/api-docs"; import { KMS } from "@app/lib/api-docs";
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64"; 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 { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas"; import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; 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 keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
const keyDescriptionSchema = z.string().trim().max(500).optional(); const keyDescriptionSchema = z.string().trim().max(500).optional();
@@ -45,16 +47,46 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
description: "Create KMS key", description: "Create KMS key",
body: z.object({ body: z
projectId: z.string().describe(KMS.CREATE_KEY.projectId), .object({
name: keyNameSchema.describe(KMS.CREATE_KEY.name), projectId: z.string().describe(KMS.CREATE_KEY.projectId),
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description), name: keyNameSchema.describe(KMS.CREATE_KEY.name),
encryptionAlgorithm: z description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
.nativeEnum(SymmetricEncryption) keyUsage: z
.optional() .nativeEnum(KmsKeyUsage)
.default(SymmetricEncryption.AES_GCM_256) .optional()
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others .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: { response: {
200: z.object({ 200: z.object({
key: CmekSchema key: CmekSchema
@@ -64,12 +96,19 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { const {
body: { projectId, name, description, encryptionAlgorithm }, body: { projectId, name, description, encryptionAlgorithm, keyUsage },
permission permission
} = req; } = req;
const cmek = await server.services.cmek.createCmek( 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 permission
); );
@@ -82,7 +121,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
keyId: cmek.id, keyId: cmek.id,
name, name,
description, description,
encryptionAlgorithm encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm
} }
} }
}); });
@@ -126,7 +165,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: permission.orgId, projectId: cmek.projectId!,
event: { event: {
type: EventType.UPDATE_CMEK, type: EventType.UPDATE_CMEK,
metadata: { metadata: {
@@ -169,7 +208,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: permission.orgId, projectId: cmek.projectId!,
event: { event: {
type: EventType.DELETE_CMEK, type: EventType.DELETE_CMEK,
metadata: { metadata: {
@@ -282,7 +321,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
description: "Get KMS key by Name", description: "Get KMS key by name",
params: z.object({ params: z.object({
keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName) keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName)
}), }),
@@ -349,11 +388,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
permission permission
} = req; } = 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: permission.orgId, projectId,
event: { event: {
type: EventType.CMEK_ENCRYPT, type: EventType.CMEK_ENCRYPT,
metadata: { 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({ server.route({
method: "POST", method: "POST",
url: "/keys/:keyId/decrypt", url: "/keys/:keyId/decrypt",
@@ -394,11 +625,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
permission permission
} = req; } = 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: permission.orgId, projectId,
event: { event: {
type: EventType.CMEK_DECRYPT, type: EventType.CMEK_DECRYPT,
metadata: { metadata: {
+161 -3
View File
@@ -3,12 +3,18 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { ActionProjectType, ProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; 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 { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { import {
TCmekDecryptDTO, TCmekDecryptDTO,
TCmekEncryptDTO, TCmekEncryptDTO,
TCmekGetPublicKeyDTO,
TCmekKeyEncryptionAlgorithm,
TCmekListSigningAlgorithmsDTO,
TCmekSignDTO,
TCmekVerifyDTO,
TCreateCmekDTO, TCreateCmekDTO,
TListCmeksByProjectIdDTO, TListCmeksByProjectIdDTO,
TUpdabteCmekByIdDTO TUpdabteCmekByIdDTO
@@ -16,6 +22,7 @@ import {
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsKeyUsage } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
type TCmekServiceFactoryDep = { type TCmekServiceFactoryDep = {
@@ -221,7 +228,151 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") }); 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) => { 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") }); const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") });
return plaintextBlob.toString("base64"); return {
plaintext: plaintextBlob.toString("base64"),
projectId: key.projectId
};
}; };
return { return {
@@ -259,6 +413,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
cmekEncrypt, cmekEncrypt,
cmekDecrypt, cmekDecrypt,
findCmekById, findCmekById,
findCmekByName findCmekByName,
cmekSign,
cmekVerify,
listSigningAlgorithms,
getPublicKey
}; };
}; };
+31 -2
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 { OrderByDirection } from "@app/lib/types";
import { KmsKeyUsage } from "../kms/kms-types";
export type TCmekKeyEncryptionAlgorithm = SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
export type TCreateCmekDTO = { export type TCreateCmekDTO = {
orgId: string; orgId: string;
projectId: string; projectId: string;
name: string; name: string;
description?: string; description?: string;
encryptionAlgorithm: SymmetricEncryption; encryptionAlgorithm: TCmekKeyEncryptionAlgorithm;
keyUsage: KmsKeyUsage;
}; };
export type TUpdabteCmekByIdDTO = { export type TUpdabteCmekByIdDTO = {
@@ -38,3 +44,26 @@ export type TCmekDecryptDTO = {
export enum CmekOrderBy { export enum CmekOrderBy {
Name = "name" 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;
};
+46 -4
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 KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => { export const getByteLengthForSymmetricEncryptionAlgorithm = (encryptionAlgorithm: SymmetricKeyAlgorithm) => {
switch (encryptionAlgorithm) { switch (encryptionAlgorithm) {
case SymmetricEncryption.AES_GCM_128: case SymmetricKeyAlgorithm.AES_GCM_128:
return 16; return 16;
case SymmetricEncryption.AES_GCM_256: case SymmetricKeyAlgorithm.AES_GCM_256:
default: default:
return 32; 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}`
});
};
+141 -22
View File
@@ -15,12 +15,17 @@ import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { TEnvConfig } from "@app/lib/config/env"; import { TEnvConfig } from "@app/lib/config/env";
import { randomSecureBytes } from "@app/lib/crypto"; 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 { generateHash } from "@app/lib/crypto/encryption";
import { AsymmetricKeyAlgorithm, signingService } from "@app/lib/crypto/sign";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; 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 { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-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 { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
import { import {
KmsDataKey, KmsDataKey,
KmsKeyUsage,
KmsType, KmsType,
RootKeyEncryptionStrategy, RootKeyEncryptionStrategy,
TDecryptWithKeyDTO, TDecryptWithKeyDTO,
@@ -38,8 +44,11 @@ import {
TEncryptWithKmsDTO, TEncryptWithKmsDTO,
TGenerateKMSDTO, TGenerateKMSDTO,
TGetKeyMaterialDTO, TGetKeyMaterialDTO,
TGetPublicKeyDTO,
TImportKeyMaterialDTO, TImportKeyMaterialDTO,
TUpdateProjectSecretManagerKmsKeyDTO TSignWithKmsDTO,
TUpdateProjectSecretManagerKmsKeyDTO,
TVerifyWithKmsDTO
} from "./kms-types"; } from "./kms-types";
type TKmsServiceFactoryDep = { type TKmsServiceFactoryDep = {
@@ -83,19 +92,42 @@ export const kmsServiceFactory = ({
tx, tx,
name, name,
projectId, projectId,
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256, encryptionAlgorithm = SymmetricKeyAlgorithm.AES_GCM_256,
keyUsage = KmsKeyUsage.ENCRYPT_DECRYPT,
description description
}: TGenerateKMSDTO) => { }: 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 encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
const dbQuery = async (db: Knex) => { const dbQuery = async (db: Knex) => {
const kmsDoc = await kmsDAL.create( const kmsDoc = await kmsDAL.create(
{ {
name: sanitizedName, name: sanitizedName,
keyUsage,
orgId, orgId,
isReserved, isReserved,
projectId, projectId,
@@ -115,6 +147,7 @@ export const kmsServiceFactory = ({
); );
return kmsDoc; return kmsDoc;
}; };
if (tx) return dbQuery(tx); if (tx) return dbQuery(tx);
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2)); const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
return doc; return doc;
@@ -134,7 +167,7 @@ export const kmsServiceFactory = ({
*/ */
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => { const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm // 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">) => { return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const encryptedPlainTextBlob = cipher.encrypt(plainText, key); const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
// Buffer#1 encrypted text + Buffer#2 version number // 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 * This can be even later exposed directly as api for encryption as function
*/ */
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => { 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">) => { return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
@@ -227,7 +260,7 @@ export const kmsServiceFactory = ({
}; };
const encryptWithRootKey = () => { const encryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
return (plainTextBuffer: Buffer) => { return (plainTextBuffer: Buffer) => {
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY); const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
@@ -236,7 +269,7 @@ export const kmsServiceFactory = ({
}; };
const decryptWithRootKey = () => { const decryptWithRootKey = () => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
return (cipherTextBuffer: Buffer) => { return (cipherTextBuffer: Buffer) => {
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY); return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
@@ -255,6 +288,11 @@ export const kmsServiceFactory = ({
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); 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) { if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns; let externalKms: TExternalKmsProviderFns;
@@ -316,8 +354,8 @@ export const kmsServiceFactory = ({
} }
// internal KMS // internal KMS
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption); const dataCipher = symmetricCipherService(encryptionAlgorithm);
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => { 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); const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
return kmsKey; return kmsKey;
}; };
const importKeyMaterial = async ( const importKeyMaterial = async (
{ key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO, { key, algorithm, name, isReserved, projectId, orgId, keyUsage }: TImportKeyMaterialDTO,
tx?: Knex 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) { if (key.byteLength !== expectedByteLength) {
throw new BadRequestError({ throw new BadRequestError({
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes` 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( const kmsDoc = await kmsDAL.create(
{ {
name: sanitizedName, name: sanitizedName,
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
orgId, orgId,
isReserved, isReserved,
projectId projectId
@@ -393,12 +435,85 @@ export const kmsServiceFactory = ({
return doc; 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 encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx); const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
if (!kmsDoc) { if (!kmsDoc) {
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` }); 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) { if (kmsDoc.externalKms) {
let externalKms: TExternalKmsProviderFns; let externalKms: TExternalKmsProviderFns;
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) { if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
@@ -454,8 +569,8 @@ export const kmsServiceFactory = ({
} }
// internal KMS // internal KMS
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption); const dataCipher = symmetricCipherService(encryptionAlgorithm);
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => { return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey); const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
@@ -729,7 +844,7 @@ export const kmsServiceFactory = ({
// case 2: root key is encrypted with software encryption // case 2: root key is encrypted with software encryption
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) { if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const encryptionKeyBuffer = $getBasicEncryptionKey(); const encryptionKeyBuffer = $getBasicEncryptionKey();
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer); return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
@@ -749,7 +864,7 @@ export const kmsServiceFactory = ({
} }
if (strategy === RootKeyEncryptionStrategy.Software) { if (strategy === RootKeyEncryptionStrategy.Software) {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const encryptionKeyBuffer = $getBasicEncryptionKey(); const encryptionKeyBuffer = $getBasicEncryptionKey();
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer); return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
@@ -765,7 +880,7 @@ export const kmsServiceFactory = ({
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => { const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
const dataKey = await $getDataKey(encryptionContext, trx); const dataKey = await $getDataKey(encryptionContext, trx);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
return { return {
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => { encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
@@ -966,6 +1081,7 @@ export const kmsServiceFactory = ({
const decryptedRootKey = await $decryptRootKey(kmsRootConfig); const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
logger.info("KMS: Loading ROOT Key into Memory."); logger.info("KMS: Loading ROOT Key into Memory.");
ROOT_ENCRYPTION_KEY = decryptedRootKey; ROOT_ENCRYPTION_KEY = decryptedRootKey;
}; };
@@ -1014,6 +1130,9 @@ export const kmsServiceFactory = ({
getKmsById, getKmsById,
createCipherPairWithDataKey, createCipherPairWithDataKey,
getKeyMaterial, getKeyMaterial,
importKeyMaterial importKeyMaterial,
signWithKmsKey,
verifyWithKmsKey,
getPublicKey
}; };
}; };
+30 -3
View File
@@ -1,6 +1,7 @@
import { Knex } from "knex"; 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 { export enum KmsDataKey {
Organization, Organization,
@@ -13,6 +14,11 @@ export enum KmsType {
Internal = "internal" Internal = "internal"
} }
export enum KmsKeyUsage {
ENCRYPT_DECRYPT = "encrypt-decrypt",
SIGN_VERIFY = "sign-verify"
}
export type TEncryptWithKmsDataKeyDTO = export type TEncryptWithKmsDataKeyDTO =
| { type: KmsDataKey.Organization; orgId: string } | { type: KmsDataKey.Organization; orgId: string }
| { type: KmsDataKey.SecretManager; projectId: string }; | { type: KmsDataKey.SecretManager; projectId: string };
@@ -25,7 +31,8 @@ export type TEncryptWithKmsDataKeyDTO =
export type TGenerateKMSDTO = { export type TGenerateKMSDTO = {
orgId: string; orgId: string;
projectId?: string; projectId?: string;
encryptionAlgorithm?: SymmetricEncryption; encryptionAlgorithm?: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
keyUsage?: KmsKeyUsage;
isReserved?: boolean; isReserved?: boolean;
name?: string; name?: string;
description?: string; description?: string;
@@ -37,6 +44,25 @@ export type TEncryptWithKmsDTO = {
plainText: Buffer; 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 = { export type TEncryptionWithKeyDTO = {
key: Buffer; key: Buffer;
plainText: Buffer; plainText: Buffer;
@@ -67,9 +93,10 @@ export type TGetKeyMaterialDTO = {
export type TImportKeyMaterialDTO = { export type TImportKeyMaterialDTO = {
key: Buffer; key: Buffer;
algorithm: SymmetricEncryption; algorithm: SymmetricKeyAlgorithm;
name?: string; name?: string;
isReserved: boolean; isReserved: boolean;
projectId: string; projectId: string;
orgId: string; orgId: string;
keyUsage: KmsKeyUsage;
}; };
@@ -0,0 +1,4 @@
---
title: "Retrieve Public Key"
openapi: "GET /api/v1/kms/keys/{keyId}/public-key"
---
@@ -0,0 +1,4 @@
---
title: "Sign Data"
openapi: "POST /api/v1/kms/keys/{keyId}/sign"
---
@@ -0,0 +1,4 @@
---
title: "List Signing Algorithms"
openapi: "GET /api/v1/kms/keys/{keyId}/signing-algorithms"
---
@@ -0,0 +1,4 @@
---
title: "Verify Signature"
openapi: "POST /api/v1/kms/keys/{keyId}/verify"
---
+17 -3
View File
@@ -1319,9 +1319,23 @@
"api-reference/endpoints/kms/keys/get-by-name", "api-reference/endpoints/kms/keys/get-by-name",
"api-reference/endpoints/kms/keys/create", "api-reference/endpoints/kms/keys/create",
"api-reference/endpoints/kms/keys/update", "api-reference/endpoints/kms/keys/update",
"api-reference/endpoints/kms/keys/delete", "api-reference/endpoints/kms/keys/delete"
"api-reference/endpoints/kms/keys/encrypt", ]
"api-reference/endpoints/kms/keys/decrypt" },
{
"group": "Encryption",
"pages": [
"api-reference/endpoints/kms/encryption/encrypt",
"api-reference/endpoints/kms/encryption/decrypt"
]
},
{
"group": "Signing",
"pages": [
"api-reference/endpoints/kms/signing/sign",
"api-reference/endpoints/kms/signing/verify",
"api-reference/endpoints/kms/signing/public-key",
"api-reference/endpoints/kms/signing/signing-algorithms"
] ]
} }
] ]
@@ -30,7 +30,9 @@ export enum ProjectPermissionCmekActions {
Edit = "edit", Edit = "edit",
Delete = "delete", Delete = "delete",
Encrypt = "encrypt", Encrypt = "encrypt",
Decrypt = "decrypt" Decrypt = "decrypt",
Sign = "sign",
Verify = "verify"
} }
export enum ProjectPermissionKmipActions { export enum ProjectPermissionKmipActions {
+27
View File
@@ -0,0 +1,27 @@
import { AsymmetricKeyAlgorithm, KmsKeyUsage, SymmetricKeyAlgorithm } from "@app/hooks/api/cmeks";
export const kmsKeyUsageOptions: Record<
KmsKeyUsage,
{
label: string;
tooltip: string;
}
> = {
[KmsKeyUsage.ENCRYPT_DECRYPT]: {
label: "Encrypt/Decrypt",
tooltip: "Use the key only to encrypt and decrypt data."
},
[KmsKeyUsage.SIGN_VERIFY]: {
label: "Sign/Verify",
tooltip:
"Key pairs for digital signing. Uses the private key for signing and the public key for verification."
}
};
export const keyUsageDefaultOption: Record<
KmsKeyUsage,
SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm
> = {
[KmsKeyUsage.ENCRYPT_DECRYPT]: SymmetricKeyAlgorithm.AES_GCM_256,
[KmsKeyUsage.SIGN_VERIFY]: AsymmetricKeyAlgorithm.RSA_4096
};
@@ -106,6 +106,10 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.GET_CMEK]: "Get KMS key", [EventType.GET_CMEK]: "Get KMS key",
[EventType.CMEK_ENCRYPT]: "Encrypt with KMS key", [EventType.CMEK_ENCRYPT]: "Encrypt with KMS key",
[EventType.CMEK_DECRYPT]: "Decrypt with KMS key", [EventType.CMEK_DECRYPT]: "Decrypt with KMS key",
[EventType.CMEK_SIGN]: "Sign with KMS key",
[EventType.CMEK_VERIFY]: "Verify with KMS key",
[EventType.CMEK_LIST_SIGNING_ALGORITHMS]: "List signing algorithms for KMS key",
[EventType.CMEK_GET_PUBLIC_KEY]: "Get public key for KMS key",
[EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]: [EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]:
"Update SSO group to organization role mapping", "Update SSO group to organization role mapping",
[EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]: "List SSO group to organization role mapping", [EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]: "List SSO group to organization role mapping",
@@ -110,6 +110,10 @@ export enum EventType {
GET_CMEK = "get-cmek", GET_CMEK = "get-cmek",
CMEK_ENCRYPT = "cmek-encrypt", CMEK_ENCRYPT = "cmek-encrypt",
CMEK_DECRYPT = "cmek-decrypt", 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", 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_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
GET_PROJECT_TEMPLATES = "get-project-templates", GET_PROJECT_TEMPLATES = "get-project-templates",
@@ -8,6 +8,10 @@ import {
TCmekDecryptResponse, TCmekDecryptResponse,
TCmekEncrypt, TCmekEncrypt,
TCmekEncryptResponse, TCmekEncryptResponse,
TCmekSign,
TCmekSignResponse,
TCmekVerify,
TCmekVerifyResponse,
TCreateCmek, TCreateCmek,
TDeleteCmek, TDeleteCmek,
TUpdateCmek TUpdateCmek
@@ -74,6 +78,44 @@ export const useCmekEncrypt = () => {
}); });
}; };
export const useCmekSign = () => {
return useMutation({
mutationFn: async ({
keyId,
data,
signingAlgorithm,
isBase64Encoded
}: TCmekSign & { isBase64Encoded: boolean }) => {
const res = await apiRequest.post<TCmekSignResponse>(`/api/v1/kms/keys/${keyId}/sign`, {
data: isBase64Encoded ? data : encodeBase64(Buffer.from(data)),
signingAlgorithm
});
return res.data;
}
});
};
export const useCmekVerify = () => {
return useMutation({
mutationFn: async ({
keyId,
data,
signature,
signingAlgorithm,
isBase64Encoded
}: TCmekVerify & { isBase64Encoded: boolean }) => {
const res = await apiRequest.post<TCmekVerifyResponse>(`/api/v1/kms/keys/${keyId}/verify`, {
data: isBase64Encoded ? data : encodeBase64(Buffer.from(data)),
signature,
signingAlgorithm
});
return res.data;
}
});
};
export const useCmekDecrypt = () => { export const useCmekDecrypt = () => {
return useMutation({ return useMutation({
mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => { mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => {
+59 -3
View File
@@ -1,10 +1,18 @@
import { z } from "zod";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
export enum KmsKeyUsage {
ENCRYPT_DECRYPT = "encrypt-decrypt",
SIGN_VERIFY = "sign-verify"
}
export type TCmek = { export type TCmek = {
id: string; id: string;
keyUsage: KmsKeyUsage;
name: string; name: string;
description?: string; description?: string;
encryptionAlgorithm: EncryptionAlgorithm; encryptionAlgorithm: AsymmetricKeyAlgorithm | SymmetricKeyAlgorithm;
projectId: string; projectId: string;
isDisabled: boolean; isDisabled: boolean;
isReserved: boolean; isReserved: boolean;
@@ -17,7 +25,8 @@ export type TCmek = {
type ProjectRef = { projectId: string }; type ProjectRef = { projectId: string };
type KeyRef = { keyId: string }; type KeyRef = { keyId: string };
export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm"> & ProjectRef; export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm" | "keyUsage"> &
ProjectRef;
export type TUpdateCmek = KeyRef & export type TUpdateCmek = KeyRef &
Partial<Pick<TCmek, "name" | "description" | "isDisabled">> & Partial<Pick<TCmek, "name" | "description" | "isDisabled">> &
ProjectRef; ProjectRef;
@@ -26,6 +35,13 @@ export type TDeleteCmek = KeyRef & ProjectRef;
export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean }; export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean };
export type TCmekDecrypt = KeyRef & { ciphertext: string }; export type TCmekDecrypt = KeyRef & { ciphertext: string };
export type TCmekSign = KeyRef & { data: string; signingAlgorithm: SigningAlgorithm };
export type TCmekVerify = KeyRef & {
data: string;
signature: string;
signingAlgorithm: SigningAlgorithm;
};
export type TProjectCmeksList = { export type TProjectCmeksList = {
keys: TCmek[]; keys: TCmek[];
totalCount: number; totalCount: number;
@@ -44,6 +60,18 @@ export type TCmekEncryptResponse = {
ciphertext: string; ciphertext: string;
}; };
export type TCmekSignResponse = {
signature: string;
keyId: string;
signingAlgorithm: SigningAlgorithm;
};
export type TCmekVerifyResponse = {
signatureValid: boolean;
keyId: string;
signingAlgorithm: SigningAlgorithm;
};
export type TCmekDecryptResponse = { export type TCmekDecryptResponse = {
plaintext: string; plaintext: string;
}; };
@@ -52,7 +80,35 @@ export enum CmekOrderBy {
Name = "name" Name = "name"
} }
export enum EncryptionAlgorithm { export enum AsymmetricKeyAlgorithm {
RSA_4096 = "RSA_4096",
ECC_NIST_P256 = "ECC_NIST_P256"
}
// Supported symmetric encrypt/decrypt algorithms
export enum SymmetricKeyAlgorithm {
AES_GCM_256 = "aes-256-gcm", AES_GCM_256 = "aes-256-gcm",
AES_GCM_128 = "aes-128-gcm" AES_GCM_128 = "aes-128-gcm"
} }
export const AllowedEncryptionKeyAlgorithms = z.enum([
...Object.values(SymmetricKeyAlgorithm),
...Object.values(AsymmetricKeyAlgorithm)
] as [string, ...string[]]).options;
export enum SigningAlgorithm {
// RSA PSS algorithms
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
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
ECDSA_SHA_256 = "ECDSA_SHA_256",
ECDSA_SHA_384 = "ECDSA_SHA_384",
ECDSA_SHA_512 = "ECDSA_SHA_512"
}
+14
View File
@@ -0,0 +1,14 @@
const base64WithPadding =
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
export const isBase64 = (str: string): boolean => {
if (typeof str !== "string") {
throw new TypeError("Expected a string");
}
if (str === "") return true;
const regex = base64WithPadding;
return regex.test(str);
};
@@ -15,13 +15,23 @@ import {
TextArea TextArea
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { EncryptionAlgorithm, TCmek, useCreateCmek, useUpdateCmek } from "@app/hooks/api/cmeks"; import { keyUsageDefaultOption, kmsKeyUsageOptions } from "@app/helpers/kms";
import {
AllowedEncryptionKeyAlgorithms,
AsymmetricKeyAlgorithm,
KmsKeyUsage,
SymmetricKeyAlgorithm,
TCmek,
useCreateCmek,
useUpdateCmek
} from "@app/hooks/api/cmeks";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({ const formSchema = z.object({
name: slugSchema({ min: 1, max: 32, field: "Name" }), name: slugSchema({ min: 1, max: 32, field: "Name" }),
description: z.string().max(500).optional(), description: z.string().max(500).optional(),
encryptionAlgorithm: z.nativeEnum(EncryptionAlgorithm) encryptionAlgorithm: z.enum(AllowedEncryptionKeyAlgorithms),
keyUsage: z.nativeEnum(KmsKeyUsage)
}); });
export type FormData = z.infer<typeof formSchema>; export type FormData = z.infer<typeof formSchema>;
@@ -47,24 +57,33 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
control, control,
handleSubmit, handleSubmit,
register, register,
setValue,
watch,
formState: { isSubmitting, errors } formState: { isSubmitting, errors }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
name: cmek?.name, name: cmek?.name,
description: cmek?.description, description: cmek?.description,
encryptionAlgorithm: EncryptionAlgorithm.AES_GCM_256 encryptionAlgorithm: SymmetricKeyAlgorithm.AES_GCM_256,
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT
} }
}); });
const handleCreateCmek = async ({ encryptionAlgorithm, name, description }: FormData) => { const handleCreateCmek = async ({
encryptionAlgorithm,
name,
description,
keyUsage
}: FormData) => {
const mutation = isUpdate const mutation = isUpdate
? updateCmek.mutateAsync({ keyId: cmek.id, projectId, name, description }) ? updateCmek.mutateAsync({ keyId: cmek.id, projectId, name, description })
: createCmek.mutateAsync({ : createCmek.mutateAsync({
projectId, projectId,
encryptionAlgorithm,
name, name,
description description,
keyUsage,
encryptionAlgorithm: encryptionAlgorithm as AsymmetricKeyAlgorithm | SymmetricKeyAlgorithm
}); });
try { try {
@@ -83,6 +102,8 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
} }
}; };
const selectedKeyUsage = watch("keyUsage");
return ( return (
<form onSubmit={handleSubmit(handleCreateCmek)}> <form onSubmit={handleSubmit(handleCreateCmek)}>
<FormControl <FormControl
@@ -93,23 +114,97 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
> >
<Input autoFocus placeholder="my-secret-key" {...register("name")} /> <Input autoFocus placeholder="my-secret-key" {...register("name")} />
</FormControl> </FormControl>
{!isUpdate && ( <div className="flex w-full items-center gap-2">
<Controller {!isUpdate && (
control={control} <>
name="encryptionAlgorithm" <Controller
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( control={control}
<FormControl label="Algorithm" errorText={error?.message} isError={Boolean(error)}> name="keyUsage"
<Select defaultValue={field.value} onValueChange={onChange} className="w-full"> render={({ field: { onChange, ...field }, fieldState: { error } }) => (
{Object.entries(EncryptionAlgorithm)?.map(([key, value]) => ( <FormControl
<SelectItem value={value} key={`source-environment-${key}`}> className="w-full"
{key.replaceAll("_", "-")} tooltipText={
</SelectItem> <div className="space-y-4">
))} {Object.entries(KmsKeyUsage).map(([key, value]) => (
</Select> <div key={`key-usage-${key}`}>
</FormControl> <p className="font-bold">{kmsKeyUsageOptions[value].label}</p>
)} <p>{kmsKeyUsageOptions[value].tooltip}</p>
/> </div>
)} ))}
</div>
}
label="Key Usage"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
onValueChange={(e) => {
if (keyUsageDefaultOption[e as KmsKeyUsage]) {
setValue("encryptionAlgorithm", keyUsageDefaultOption[e as KmsKeyUsage], {
shouldDirty: true,
shouldValidate: true
});
}
onChange(e);
}}
className="w-full"
>
{Object.entries(KmsKeyUsage)?.map(([key, value]) => (
<SelectItem value={value} key={`key-usage-${key}`}>
{kmsKeyUsageOptions[value].label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="encryptionAlgorithm"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="w-full"
label="Algorithm"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
value={field.value}
onValueChange={onChange}
className="w-full"
>
{Object.entries(AllowedEncryptionKeyAlgorithms)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
?.filter(([_, value]) => {
if (selectedKeyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
return Object.values(SymmetricKeyAlgorithm).includes(
value as unknown as SymmetricKeyAlgorithm
);
}
if (selectedKeyUsage === KmsKeyUsage.SIGN_VERIFY) {
return Object.values(AsymmetricKeyAlgorithm).includes(
value as unknown as AsymmetricKeyAlgorithm
);
}
return false;
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(([_, value]) => (
<SelectItem value={value} key={`encryption-algorithm-${value}`}>
<span className="uppercase">{value.replaceAll("-", " ")}</span>
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</>
)}
</div>
<FormControl <FormControl
label="Description (optional)" label="Description (optional)"
errorText={errors.description?.message} errorText={errors.description?.message}
@@ -0,0 +1,195 @@
import { Controller, useForm } from "react-hook-form";
import { faCheckCircle, faFileSignature, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem,
Switch,
TextArea,
Tooltip
} from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { SigningAlgorithm, TCmek, useCmekSign } from "@app/hooks/api/cmeks";
const formSchema = z.object({
data: z.string(),
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
isBase64Encoded: z.boolean()
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
cmek: TCmek;
};
type FormProps = Pick<Props, "cmek">;
const SignForm = ({ cmek }: FormProps) => {
const cmekSign = useCmekSign();
const {
handleSubmit,
register,
control,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
signingAlgorithm: cmek?.encryptionAlgorithm?.startsWith("RSA")
? SigningAlgorithm.RSASSA_PSS_SHA_512
: SigningAlgorithm.ECDSA_SHA_256,
isBase64Encoded: false
}
});
const [copySignature, isCopyingSignature, setCopySignature] = useTimedReset<string>({
initialState: "Copy to Clipboard"
});
const handleSignData = async (formData: FormData) => {
try {
await cmekSign.mutateAsync({ ...formData, keyId: cmek.id });
createNotification({
text: "Successfully signed data",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to sign data",
type: "error"
});
}
};
const signature = cmekSign.data?.signature;
const handleCopyToClipboard = () => {
navigator.clipboard.writeText(signature ?? "");
setCopySignature("Copied to Clipboard");
};
const allowedSigningAlgorithms = Object.values(SigningAlgorithm).filter((a) =>
cmek?.encryptionAlgorithm?.startsWith("RSA")
? a.toLowerCase().startsWith("rsa")
: a.toLowerCase().startsWith("ecdsa")
);
return (
<form onSubmit={handleSubmit(handleSignData)}>
{signature ? (
<FormControl label="Data Signature">
<TextArea
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
isDisabled
value={signature}
/>
</FormControl>
) : (
<>
<FormControl
label="Data to Sign"
errorText={errors.data?.message}
isError={Boolean(errors.data)}
>
<TextArea
{...register("data")}
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
/>
</FormControl>
<div className="mb-6 flex w-full items-center justify-between gap-2">
<Controller
control={control}
name="signingAlgorithm"
render={({ field: { onChange, value } }) => (
<FormControl label="Signing Algorithm">
<Select onValueChange={onChange} value={value} className="w-full">
{allowedSigningAlgorithms.map((a) => (
<SelectItem key={a} value={a}>
{a.replaceAll("_", " ")}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="isBase64Encoded"
render={({ field: { onChange, value } }) => (
<Switch id="encode-base-64" isChecked={value} onCheckedChange={onChange}>
Data is Base64 encoded{" "}
<Tooltip content="Toggle this switch on if your data is already Base64 encoded to avoid redundant encoding.">
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
</Tooltip>
</Switch>
)}
/>
</div>
</>
)}
<div className="flex items-center">
<Button
className={`mr-4 ${signature ? "w-44" : ""}`}
size="sm"
leftIcon={
// eslint-disable-next-line no-nested-ternary
signature ? (
isCopyingSignature ? (
<FontAwesomeIcon icon={faCheckCircle} />
) : (
<FontAwesomeIcon icon={faFileSignature} />
)
) : (
<FontAwesomeIcon icon={faFileSignature} />
)
}
onClick={signature ? handleCopyToClipboard : undefined}
type={signature ? "button" : "submit"}
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{signature ? copySignature : "Sign"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
{signature ? "Close" : "Cancel"}
</Button>
</ModalClose>
</div>
</form>
);
};
export const CmekSignModal = ({ isOpen, onOpenChange, cmek }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Sign Data"
subTitle={
<>
Sign data using <span className="font-bold">{cmek?.name}</span>. Returns a Base64
encoded signature.
</>
}
>
<SignForm cmek={cmek} />
</ModalContent>
</Modal>
);
};
@@ -8,6 +8,7 @@ import {
faCopy, faCopy,
faEdit, faEdit,
faEllipsis, faEllipsis,
faFileSignature,
faInfoCircle, faInfoCircle,
faKey, faKey,
faLock, faLock,
@@ -51,14 +52,17 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { kmsKeyUsageOptions } from "@app/helpers/kms";
import { usePagination, usePopUp, useResetPageHelper, useTimedReset } from "@app/hooks"; import { usePagination, usePopUp, useResetPageHelper, useTimedReset } from "@app/hooks";
import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks"; import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks";
import { CmekOrderBy, TCmek } from "@app/hooks/api/cmeks/types"; import { CmekOrderBy, KmsKeyUsage, TCmek } from "@app/hooks/api/cmeks/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { CmekDecryptModal } from "./CmekDecryptModal"; import { CmekDecryptModal } from "./CmekDecryptModal";
import { CmekEncryptModal } from "./CmekEncryptModal"; import { CmekEncryptModal } from "./CmekEncryptModal";
import { CmekModal } from "./CmekModal"; import { CmekModal } from "./CmekModal";
import { CmekSignModal } from "./CmekSignModal";
import { CmekVerifyModal } from "./CmekVerifyModal";
import { DeleteCmekModal } from "./DeleteCmekModal"; import { DeleteCmekModal } from "./DeleteCmekModal";
const getStatusBadgeProps = ( const getStatusBadgeProps = (
@@ -123,7 +127,9 @@ export const CmekTable = () => {
"upsertKey", "upsertKey",
"deleteKey", "deleteKey",
"encryptData", "encryptData",
"decryptData" "decryptData",
"signData",
"verifyData"
] as const); ] as const);
const handleSort = () => { const handleSort = () => {
@@ -179,6 +185,15 @@ export const CmekTable = () => {
ProjectPermissionSub.Cmek ProjectPermissionSub.Cmek
); );
const cannotSignData = permission.cannot(
ProjectPermissionCmekActions.Sign,
ProjectPermissionSub.Cmek
);
const cannotVerifyData = permission.cannot(
ProjectPermissionCmekActions.Verify,
ProjectPermissionSub.Cmek
);
return ( return (
<motion.div <motion.div
key="kms-keys-tab" key="kms-keys-tab"
@@ -246,6 +261,7 @@ export const CmekTable = () => {
</div> </div>
</Th> </Th>
<Th>Key ID</Th> <Th>Key ID</Th>
<Th>Key Usage</Th>
<Th>Algorithm</Th> <Th>Algorithm</Th>
<Th>Status</Th> <Th>Status</Th>
<Th>Version</Th> <Th>Version</Th>
@@ -257,7 +273,15 @@ export const CmekTable = () => {
{!isPending && {!isPending &&
keys.length > 0 && keys.length > 0 &&
keys.map((cmek) => { keys.map((cmek) => {
const { name, id, version, description, encryptionAlgorithm, isDisabled } = cmek; const {
name,
id,
version,
description,
encryptionAlgorithm,
isDisabled,
keyUsage
} = cmek;
const { variant, label } = getStatusBadgeProps(isDisabled); const { variant, label } = getStatusBadgeProps(isDisabled);
return ( return (
@@ -295,6 +319,14 @@ export const CmekTable = () => {
</IconButton> </IconButton>
</div> </div>
</Td> </Td>
<Td>
<div className="flex items-center gap-2">
{kmsKeyUsageOptions[keyUsage].label}
<Tooltip content={kmsKeyUsageOptions[keyUsage].tooltip}>
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
</Tooltip>
</div>
</Td>
<Td className="uppercase">{encryptionAlgorithm}</Td> <Td className="uppercase">{encryptionAlgorithm}</Td>
<Td> <Td>
<Badge variant={variant}>{label}</Badge> <Badge variant={variant}>{label}</Badge>
@@ -314,50 +346,104 @@ export const CmekTable = () => {
</IconButton> </IconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[160px]"> <DropdownMenuContent className="min-w-[160px]">
<Tooltip {keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT && (
content={ <>
// eslint-disable-next-line no-nested-ternary <Tooltip
cannotEncryptData content={
? "Access Restricted" // eslint-disable-next-line no-nested-ternary
: isDisabled cannotEncryptData
? "Key Disabled" ? "Access Restricted"
: "" : isDisabled
} ? "Key Disabled"
position="left" : ""
> }
<div> position="left"
<DropdownMenuItem
onClick={() => handlePopUpOpen("encryptData", cmek)}
icon={<FontAwesomeIcon icon={faLock} />}
iconPos="left"
isDisabled={cannotEncryptData || isDisabled}
> >
Encrypt Data <div>
</DropdownMenuItem> <DropdownMenuItem
</div> onClick={() => handlePopUpOpen("encryptData", cmek)}
</Tooltip> icon={<FontAwesomeIcon icon={faLock} />}
<Tooltip iconPos="left"
content={ isDisabled={cannotEncryptData || isDisabled}
// eslint-disable-next-line no-nested-ternary >
cannotDecryptData Encrypt Data
? "Access Restricted" </DropdownMenuItem>
: isDisabled </div>
? "Key Disabled" </Tooltip>
: "" <Tooltip
} content={
position="left" // eslint-disable-next-line no-nested-ternary
> cannotDecryptData
<div> ? "Access Restricted"
<DropdownMenuItem : isDisabled
onClick={() => handlePopUpOpen("decryptData", cmek)} ? "Key Disabled"
icon={<FontAwesomeIcon icon={faLockOpen} />} : ""
iconPos="left" }
isDisabled={cannotDecryptData || isDisabled} position="left"
> >
Decrypt Data <div>
</DropdownMenuItem> <DropdownMenuItem
</div> onClick={() => handlePopUpOpen("decryptData", cmek)}
</Tooltip> icon={<FontAwesomeIcon icon={faLockOpen} />}
iconPos="left"
isDisabled={cannotDecryptData || isDisabled}
>
Decrypt Data
</DropdownMenuItem>
</div>
</Tooltip>
</>
)}
{keyUsage === KmsKeyUsage.SIGN_VERIFY && (
<>
<Tooltip
content={
// eslint-disable-next-line no-nested-ternary
cannotSignData
? "Access Restricted"
: isDisabled
? "Key Disabled"
: ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("signData", cmek)}
icon={<FontAwesomeIcon icon={faFileSignature} />}
iconPos="left"
isDisabled={cannotSignData || isDisabled}
>
Sign Data
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={
// eslint-disable-next-line no-nested-ternary
cannotVerifyData
? "Access Restricted"
: isDisabled
? "Key Disabled"
: ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("verifyData", cmek)}
icon={<FontAwesomeIcon icon={faCheckCircle} />}
iconPos="left"
isDisabled={cannotVerifyData || isDisabled}
>
Verify Data
</DropdownMenuItem>
</div>
</Tooltip>
</>
)}
<Tooltip <Tooltip
content={cannotEditKey ? "Access Restricted" : ""} content={cannotEditKey ? "Access Restricted" : ""}
position="left" position="left"
@@ -456,6 +542,16 @@ export const CmekTable = () => {
onOpenChange={(isOpen) => handlePopUpToggle("decryptData", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("decryptData", isOpen)}
cmek={popUp.decryptData.data as TCmek} cmek={popUp.decryptData.data as TCmek}
/> />
<CmekSignModal
isOpen={popUp.signData.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("signData", isOpen)}
cmek={popUp.signData.data as TCmek}
/>
<CmekVerifyModal
isOpen={popUp.verifyData.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("verifyData", isOpen)}
cmek={popUp.verifyData.data as TCmek}
/>
</div> </div>
</motion.div> </motion.div>
); );
@@ -0,0 +1,251 @@
import { Controller, useForm } from "react-hook-form";
import { faFileSignature, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { decodeBase64 } from "tweetnacl-util";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
FormControl,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem,
Switch,
TextArea,
Tooltip
} from "@app/components/v2";
import { SigningAlgorithm, TCmek, useCmekVerify } from "@app/hooks/api/cmeks";
import { isBase64 } from "@app/lib/fn/base64";
const formSchema = z.object({
data: z.string().min(1, { message: "Data cannot be empty" }),
signature: z
.string()
.min(1, { message: "Signature cannot be empty" })
.superRefine((val, ctx) => {
if (!isBase64(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Signature must be base64-encoded"
});
}
}),
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
isBase64Encoded: z.boolean()
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
cmek: TCmek;
};
type FormProps = Pick<Props, "cmek">;
const VerifyForm = ({ cmek }: FormProps) => {
const cmekVerify = useCmekVerify();
const {
handleSubmit,
register,
watch,
control,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
signingAlgorithm: cmek?.encryptionAlgorithm?.startsWith("RSA")
? SigningAlgorithm.RSASSA_PSS_SHA_512
: SigningAlgorithm.ECDSA_SHA_256,
isBase64Encoded: false
}
});
const handleVerifyData = async (formData: FormData) => {
try {
const result = await cmekVerify.mutateAsync({ ...formData, keyId: cmek.id });
if (result.signatureValid) {
createNotification({
text: "Successfully verified signature",
type: "success"
});
} else {
createNotification({
title: "Signature Verification Failed",
text: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with.",
type: "error"
});
}
} catch (err) {
console.error(err);
createNotification({
text: "Failed to sign data",
type: "error"
});
}
};
const signature = watch("signature");
const data = watch("data");
const isBase64Encoded = watch("isBase64Encoded");
const signatureValid = cmekVerify.data?.signatureValid;
const signingAlgorithm = cmekVerify.data?.signingAlgorithm;
const allowedSigningAlgorithms = Object.values(SigningAlgorithm).filter((a) =>
cmek?.encryptionAlgorithm?.startsWith("RSA")
? a.toLowerCase().startsWith("rsa")
: a.toLowerCase().startsWith("ecdsa")
);
return (
<form onSubmit={handleSubmit(handleVerifyData)}>
{signatureValid !== undefined ? (
<div className="mb-6 flex flex-col gap-2">
<div className="flex items-center justify-between space-x-2">
<span className="text-sm opacity-60">Signature Status:</span>
<Badge variant={signatureValid ? "success" : "danger"}>
<Tooltip
content={
signatureValid
? "The signature is valid. signature was created using the same signing algorithm and key as the one used to sign the data."
: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with."
}
>
{signatureValid ? (
<div className="flex items-center justify-center gap-2">
<p>Valid</p>
</div>
) : (
<div className="flex items-center justify-center gap-2">
<p>Invalid</p>
</div>
)}
</Tooltip>
</Badge>
</div>
<div className="flex items-center justify-between gap-2">
<span className="text-sm opacity-60">Signing Algorithm:</span>
<Badge variant="primary">{signingAlgorithm}</Badge>
</div>
<div className="mt-3">
<span className="text-sm opacity-60">Signature:</span>{" "}
<div className="whitespace-pre-wrap break-words rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 text-sm">
{signature}
</div>
</div>
<div>
<span className="text-sm opacity-60">Data:</span>{" "}
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 text-sm">
{isBase64Encoded ? decodeBase64(data).toString() : data}
</div>
</div>
</div>
) : (
<>
<FormControl
label="Data to Verify"
errorText={errors.data?.message}
isError={Boolean(errors.data)}
>
<TextArea
{...register("data")}
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
/>
</FormControl>
<FormControl
label="Signature of Data"
tooltipText="Must be base64-encoded, like the signature you received when you signed the data."
errorText={errors.signature?.message}
isError={Boolean(errors.signature)}
>
<TextArea
{...register("signature")}
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
/>
</FormControl>
<div className="mb-6 flex w-full items-center justify-between gap-2">
<Controller
control={control}
name="signingAlgorithm"
render={({ field: { onChange, value } }) => (
<FormControl label="Signing Algorithm">
<Select onValueChange={onChange} value={value} className="w-full">
{allowedSigningAlgorithms.map((a) => (
<SelectItem key={a} value={a}>
{a.replaceAll("_", " ")}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="isBase64Encoded"
render={({ field: { onChange, value } }) => (
<Switch id="encode-base-64" isChecked={value} onCheckedChange={onChange}>
Data is Base64 encoded{" "}
<Tooltip content="Toggle this switch on if your data is already Base64 encoded to avoid redundant encoding.">
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
</Tooltip>
</Switch>
)}
/>
</div>
</>
)}
<div className="flex items-center">
{signatureValid === undefined && (
<Button
className="mr-4 w-44"
size="sm"
leftIcon={<FontAwesomeIcon icon={faFileSignature} />}
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Verify
</Button>
)}
<ModalClose asChild>
<Button
colorSchema={signatureValid === undefined ? "secondary" : "primary"}
variant={signatureValid === undefined ? "plain" : undefined}
>
{signatureValid !== undefined ? "Close" : "Cancel"}
</Button>
</ModalClose>
</div>
</form>
);
};
export const CmekVerifyModal = ({ isOpen, onOpenChange, cmek }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Verify Signature"
subTitle={
<>
Verify a signature using <span className="font-bold">{cmek?.name}</span>.
</>
}
>
<VerifyForm cmek={cmek} />
</ModalContent>
</Modal>
);
};
@@ -47,7 +47,9 @@ const CmekPolicyActionSchema = z.object({
delete: z.boolean().optional(), delete: z.boolean().optional(),
create: z.boolean().optional(), create: z.boolean().optional(),
encrypt: z.boolean().optional(), encrypt: z.boolean().optional(),
decrypt: z.boolean().optional() decrypt: z.boolean().optional(),
sign: z.boolean().optional(),
verify: z.boolean().optional()
}); });
const DynamicSecretPolicyActionSchema = z.object({ const DynamicSecretPolicyActionSchema = z.object({
@@ -482,6 +484,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const canCreate = action.includes(ProjectPermissionCmekActions.Create); const canCreate = action.includes(ProjectPermissionCmekActions.Create);
const canEncrypt = action.includes(ProjectPermissionCmekActions.Encrypt); const canEncrypt = action.includes(ProjectPermissionCmekActions.Encrypt);
const canDecrypt = action.includes(ProjectPermissionCmekActions.Decrypt); const canDecrypt = action.includes(ProjectPermissionCmekActions.Decrypt);
const canSign = action.includes(ProjectPermissionCmekActions.Sign);
const canVerify = action.includes(ProjectPermissionCmekActions.Verify);
if (!formVal[subject]) formVal[subject] = [{}]; if (!formVal[subject]) formVal[subject] = [{}];
@@ -492,6 +496,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
if (canDelete) formVal[subject]![0].delete = true; if (canDelete) formVal[subject]![0].delete = true;
if (canEncrypt) formVal[subject]![0].encrypt = true; if (canEncrypt) formVal[subject]![0].encrypt = true;
if (canDecrypt) formVal[subject]![0].decrypt = true; if (canDecrypt) formVal[subject]![0].decrypt = true;
if (canSign) formVal[subject]![0].sign = true;
if (canVerify) formVal[subject]![0].verify = true;
return; return;
} }
@@ -769,7 +775,9 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Modify", value: "edit" }, { label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }, { label: "Remove", value: "delete" },
{ label: "Encrypt", value: "encrypt" }, { label: "Encrypt", value: "encrypt" },
{ label: "Decrypt", value: "decrypt" } { label: "Decrypt", value: "decrypt" },
{ label: "Sign", value: "sign" },
{ label: "Verify", value: "verify" }
] ]
}, },
[ProjectPermissionSub.Kms]: { [ProjectPermissionSub.Kms]: {