mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(kms): sign & verify data
This commit is contained in:
22
backend/package-lock.json
generated
22
backend/package-lock.json
generated
@@ -132,7 +132,7 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20.9.5",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-github": "^1.1.12",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
@@ -9753,11 +9753,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
|
||||
"integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==",
|
||||
"version": "20.17.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
||||
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
@@ -20081,11 +20082,6 @@
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/scim-patch/node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"node_modules/scim2-parse-filter": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
||||
@@ -22442,9 +22438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.0",
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20.9.5",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-github": "^1.1.12",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { KmsKeyIntent } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasTypeColumn = await knex.schema.hasColumn(TableName.KmsKey, "type");
|
||||
|
||||
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||
if (!hasTypeColumn) t.string("type").notNullable().defaultTo(KmsKeyIntent.ENCRYPT_DECRYPT);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||
t.dropColumn("type");
|
||||
});
|
||||
}
|
||||
@@ -16,7 +16,8 @@ export const KmsKeysSchema = z.object({
|
||||
name: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
projectId: z.string().nullable().optional()
|
||||
projectId: z.string().nullable().optional(),
|
||||
type: z.string().default("encrypt-decrypt")
|
||||
});
|
||||
|
||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||
|
||||
@@ -23,10 +23,11 @@ export const OrganizationsSchema = z.object({
|
||||
defaultMembershipRole: z.string().default("member"),
|
||||
enforceMfa: z.boolean().default(false),
|
||||
selectedMfaMethod: z.string().nullable().optional(),
|
||||
secretShareSendToAnyone: z.boolean().default(true).nullable().optional(),
|
||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
|
||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
||||
@@ -2,7 +2,7 @@ import z from "zod";
|
||||
|
||||
import { KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -74,7 +74,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
description: "KMIP endpoint for creating managed objects",
|
||||
body: z.object({
|
||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||
algorithm: z.nativeEnum(SymmetricKeyEncryptDecrypt)
|
||||
}),
|
||||
response: {
|
||||
200: KmsKeysSchema
|
||||
@@ -433,7 +433,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
key: z.string(),
|
||||
name: z.string(),
|
||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||
algorithm: z.nativeEnum(SymmetricKeyEncryptDecrypt)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
} from "@app/ee/services/project-template/project-template-types";
|
||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeySignVerify } from "@app/lib/crypto/sign/types";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||
@@ -1897,7 +1898,7 @@ interface CreateCmekEvent {
|
||||
keyId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
encryptionAlgorithm: SymmetricEncryption;
|
||||
encryptionAlgorithm: SymmetricKeyEncryptDecrypt | AsymmetricKeySignVerify;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/er
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { KmsDataKey, KmsKeyIntent } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@@ -115,6 +115,7 @@ export const externalKmsServiceFactory = ({
|
||||
{
|
||||
isReserved: false,
|
||||
description,
|
||||
type: KmsKeyIntent.ENCRYPT_DECRYPT,
|
||||
name: kmsName,
|
||||
orgId: actorOrgId
|
||||
},
|
||||
|
||||
@@ -92,7 +92,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
||||
plaintext: data
|
||||
});
|
||||
if (!encryptedText[0].ciphertext) throw new Error("encryption failed");
|
||||
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext) };
|
||||
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext as Uint8Array) };
|
||||
};
|
||||
|
||||
const decrypt = async (encryptedBlob: Buffer) => {
|
||||
@@ -101,7 +101,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
||||
ciphertext: encryptedBlob
|
||||
});
|
||||
if (!decryptedText[0].plaintext) throw new Error("decryption failed");
|
||||
return { data: Buffer.from(decryptedText[0].plaintext) };
|
||||
return { data: Buffer.from(decryptedText[0].plaintext as Uint8Array) };
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -258,7 +258,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
||||
const decrypt: {
|
||||
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
||||
(encryptedBlob: Buffer): Promise<Buffer>;
|
||||
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
|
||||
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle): Promise<Buffer> => {
|
||||
if (!pkcs11 || !isInitialized) {
|
||||
throw new Error("PKCS#11 module is not initialized");
|
||||
}
|
||||
@@ -309,10 +309,10 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
||||
|
||||
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
|
||||
|
||||
const tempBuffer = Buffer.alloc(encryptedData.length);
|
||||
const tempBuffer: Buffer = Buffer.alloc(encryptedData.length);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
|
||||
|
||||
// Create a new buffer from the decrypted data
|
||||
return Buffer.from(decryptedData);
|
||||
} catch (error) {
|
||||
logger.error(error, "HSM: Failed to perform decryption");
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsKeyIntent } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@@ -403,6 +404,7 @@ export const kmipOperationServiceFactory = ({
|
||||
algorithm,
|
||||
isReserved: false,
|
||||
projectId,
|
||||
type: KmsKeyIntent.ENCRYPT_DECRYPT,
|
||||
orgId: project.orgId
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
|
||||
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
@@ -49,7 +49,7 @@ type KmipOperationBaseDTO = {
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TKmipCreateDTO = {
|
||||
algorithm: SymmetricEncryption;
|
||||
algorithm: SymmetricKeyEncryptDecrypt;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipGetDTO = {
|
||||
@@ -77,7 +77,7 @@ export type TKmipLocateDTO = KmipOperationBaseDTO;
|
||||
export type TKmipRegisterDTO = {
|
||||
name: string;
|
||||
key: string;
|
||||
algorithm: SymmetricEncryption;
|
||||
algorithm: SymmetricKeyEncryptDecrypt;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TSetupOrgKmipDTO = {
|
||||
|
||||
@@ -32,7 +32,9 @@ export enum ProjectPermissionCmekActions {
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Encrypt = "encrypt",
|
||||
Decrypt = "decrypt"
|
||||
Decrypt = "decrypt",
|
||||
Sign = "sign",
|
||||
Verify = "verify"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionDynamicSecretActions {
|
||||
@@ -650,7 +652,9 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
@@ -839,7 +843,9 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
||||
@@ -633,7 +633,8 @@ export const FOLDERS = {
|
||||
path: "The path to list folders from.",
|
||||
directory: "The directory to list folders from. (Deprecated in favor of path)",
|
||||
recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories.",
|
||||
lastSecretModified: "The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)"
|
||||
lastSecretModified:
|
||||
"The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)"
|
||||
},
|
||||
GET_BY_ID: {
|
||||
folderId: "The ID of the folder to get details."
|
||||
@@ -1590,7 +1591,8 @@ export const KMS = {
|
||||
projectId: "The ID of the project to create the key in.",
|
||||
name: "The name of the key to be created. Must be slug-friendly.",
|
||||
description: "An optional description of the key.",
|
||||
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
||||
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key.",
|
||||
type: "The type of key to be created, either encrypt-decrypt or sign-verify, based on your intended use for the key."
|
||||
},
|
||||
UPDATE_KEY: {
|
||||
keyId: "The ID of the key to be updated.",
|
||||
@@ -1623,6 +1625,24 @@ export const KMS = {
|
||||
DECRYPT: {
|
||||
keyId: "The ID of the key to decrypt the data with.",
|
||||
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
|
||||
},
|
||||
|
||||
LIST_SIGNING_ALGORITHMS: {
|
||||
keyId: "The ID of the key to list the signing algorithms for. The key must be for signing and verifying."
|
||||
},
|
||||
|
||||
GET_PUBLIC_KEY: {
|
||||
keyId: "The ID of the key to get the public key for. The key must be for signing and verifying."
|
||||
},
|
||||
|
||||
SIGN: {
|
||||
keyId: "The ID of the key to sign the data with.",
|
||||
data: "The data in string format to be signed (base64 encoded)."
|
||||
},
|
||||
VERIFY: {
|
||||
keyId: "The ID of the key to verify the data with.",
|
||||
data: "The data in string format to be verified (base64 encoded).",
|
||||
signature: "The signature to be verified (base64 encoded)."
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
|
||||
import { SymmetricKeyEncryptDecrypt, TSymmetricEncryptionFns } from "./types";
|
||||
|
||||
const getIvLength = () => {
|
||||
return 12;
|
||||
@@ -10,7 +10,9 @@ const getTagLength = () => {
|
||||
return 16;
|
||||
};
|
||||
|
||||
export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
|
||||
export const symmetricCipherService = (
|
||||
type: SymmetricKeyEncryptDecrypt.AES_GCM_128 | SymmetricKeyEncryptDecrypt.AES_GCM_256
|
||||
): TSymmetricEncryptionFns => {
|
||||
const IV_LENGTH = getIvLength();
|
||||
const TAG_LENGTH = getTagLength();
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { symmetricCipherService } from "./cipher";
|
||||
export { SymmetricEncryption } from "./types";
|
||||
export { AllowedEncryptionKeyAlgorithms, SymmetricKeyEncryptDecrypt } from "./types";
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
export enum SymmetricEncryption {
|
||||
import { z } from "zod";
|
||||
|
||||
import { AsymmetricKeySignVerify } from "../sign/types";
|
||||
|
||||
// Supported symmetric encrypt/decrypt algorithms
|
||||
export enum SymmetricKeyEncryptDecrypt {
|
||||
AES_GCM_256 = "aes-256-gcm",
|
||||
AES_GCM_128 = "aes-128-gcm"
|
||||
}
|
||||
export const SymmetricKeyEncryptDecryptEnum = z.enum(
|
||||
Object.values(SymmetricKeyEncryptDecrypt) as [string, ...string[]]
|
||||
).options;
|
||||
|
||||
export const AllowedEncryptionKeyAlgorithms = z.enum([
|
||||
...Object.values(SymmetricKeyEncryptDecrypt),
|
||||
...Object.values(AsymmetricKeySignVerify)
|
||||
] as [string, ...string[]]).options;
|
||||
|
||||
export type TSymmetricEncryptionFns = {
|
||||
encrypt: (text: Buffer, key: Buffer) => Buffer;
|
||||
|
||||
2
backend/src/lib/crypto/sign/index.ts
Normal file
2
backend/src/lib/crypto/sign/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { signingService } from "./signing";
|
||||
export { AsymmetricKeySignVerify, SigningAlgorithm } from "./types";
|
||||
287
backend/src/lib/crypto/sign/signing.ts
Normal file
287
backend/src/lib/crypto/sign/signing.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { AsymmetricKeySignVerify, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types";
|
||||
|
||||
// Map of signing algorithms to their parameters
|
||||
interface SigningParams {
|
||||
hashAlgorithm: string;
|
||||
padding?: number; // Will use crypto.constants values
|
||||
saltLength?: number;
|
||||
}
|
||||
|
||||
const SHA256_DIGEST_LENGTH = 32;
|
||||
const SHA384_DIGEST_LENGTH = 48;
|
||||
const SHA512_DIGEST_LENGTH = 64;
|
||||
|
||||
/**
|
||||
* Service for cryptographic signing and verification operations using asymmetric keys
|
||||
*
|
||||
* @param algorithm The signing algorithm to use
|
||||
* @returns Object with sign and verify functions
|
||||
*/
|
||||
export const signingService = (algorithm: AsymmetricKeySignVerify): TAsymmetricSignVerifyFns => {
|
||||
const $getSigningParams = (signingAlgorithm: SigningAlgorithm): SigningParams => {
|
||||
switch (signingAlgorithm) {
|
||||
// RSA PSS
|
||||
case SigningAlgorithm.RSASSA_PSS_SHA_256:
|
||||
return {
|
||||
hashAlgorithm: "sha256",
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: SHA256_DIGEST_LENGTH
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PSS_SHA_384:
|
||||
return {
|
||||
hashAlgorithm: "sha384",
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: SHA384_DIGEST_LENGTH
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PSS_SHA_512:
|
||||
return {
|
||||
hashAlgorithm: "sha512",
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: SHA512_DIGEST_LENGTH
|
||||
};
|
||||
|
||||
// RSA PKCS#1 v1.5
|
||||
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256:
|
||||
return {
|
||||
hashAlgorithm: "sha256",
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384:
|
||||
return {
|
||||
hashAlgorithm: "sha384",
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512:
|
||||
return {
|
||||
hashAlgorithm: "sha512",
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
};
|
||||
|
||||
// ECDSA
|
||||
case SigningAlgorithm.ECDSA_SHA_256:
|
||||
return { hashAlgorithm: "sha256" };
|
||||
case SigningAlgorithm.ECDSA_SHA_384:
|
||||
return { hashAlgorithm: "sha384" };
|
||||
case SigningAlgorithm.ECDSA_SHA_512:
|
||||
return { hashAlgorithm: "sha512" };
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported signing algorithm: ${signingAlgorithm as string}`);
|
||||
}
|
||||
};
|
||||
|
||||
// For ECC key generation, nodejs has some strange and hardly documented curve naming conventions
|
||||
const $getEcCurveName = (keyAlgorithm: AsymmetricKeySignVerify): string => {
|
||||
// We will support more in the future
|
||||
switch (keyAlgorithm) {
|
||||
case AsymmetricKeySignVerify.ECC_NIST_P256:
|
||||
return "prime256v1";
|
||||
default:
|
||||
throw new Error(`Unsupported EC curve: ${keyAlgorithm}`);
|
||||
}
|
||||
};
|
||||
|
||||
const $validateAlgorithmWithKeyType = (signingAlgorithm: SigningAlgorithm) => {
|
||||
const isRsaKey = algorithm.startsWith("rsa");
|
||||
const isEccKey = algorithm.startsWith("ecc");
|
||||
|
||||
const isRsaAlgorithm = signingAlgorithm.startsWith("RSASSA");
|
||||
const isEccAlgorithm = signingAlgorithm.startsWith("ECDSA");
|
||||
|
||||
if (isRsaKey && !isRsaAlgorithm) {
|
||||
throw new BadRequestError({ message: `KMS RSA key cannot be used with ${signingAlgorithm}` });
|
||||
}
|
||||
|
||||
if (isEccKey && !isEccAlgorithm) {
|
||||
throw new BadRequestError({ message: `KMS ECC key cannot be used with ${signingAlgorithm}` });
|
||||
}
|
||||
};
|
||||
|
||||
const generateAsymmetricPrivateKey = async () => {
|
||||
const { privateKey } = await new Promise<{ privateKey: string }>((resolve, reject) => {
|
||||
if (algorithm.startsWith("rsa")) {
|
||||
crypto.generateKeyPair(
|
||||
"rsa",
|
||||
{
|
||||
modulusLength: Number(algorithm.split("-")[1]),
|
||||
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||
},
|
||||
(err, _, pk) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ privateKey: pk });
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const namedCurve = $getEcCurveName(algorithm);
|
||||
|
||||
crypto.generateKeyPair(
|
||||
"ec",
|
||||
{
|
||||
namedCurve,
|
||||
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||
},
|
||||
(err, _, pk) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({
|
||||
privateKey: pk
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Buffer.from(privateKey);
|
||||
};
|
||||
|
||||
const getPublicKeyFromPrivateKey = (privateKey: Buffer) => {
|
||||
if (algorithm.startsWith("rsa")) {
|
||||
// For RSA keys in PEM format
|
||||
const privateKeyObj = crypto.createPrivateKey({
|
||||
key: privateKey,
|
||||
format: "pem",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
const publicKey = crypto.createPublicKey(privateKeyObj).export({
|
||||
type: "spki",
|
||||
format: "pem"
|
||||
});
|
||||
|
||||
if (Buffer.isBuffer(publicKey)) {
|
||||
return publicKey;
|
||||
}
|
||||
return Buffer.from(publicKey);
|
||||
}
|
||||
|
||||
const privateKeyObj = crypto.createPrivateKey({
|
||||
key: privateKey,
|
||||
format: "pem",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
// Return public key in PEM format for both RSA and EC
|
||||
const publicKey = crypto.createPublicKey(privateKeyObj).export({
|
||||
type: "spki",
|
||||
format: "pem"
|
||||
});
|
||||
|
||||
if (Buffer.isBuffer(publicKey)) {
|
||||
return publicKey;
|
||||
}
|
||||
return Buffer.from(publicKey);
|
||||
};
|
||||
|
||||
const sign = (data: Buffer, privateKey: Buffer, signingAlgorithm: SigningAlgorithm): Buffer => {
|
||||
$validateAlgorithmWithKeyType(signingAlgorithm);
|
||||
|
||||
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
|
||||
|
||||
const privateKeyObject = crypto.createPrivateKey({
|
||||
key: privateKey,
|
||||
format: "pem",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
// For RSA signatures
|
||||
if (signingAlgorithm.startsWith("RSASSA")) {
|
||||
const signer = crypto.createSign(hashAlgorithm);
|
||||
signer.update(data);
|
||||
|
||||
if (signingAlgorithm.includes("PSS")) {
|
||||
// For PSS padding
|
||||
return signer.sign({
|
||||
key: privateKeyObject,
|
||||
padding,
|
||||
saltLength
|
||||
});
|
||||
}
|
||||
// For PKCS1 v1.5 padding
|
||||
return signer.sign({
|
||||
key: privateKeyObject
|
||||
});
|
||||
}
|
||||
if (signingAlgorithm.startsWith("ECDSA")) {
|
||||
// For ECDSA signatures
|
||||
const signer = crypto.createSign(hashAlgorithm);
|
||||
signer.update(data);
|
||||
return signer.sign({
|
||||
key: privateKeyObject,
|
||||
dsaEncoding: "ieee-p1363" // Based on AWS KMS implementation, where ECDSA signatures follow the ANSI X9.62-2005 format, which is equivalent to the IEEE-P1363 format
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Signing algorithm ${signingAlgorithm} not implemented`
|
||||
});
|
||||
};
|
||||
|
||||
const verify = (data: Buffer, signature: Buffer, publicKey: Buffer, signingAlgorithm: SigningAlgorithm): boolean => {
|
||||
try {
|
||||
$validateAlgorithmWithKeyType(signingAlgorithm);
|
||||
|
||||
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
|
||||
|
||||
// For RSA signatures
|
||||
if (signingAlgorithm.startsWith("RSASSA")) {
|
||||
const verifier = crypto.createVerify(hashAlgorithm);
|
||||
verifier.update(data);
|
||||
|
||||
if (signingAlgorithm.includes("PSS")) {
|
||||
// For PSS padding
|
||||
return verifier.verify(
|
||||
{
|
||||
key: publicKey.toString(),
|
||||
padding,
|
||||
saltLength
|
||||
},
|
||||
signature
|
||||
);
|
||||
}
|
||||
// For PKCS1 v1.5 padding
|
||||
return verifier.verify(
|
||||
{
|
||||
key: publicKey.toString()
|
||||
},
|
||||
signature
|
||||
);
|
||||
}
|
||||
// For ECDSA signatures
|
||||
if (signingAlgorithm.startsWith("ECDSA")) {
|
||||
const verifier = crypto.createVerify(hashAlgorithm);
|
||||
verifier.update(data);
|
||||
return verifier.verify(
|
||||
{
|
||||
key: publicKey.toString(),
|
||||
dsaEncoding: "ieee-p1363"
|
||||
},
|
||||
signature
|
||||
);
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Verification for algorithm ${signingAlgorithm} not implemented`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error, "KMS: Failed to verify signature");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
sign,
|
||||
verify,
|
||||
generateAsymmetricPrivateKey,
|
||||
getPublicKeyFromPrivateKey
|
||||
};
|
||||
};
|
||||
39
backend/src/lib/crypto/sign/types.ts
Normal file
39
backend/src/lib/crypto/sign/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type TAsymmetricSignVerifyFns = {
|
||||
sign: (data: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm) => Buffer;
|
||||
verify: (data: Buffer, signature: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm) => boolean;
|
||||
generateAsymmetricPrivateKey: () => Promise<Buffer>;
|
||||
getPublicKeyFromPrivateKey: (privateKey: Buffer) => Buffer;
|
||||
};
|
||||
|
||||
// Supported asymmetric key types
|
||||
export enum AsymmetricKeySignVerify {
|
||||
RSA_4096 = "rsa-4096",
|
||||
ECC_NIST_P256 = "ecc-nist-p256"
|
||||
}
|
||||
|
||||
export const AsymmetricKeySignVerifyEnum = z.enum(
|
||||
Object.values(AsymmetricKeySignVerify) as [string, ...string[]]
|
||||
).options;
|
||||
|
||||
export enum SigningAlgorithm {
|
||||
// RSA PSS algorithms
|
||||
// These are NOT deterministic and include randomness.
|
||||
// This means that the output signature is different each time for the same input.
|
||||
RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256",
|
||||
RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384",
|
||||
RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512",
|
||||
|
||||
// RSA PKCS#1 v1.5 algorithms
|
||||
// These are deterministic and the output is the same each time for the same input.
|
||||
RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256",
|
||||
RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384",
|
||||
RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512",
|
||||
|
||||
// ECDSA algorithms
|
||||
// None of these are deterministic and include randomness like RSA PSS.
|
||||
ECDSA_SHA_256 = "ECDSA_SHA_256",
|
||||
ECDSA_SHA_384 = "ECDSA_SHA_384",
|
||||
ECDSA_SHA_512 = "ECDSA_SHA_512"
|
||||
}
|
||||
@@ -4,18 +4,22 @@ import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { KMS } from "@app/lib/api-docs";
|
||||
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { AllowedEncryptionKeyAlgorithms, SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeySignVerify, SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
|
||||
import { CmekOrderBy, TCmekKeyEncryptionAlgorithm } from "@app/services/cmek/cmek-types";
|
||||
import { KmsKeyIntent } from "@app/services/kms/kms-types";
|
||||
|
||||
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
|
||||
const keyDescriptionSchema = z.string().trim().max(500).optional();
|
||||
|
||||
const CmekSchema = KmsKeysSchema.merge(InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true })).omit({
|
||||
const CmekSchema = KmsKeysSchema.merge(
|
||||
InternalKmsSchema.pick({ version: true, encryptionAlgorithm: true, type: true })
|
||||
).omit({
|
||||
isReserved: true
|
||||
});
|
||||
|
||||
@@ -45,15 +49,45 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
description: "Create KMS key",
|
||||
body: z.object({
|
||||
body: z
|
||||
.object({
|
||||
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
||||
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
||||
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
||||
encryptionAlgorithm: z
|
||||
.nativeEnum(SymmetricEncryption)
|
||||
type: z
|
||||
.nativeEnum(KmsKeyIntent)
|
||||
.optional()
|
||||
.default(SymmetricEncryption.AES_GCM_256)
|
||||
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others
|
||||
.default(KmsKeyIntent.ENCRYPT_DECRYPT)
|
||||
.describe(KMS.CREATE_KEY.type),
|
||||
encryptionAlgorithm: z
|
||||
.enum(AllowedEncryptionKeyAlgorithms)
|
||||
.optional()
|
||||
.default(SymmetricKeyEncryptDecrypt.AES_GCM_256)
|
||||
.describe(KMS.CREATE_KEY.encryptionAlgorithm)
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (
|
||||
data.type === KmsKeyIntent.ENCRYPT_DECRYPT &&
|
||||
!Object.values(SymmetricKeyEncryptDecrypt).includes(data.encryptionAlgorithm as SymmetricKeyEncryptDecrypt)
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `encryptionAlgorithm must be a valid symmetric encryption algorithm. Valid options are: ${Object.values(
|
||||
SymmetricKeyEncryptDecrypt
|
||||
).join(", ")}`
|
||||
});
|
||||
}
|
||||
if (
|
||||
data.type === KmsKeyIntent.SIGN_VERIFY &&
|
||||
!Object.values(AsymmetricKeySignVerify).includes(data.encryptionAlgorithm as AsymmetricKeySignVerify)
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `encryptionAlgorithm must be a valid asymmetric sign-verify algorithm. Valid options are: ${Object.values(
|
||||
AsymmetricKeySignVerify
|
||||
).join(", ")}`
|
||||
});
|
||||
}
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -64,12 +98,19 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
body: { projectId, name, description, encryptionAlgorithm },
|
||||
body: { projectId, name, description, encryptionAlgorithm, type },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const cmek = await server.services.cmek.createCmek(
|
||||
{ orgId: permission.orgId, projectId, name, description, encryptionAlgorithm },
|
||||
{
|
||||
orgId: permission.orgId,
|
||||
projectId,
|
||||
name,
|
||||
description,
|
||||
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm,
|
||||
type
|
||||
},
|
||||
permission
|
||||
);
|
||||
|
||||
@@ -82,7 +123,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
keyId: cmek.id,
|
||||
name,
|
||||
description,
|
||||
encryptionAlgorithm
|
||||
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -366,6 +407,149 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/keys/:keyId/public-key",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get public key for a KMS key",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.GET_PUBLIC_KEY.keyId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
publicKey: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { keyId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const publicKey = await server.services.cmek.getPublicKey({ keyId }, permission);
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/keys/:keyId/signing-algorithms",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List signing algorithms for a KMS key",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.LIST_SIGNING_ALGORITHMS.keyId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
signingAlgorithms: z.array(z.nativeEnum(SigningAlgorithm))
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const result = await server.services.cmek.listSigningAlgorithms(
|
||||
{
|
||||
keyId: req.params.keyId
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/keys/:keyId/sign",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Sign data with KMS key",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.SIGN.keyId)
|
||||
}),
|
||||
body: z.object({
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
|
||||
data: z
|
||||
.string()
|
||||
.superRefine((data, ctx) => {
|
||||
if (!isBase64(data)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "data must be base64 encoded"
|
||||
});
|
||||
}
|
||||
})
|
||||
.describe(KMS.SIGN.data)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
signature: z.string(),
|
||||
keyId: z.string().uuid(),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { keyId: inputKeyId },
|
||||
body: { data, signingAlgorithm },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const result = await server.services.cmek.cmekSign({ keyId: inputKeyId, data, signingAlgorithm }, permission);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/keys/:keyId/verify",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Verify data with KMS key",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.VERIFY.keyId)
|
||||
}),
|
||||
body: z.object({
|
||||
data: z.string().describe(KMS.VERIFY.data),
|
||||
signature: z.string().describe(KMS.VERIFY.signature),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
signatureValid: z.boolean(),
|
||||
keyId: z.string().uuid(),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { keyId },
|
||||
body: { data, signature, signingAlgorithm },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const result = await server.services.cmek.cmekVerify({ keyId, data, signature, signingAlgorithm }, permission);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/keys/:keyId/decrypt",
|
||||
|
||||
@@ -3,12 +3,18 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import {
|
||||
TCmekDecryptDTO,
|
||||
TCmekEncryptDTO,
|
||||
TCmekGetPublicKeyDTO,
|
||||
TCmekKeyEncryptionAlgorithm,
|
||||
TCmekListSigningAlgorithmsDTO,
|
||||
TCmekSignDTO,
|
||||
TCmekVerifyDTO,
|
||||
TCreateCmekDTO,
|
||||
TListCmeksByProjectIdDTO,
|
||||
TUpdabteCmekByIdDTO
|
||||
@@ -16,6 +22,7 @@ import {
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { KmsKeyIntent } from "../kms/kms-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
|
||||
type TCmekServiceFactoryDep = {
|
||||
@@ -224,6 +231,142 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
return cipherTextBlob.toString("base64");
|
||||
};
|
||||
|
||||
const listSigningAlgorithms = async ({ keyId }: TCmekListSigningAlgorithmsDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: key.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
|
||||
if (key.type !== KmsKeyIntent.SIGN_VERIFY) {
|
||||
throw new BadRequestError({ message: `Key with ID '${keyId}' is not intended for signing` });
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = key.encryptionAlgorithm as TCmekKeyEncryptionAlgorithm;
|
||||
|
||||
const algos = [
|
||||
{
|
||||
keyAlgorithm: "rsa",
|
||||
signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) =>
|
||||
algorithm.toLowerCase().startsWith("rsa")
|
||||
)
|
||||
},
|
||||
{
|
||||
keyAlgorithm: "ecc",
|
||||
signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) =>
|
||||
algorithm.toLowerCase().startsWith("ecdsa")
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const selectedAlgorithm = algos.find((algo) => encryptionAlgorithm.toLowerCase().startsWith(algo.keyAlgorithm));
|
||||
|
||||
if (!selectedAlgorithm) {
|
||||
throw new BadRequestError({ message: `Unsupported encryption algorithm: ${encryptionAlgorithm}` });
|
||||
}
|
||||
|
||||
return { signingAlgorithms: selectedAlgorithm.signingAlgorithms };
|
||||
};
|
||||
|
||||
const getPublicKey = async ({ keyId }: TCmekGetPublicKeyDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: key.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
|
||||
const publicKey = await kmsService.getPublicKey({ kmsId: keyId });
|
||||
|
||||
return { publicKey };
|
||||
};
|
||||
|
||||
const cmekSign = async ({ keyId, data, signingAlgorithm }: TCmekSignDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: key.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Sign, ProjectPermissionSub.Cmek);
|
||||
|
||||
const sign = await kmsService.signWithKmsKey({ kmsId: keyId });
|
||||
|
||||
const { signature, algorithm } = await sign({ data: Buffer.from(data, "base64"), signingAlgorithm });
|
||||
|
||||
return {
|
||||
signature: signature.toString("base64"),
|
||||
keyId: key.id,
|
||||
signingAlgorithm: algorithm
|
||||
};
|
||||
};
|
||||
|
||||
const cmekVerify = async ({ keyId, data, signature, signingAlgorithm }: TCmekVerifyDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: key.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Verify, ProjectPermissionSub.Cmek);
|
||||
|
||||
const verify = await kmsService.verifyWithKmsKey({ kmsId: keyId, signingAlgorithm });
|
||||
|
||||
const { signatureValid, algorithm } = await verify({
|
||||
data: Buffer.from(data, "base64"),
|
||||
signature: Buffer.from(signature, "base64")
|
||||
});
|
||||
|
||||
return {
|
||||
signatureValid,
|
||||
keyId: key.id,
|
||||
signingAlgorithm: algorithm
|
||||
};
|
||||
};
|
||||
|
||||
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findById(keyId);
|
||||
|
||||
@@ -259,6 +402,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
cmekEncrypt,
|
||||
cmekDecrypt,
|
||||
findCmekById,
|
||||
findCmekByName
|
||||
findCmekByName,
|
||||
cmekSign,
|
||||
cmekVerify,
|
||||
listSigningAlgorithms,
|
||||
getPublicKey
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeySignVerify, SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
|
||||
import { KmsKeyIntent } from "../kms/kms-types";
|
||||
|
||||
export type TCmekKeyEncryptionAlgorithm = SymmetricKeyEncryptDecrypt | AsymmetricKeySignVerify;
|
||||
|
||||
export type TCreateCmekDTO = {
|
||||
orgId: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
encryptionAlgorithm: SymmetricEncryption;
|
||||
encryptionAlgorithm: TCmekKeyEncryptionAlgorithm;
|
||||
type: KmsKeyIntent;
|
||||
};
|
||||
|
||||
export type TUpdabteCmekByIdDTO = {
|
||||
@@ -38,3 +44,24 @@ export type TCmekDecryptDTO = {
|
||||
export enum CmekOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
export type TCmekListSigningAlgorithmsDTO = {
|
||||
keyId: string;
|
||||
};
|
||||
|
||||
export type TCmekGetPublicKeyDTO = {
|
||||
keyId: string;
|
||||
};
|
||||
|
||||
export type TCmekSignDTO = {
|
||||
keyId: string;
|
||||
data: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
};
|
||||
|
||||
export type TCmekVerifyDTO = {
|
||||
keyId: string;
|
||||
data: string;
|
||||
signature: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,55 @@
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeySignVerify } from "@app/lib/crypto/sign";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { KmsKeyIntent } from "./kms-types";
|
||||
|
||||
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
|
||||
export const getByteLengthForSymmetricEncryptionAlgorithm = (encryptionAlgorithm: SymmetricKeyEncryptDecrypt) => {
|
||||
switch (encryptionAlgorithm) {
|
||||
case SymmetricEncryption.AES_GCM_128:
|
||||
case SymmetricKeyEncryptDecrypt.AES_GCM_128:
|
||||
return 16;
|
||||
case SymmetricEncryption.AES_GCM_256:
|
||||
case SymmetricKeyEncryptDecrypt.AES_GCM_256:
|
||||
default:
|
||||
return 32;
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyKeyTypeAndAlgorithm = (
|
||||
type: KmsKeyIntent,
|
||||
algorithm: SymmetricKeyEncryptDecrypt | AsymmetricKeySignVerify,
|
||||
extra?: {
|
||||
forceType?: KmsKeyIntent;
|
||||
}
|
||||
) => {
|
||||
if (extra?.forceType && type !== extra.forceType) {
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported key type, expected ${extra.forceType} but got ${type}`
|
||||
});
|
||||
}
|
||||
|
||||
if (type === KmsKeyIntent.ENCRYPT_DECRYPT) {
|
||||
if (!Object.values(SymmetricKeyEncryptDecrypt).includes(algorithm as SymmetricKeyEncryptDecrypt)) {
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported encryption algorithm for encrypt/decrypt key: ${algorithm as string}`
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type === KmsKeyIntent.SIGN_VERIFY) {
|
||||
if (!Object.values(AsymmetricKeySignVerify).includes(algorithm as AsymmetricKeySignVerify)) {
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported sign/verify algorithm for sign/verify key: ${algorithm as string}`
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported key type: ${type as string}`
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
@@ -15,12 +17,17 @@ import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { TEnvConfig } from "@app/lib/config/env";
|
||||
import { randomSecureBytes } from "@app/lib/crypto";
|
||||
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { symmetricCipherService, SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
|
||||
import { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { AsymmetricKeySignVerify, signingService } from "@app/lib/crypto/sign";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { getByteLengthForAlgorithm, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns";
|
||||
import {
|
||||
getByteLengthForSymmetricEncryptionAlgorithm,
|
||||
KMS_ROOT_CONFIG_UUID,
|
||||
verifyKeyTypeAndAlgorithm
|
||||
} from "@app/services/kms/kms-fns";
|
||||
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
@@ -29,6 +36,7 @@ import { TKmsKeyDALFactory } from "./kms-key-dal";
|
||||
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
||||
import {
|
||||
KmsDataKey,
|
||||
KmsKeyIntent,
|
||||
KmsType,
|
||||
RootKeyEncryptionStrategy,
|
||||
TDecryptWithKeyDTO,
|
||||
@@ -38,8 +46,11 @@ import {
|
||||
TEncryptWithKmsDTO,
|
||||
TGenerateKMSDTO,
|
||||
TGetKeyMaterialDTO,
|
||||
TGetPublicKeyDTO,
|
||||
TImportKeyMaterialDTO,
|
||||
TUpdateProjectSecretManagerKmsKeyDTO
|
||||
TSignWithKmsDTO,
|
||||
TUpdateProjectSecretManagerKmsKeyDTO,
|
||||
TVerifyWithKmsDTO
|
||||
} from "./kms-types";
|
||||
|
||||
type TKmsServiceFactoryDep = {
|
||||
@@ -83,19 +94,42 @@ export const kmsServiceFactory = ({
|
||||
tx,
|
||||
name,
|
||||
projectId,
|
||||
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
|
||||
encryptionAlgorithm = SymmetricKeyEncryptDecrypt.AES_GCM_256,
|
||||
type = KmsKeyIntent.ENCRYPT_DECRYPT,
|
||||
description
|
||||
}: TGenerateKMSDTO) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
// daniel: ensure that the key type (sign/encrypt) and the encryption algorithm are compatible.
|
||||
verifyKeyTypeAndAlgorithm(type, encryptionAlgorithm);
|
||||
|
||||
const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm));
|
||||
let kmsKeyMaterial: Buffer | null = null;
|
||||
if (type === KmsKeyIntent.ENCRYPT_DECRYPT) {
|
||||
kmsKeyMaterial = randomSecureBytes(
|
||||
getByteLengthForSymmetricEncryptionAlgorithm(encryptionAlgorithm as SymmetricKeyEncryptDecrypt)
|
||||
);
|
||||
} else if (type === KmsKeyIntent.SIGN_VERIFY) {
|
||||
const { generateAsymmetricPrivateKey, getPublicKeyFromPrivateKey } = signingService(
|
||||
encryptionAlgorithm as AsymmetricKeySignVerify
|
||||
);
|
||||
kmsKeyMaterial = await generateAsymmetricPrivateKey();
|
||||
|
||||
// daniel: safety check to ensure we're able to extract the public key from the private key before we proceed to key creation
|
||||
getPublicKeyFromPrivateKey(kmsKeyMaterial);
|
||||
}
|
||||
|
||||
if (!kmsKeyMaterial) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid KMS key type. No key material was created for key type '${type}' using algorithm '${encryptionAlgorithm}'`
|
||||
});
|
||||
}
|
||||
|
||||
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
||||
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
const dbQuery = async (db: Knex) => {
|
||||
const kmsDoc = await kmsDAL.create(
|
||||
{
|
||||
name: sanitizedName,
|
||||
type,
|
||||
orgId,
|
||||
isReserved,
|
||||
projectId,
|
||||
@@ -115,6 +149,7 @@ export const kmsServiceFactory = ({
|
||||
);
|
||||
return kmsDoc;
|
||||
};
|
||||
|
||||
if (tx) return dbQuery(tx);
|
||||
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
|
||||
return doc;
|
||||
@@ -134,7 +169,7 @@ export const kmsServiceFactory = ({
|
||||
*/
|
||||
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
|
||||
// Buffer#1 encrypted text + Buffer#2 version number
|
||||
@@ -149,7 +184,7 @@ export const kmsServiceFactory = ({
|
||||
* This can be even later exposed directly as api for encryption as function
|
||||
*/
|
||||
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
|
||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
@@ -227,7 +262,7 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
|
||||
const encryptWithRootKey = () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
|
||||
return (plainTextBuffer: Buffer) => {
|
||||
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||
@@ -236,7 +271,7 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
|
||||
const decryptWithRootKey = () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
|
||||
return (cipherTextBuffer: Buffer) => {
|
||||
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||
@@ -255,6 +290,11 @@ export const kmsServiceFactory = ({
|
||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyEncryptDecrypt;
|
||||
verifyKeyTypeAndAlgorithm(kmsDoc.type as KmsKeyIntent, encryptionAlgorithm, {
|
||||
forceType: KmsKeyIntent.ENCRYPT_DECRYPT
|
||||
});
|
||||
|
||||
if (kmsDoc.externalKms) {
|
||||
let externalKms: TExternalKmsProviderFns;
|
||||
|
||||
@@ -316,8 +356,8 @@ export const kmsServiceFactory = ({
|
||||
}
|
||||
|
||||
// internal KMS
|
||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
@@ -345,19 +385,22 @@ export const kmsServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return kmsKey;
|
||||
};
|
||||
|
||||
const importKeyMaterial = async (
|
||||
{ key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO,
|
||||
{ key, algorithm, name, isReserved, projectId, orgId, type }: TImportKeyMaterialDTO,
|
||||
tx?: Knex
|
||||
) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
// daniel: currently we only support imports for encrypt/decrypt keys
|
||||
verifyKeyTypeAndAlgorithm(type, algorithm, { forceType: KmsKeyIntent.ENCRYPT_DECRYPT });
|
||||
|
||||
const expectedByteLength = getByteLengthForAlgorithm(algorithm);
|
||||
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
|
||||
const expectedByteLength = getByteLengthForSymmetricEncryptionAlgorithm(algorithm as SymmetricKeyEncryptDecrypt);
|
||||
if (key.byteLength !== expectedByteLength) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
|
||||
@@ -370,6 +413,7 @@ export const kmsServiceFactory = ({
|
||||
const kmsDoc = await kmsDAL.create(
|
||||
{
|
||||
name: sanitizedName,
|
||||
type: KmsKeyIntent.ENCRYPT_DECRYPT,
|
||||
orgId,
|
||||
isReserved,
|
||||
projectId
|
||||
@@ -393,12 +437,89 @@ export const kmsServiceFactory = ({
|
||||
return doc;
|
||||
};
|
||||
|
||||
const getPublicKey = async ({ kmsId }: TGetPublicKeyDTO) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeySignVerify;
|
||||
|
||||
verifyKeyTypeAndAlgorithm(kmsDoc.type as KmsKeyIntent, encryptionAlgorithm, {
|
||||
forceType: KmsKeyIntent.SIGN_VERIFY
|
||||
});
|
||||
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
const publicKeyBuffer = signingService(encryptionAlgorithm).getPublicKeyFromPrivateKey(kmsKey);
|
||||
|
||||
return crypto
|
||||
.createPublicKey({
|
||||
key: publicKeyBuffer,
|
||||
format: "pem",
|
||||
type: "spki"
|
||||
})
|
||||
.export({ type: "spki", format: "pem" }) as string; // format 'pem' makes it a string
|
||||
};
|
||||
|
||||
const signWithKmsKey = async ({ kmsId }: Pick<TSignWithKmsDTO, "kmsId">) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeySignVerify;
|
||||
verifyKeyTypeAndAlgorithm(kmsDoc.type as KmsKeyIntent, encryptionAlgorithm, {
|
||||
forceType: KmsKeyIntent.SIGN_VERIFY
|
||||
});
|
||||
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
const { sign } = signingService(encryptionAlgorithm);
|
||||
return ({ data, signingAlgorithm }: Pick<TSignWithKmsDTO, "data" | "signingAlgorithm">) => {
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
const signature = sign(data, kmsKey, signingAlgorithm);
|
||||
|
||||
return Promise.resolve({ signature, algorithm: signingAlgorithm });
|
||||
};
|
||||
};
|
||||
|
||||
const verifyWithKmsKey = async ({
|
||||
kmsId,
|
||||
signingAlgorithm
|
||||
}: Pick<TVerifyWithKmsDTO, "kmsId" | "signingAlgorithm">) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeySignVerify;
|
||||
verifyKeyTypeAndAlgorithm(kmsDoc.type as KmsKeyIntent, encryptionAlgorithm, {
|
||||
forceType: KmsKeyIntent.SIGN_VERIFY
|
||||
});
|
||||
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
const { verify, getPublicKeyFromPrivateKey } = signingService(encryptionAlgorithm);
|
||||
return ({ data, signature }: Pick<TVerifyWithKmsDTO, "data" | "signature">) => {
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
const publicKey = getPublicKeyFromPrivateKey(kmsKey);
|
||||
const signatureValid = verify(data, signature, publicKey, signingAlgorithm);
|
||||
return Promise.resolve({ signatureValid, algorithm: signingAlgorithm });
|
||||
};
|
||||
};
|
||||
|
||||
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyEncryptDecrypt;
|
||||
verifyKeyTypeAndAlgorithm(kmsDoc.type as KmsKeyIntent, encryptionAlgorithm, {
|
||||
forceType: KmsKeyIntent.ENCRYPT_DECRYPT
|
||||
});
|
||||
|
||||
if (kmsDoc.externalKms) {
|
||||
let externalKms: TExternalKmsProviderFns;
|
||||
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
|
||||
@@ -454,8 +575,8 @@ export const kmsServiceFactory = ({
|
||||
}
|
||||
|
||||
// internal KMS
|
||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
|
||||
@@ -729,7 +850,7 @@ export const kmsServiceFactory = ({
|
||||
|
||||
// case 2: root key is encrypted with software encryption
|
||||
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||
|
||||
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
||||
@@ -749,7 +870,7 @@ export const kmsServiceFactory = ({
|
||||
}
|
||||
|
||||
if (strategy === RootKeyEncryptionStrategy.Software) {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||
|
||||
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
|
||||
@@ -765,7 +886,7 @@ export const kmsServiceFactory = ({
|
||||
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
|
||||
const dataKey = await $getDataKey(encryptionContext, trx);
|
||||
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyEncryptDecrypt.AES_GCM_256);
|
||||
|
||||
return {
|
||||
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
@@ -966,6 +1087,7 @@ export const kmsServiceFactory = ({
|
||||
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
|
||||
|
||||
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||
|
||||
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||
};
|
||||
|
||||
@@ -1014,6 +1136,9 @@ export const kmsServiceFactory = ({
|
||||
getKmsById,
|
||||
createCipherPairWithDataKey,
|
||||
getKeyMaterial,
|
||||
importKeyMaterial
|
||||
importKeyMaterial,
|
||||
signWithKmsKey,
|
||||
verifyWithKmsKey,
|
||||
getPublicKey
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyEncryptDecrypt } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeySignVerify, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
||||
|
||||
export enum KmsDataKey {
|
||||
Organization,
|
||||
@@ -13,6 +14,11 @@ export enum KmsType {
|
||||
Internal = "internal"
|
||||
}
|
||||
|
||||
export enum KmsKeyIntent {
|
||||
ENCRYPT_DECRYPT = "encrypt-decrypt",
|
||||
SIGN_VERIFY = "sign-verify"
|
||||
}
|
||||
|
||||
export type TEncryptWithKmsDataKeyDTO =
|
||||
| { type: KmsDataKey.Organization; orgId: string }
|
||||
| { type: KmsDataKey.SecretManager; projectId: string };
|
||||
@@ -25,7 +31,8 @@ export type TEncryptWithKmsDataKeyDTO =
|
||||
export type TGenerateKMSDTO = {
|
||||
orgId: string;
|
||||
projectId?: string;
|
||||
encryptionAlgorithm?: SymmetricEncryption;
|
||||
encryptionAlgorithm?: SymmetricKeyEncryptDecrypt | AsymmetricKeySignVerify;
|
||||
type?: KmsKeyIntent;
|
||||
isReserved?: boolean;
|
||||
name?: string;
|
||||
description?: string;
|
||||
@@ -37,6 +44,23 @@ export type TEncryptWithKmsDTO = {
|
||||
plainText: Buffer;
|
||||
};
|
||||
|
||||
export type TGetPublicKeyDTO = {
|
||||
kmsId: string;
|
||||
};
|
||||
|
||||
export type TSignWithKmsDTO = {
|
||||
kmsId: string;
|
||||
data: Buffer;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
};
|
||||
|
||||
export type TVerifyWithKmsDTO = {
|
||||
kmsId: string;
|
||||
data: Buffer;
|
||||
signature: Buffer;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
};
|
||||
|
||||
export type TEncryptionWithKeyDTO = {
|
||||
key: Buffer;
|
||||
plainText: Buffer;
|
||||
@@ -67,9 +91,10 @@ export type TGetKeyMaterialDTO = {
|
||||
|
||||
export type TImportKeyMaterialDTO = {
|
||||
key: Buffer;
|
||||
algorithm: SymmetricEncryption;
|
||||
algorithm: SymmetricKeyEncryptDecrypt;
|
||||
name?: string;
|
||||
isReserved: boolean;
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
type: KmsKeyIntent;
|
||||
};
|
||||
|
||||
@@ -30,7 +30,9 @@ export enum ProjectPermissionCmekActions {
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Encrypt = "encrypt",
|
||||
Decrypt = "decrypt"
|
||||
Decrypt = "decrypt",
|
||||
Sign = "sign",
|
||||
Verify = "verify"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionKmipActions {
|
||||
|
||||
31
frontend/src/helpers/kms.ts
Normal file
31
frontend/src/helpers/kms.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
AsymmetricKeySignVerify,
|
||||
KmsKeyIntent,
|
||||
SymmetricKeyEncryptDecrypt
|
||||
} from "@app/hooks/api/cmeks";
|
||||
|
||||
export const kmsKeyUsageOptions: Record<
|
||||
KmsKeyIntent,
|
||||
{
|
||||
label: string;
|
||||
tooltip: string;
|
||||
}
|
||||
> = {
|
||||
[KmsKeyIntent.ENCRYPT_DECRYPT]: {
|
||||
label: "Encrypt/Decrypt",
|
||||
tooltip: "Use the key only to encrypt and decrypt data."
|
||||
},
|
||||
[KmsKeyIntent.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<
|
||||
KmsKeyIntent,
|
||||
SymmetricKeyEncryptDecrypt | AsymmetricKeySignVerify
|
||||
> = {
|
||||
[KmsKeyIntent.ENCRYPT_DECRYPT]: SymmetricKeyEncryptDecrypt.AES_GCM_256,
|
||||
[KmsKeyIntent.SIGN_VERIFY]: AsymmetricKeySignVerify.RSA_4096
|
||||
};
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
TCmekDecryptResponse,
|
||||
TCmekEncrypt,
|
||||
TCmekEncryptResponse,
|
||||
TCmekSign,
|
||||
TCmekSignResponse,
|
||||
TCmekVerify,
|
||||
TCmekVerifyResponse,
|
||||
TCreateCmek,
|
||||
TDeleteCmek,
|
||||
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 = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => {
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
|
||||
export enum KmsKeyIntent {
|
||||
ENCRYPT_DECRYPT = "encrypt-decrypt",
|
||||
SIGN_VERIFY = "sign-verify"
|
||||
}
|
||||
|
||||
export type TCmek = {
|
||||
id: string;
|
||||
type: KmsKeyIntent;
|
||||
name: string;
|
||||
description?: string;
|
||||
encryptionAlgorithm: EncryptionAlgorithm;
|
||||
encryptionAlgorithm: AsymmetricKeySignVerify | SymmetricKeyEncryptDecrypt;
|
||||
projectId: string;
|
||||
isDisabled: boolean;
|
||||
isReserved: boolean;
|
||||
@@ -17,7 +25,8 @@ export type TCmek = {
|
||||
type ProjectRef = { projectId: string };
|
||||
type KeyRef = { keyId: string };
|
||||
|
||||
export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm"> & ProjectRef;
|
||||
export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm" | "type"> &
|
||||
ProjectRef;
|
||||
export type TUpdateCmek = KeyRef &
|
||||
Partial<Pick<TCmek, "name" | "description" | "isDisabled">> &
|
||||
ProjectRef;
|
||||
@@ -26,6 +35,13 @@ export type TDeleteCmek = KeyRef & ProjectRef;
|
||||
export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean };
|
||||
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 = {
|
||||
keys: TCmek[];
|
||||
totalCount: number;
|
||||
@@ -44,6 +60,18 @@ export type TCmekEncryptResponse = {
|
||||
ciphertext: string;
|
||||
};
|
||||
|
||||
export type TCmekSignResponse = {
|
||||
signature: string;
|
||||
keyId: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
};
|
||||
|
||||
export type TCmekVerifyResponse = {
|
||||
signatureValid: boolean;
|
||||
keyId: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
};
|
||||
|
||||
export type TCmekDecryptResponse = {
|
||||
plaintext: string;
|
||||
};
|
||||
@@ -52,7 +80,35 @@ export enum CmekOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
export enum EncryptionAlgorithm {
|
||||
export enum AsymmetricKeySignVerify {
|
||||
RSA_4096 = "rsa-4096",
|
||||
ECC_NIST_P256 = "ecc-nist-p256"
|
||||
}
|
||||
|
||||
// Supported symmetric encrypt/decrypt algorithms
|
||||
export enum SymmetricKeyEncryptDecrypt {
|
||||
AES_GCM_256 = "aes-256-gcm",
|
||||
AES_GCM_128 = "aes-128-gcm"
|
||||
}
|
||||
|
||||
export const AllowedEncryptionKeyAlgorithms = z.enum([
|
||||
...Object.values(SymmetricKeyEncryptDecrypt),
|
||||
...Object.values(AsymmetricKeySignVerify)
|
||||
] 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
frontend/src/lib/fn/base64.ts
Normal file
14
frontend/src/lib/fn/base64.ts
Normal 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
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { EncryptionAlgorithm, TCmek, useCreateCmek, useUpdateCmek } from "@app/hooks/api/cmeks";
|
||||
import { keyUsageDefaultOption, kmsKeyUsageOptions } from "@app/helpers/kms";
|
||||
import {
|
||||
AllowedEncryptionKeyAlgorithms,
|
||||
AsymmetricKeySignVerify,
|
||||
KmsKeyIntent,
|
||||
SymmetricKeyEncryptDecrypt,
|
||||
TCmek,
|
||||
useCreateCmek,
|
||||
useUpdateCmek
|
||||
} from "@app/hooks/api/cmeks";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: slugSchema({ min: 1, max: 32, field: "Name" }),
|
||||
description: z.string().max(500).optional(),
|
||||
encryptionAlgorithm: z.nativeEnum(EncryptionAlgorithm)
|
||||
encryptionAlgorithm: z.enum(AllowedEncryptionKeyAlgorithms),
|
||||
type: z.nativeEnum(KmsKeyIntent)
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof formSchema>;
|
||||
@@ -47,24 +57,30 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { isSubmitting, errors }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: cmek?.name,
|
||||
description: cmek?.description,
|
||||
encryptionAlgorithm: EncryptionAlgorithm.AES_GCM_256
|
||||
encryptionAlgorithm: SymmetricKeyEncryptDecrypt.AES_GCM_256,
|
||||
type: KmsKeyIntent.ENCRYPT_DECRYPT
|
||||
}
|
||||
});
|
||||
|
||||
const handleCreateCmek = async ({ encryptionAlgorithm, name, description }: FormData) => {
|
||||
const handleCreateCmek = async ({ encryptionAlgorithm, name, description, type }: FormData) => {
|
||||
const mutation = isUpdate
|
||||
? updateCmek.mutateAsync({ keyId: cmek.id, projectId, name, description })
|
||||
: createCmek.mutateAsync({
|
||||
projectId,
|
||||
encryptionAlgorithm,
|
||||
name,
|
||||
description
|
||||
description,
|
||||
type,
|
||||
encryptionAlgorithm: encryptionAlgorithm as
|
||||
| AsymmetricKeySignVerify
|
||||
| SymmetricKeyEncryptDecrypt
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -83,6 +99,8 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const selectedType = watch("type");
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateCmek)}>
|
||||
<FormControl
|
||||
@@ -93,23 +111,97 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
|
||||
>
|
||||
<Input autoFocus placeholder="my-secret-key" {...register("name")} />
|
||||
</FormControl>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{!isUpdate && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="encryptionAlgorithm"
|
||||
name="type"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Algorithm" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select defaultValue={field.value} onValueChange={onChange} className="w-full">
|
||||
{Object.entries(EncryptionAlgorithm)?.map(([key, value]) => (
|
||||
<SelectItem value={value} key={`source-environment-${key}`}>
|
||||
{key.replaceAll("_", "-")}
|
||||
<FormControl
|
||||
className="w-full"
|
||||
tooltipText={
|
||||
<div className="space-y-4">
|
||||
{Object.entries(KmsKeyIntent).map(([key, value]) => (
|
||||
<div key={`key-usage-${key}`}>
|
||||
<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 KmsKeyIntent]) {
|
||||
setValue("encryptionAlgorithm", keyUsageDefaultOption[e as KmsKeyIntent], {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true
|
||||
});
|
||||
}
|
||||
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{Object.entries(KmsKeyIntent)?.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 (selectedType === KmsKeyIntent.ENCRYPT_DECRYPT) {
|
||||
return Object.values(SymmetricKeyEncryptDecrypt).includes(
|
||||
value as unknown as SymmetricKeyEncryptDecrypt
|
||||
);
|
||||
}
|
||||
if (selectedType === KmsKeyIntent.SIGN_VERIFY) {
|
||||
return Object.values(AsymmetricKeySignVerify).includes(
|
||||
value as unknown as AsymmetricKeySignVerify
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
label="Description (optional)"
|
||||
errorText={errors.description?.message}
|
||||
|
||||
195
frontend/src/pages/kms/OverviewPage/components/CmekSignModal.tsx
Normal file
195
frontend/src/pages/kms/OverviewPage/components/CmekSignModal.tsx
Normal file
@@ -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,
|
||||
faEdit,
|
||||
faEllipsis,
|
||||
faFileSignature,
|
||||
faInfoCircle,
|
||||
faKey,
|
||||
faLock,
|
||||
@@ -51,14 +52,17 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { kmsKeyUsageOptions } from "@app/helpers/kms";
|
||||
import { usePagination, usePopUp, useResetPageHelper, useTimedReset } from "@app/hooks";
|
||||
import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks";
|
||||
import { CmekOrderBy, TCmek } from "@app/hooks/api/cmeks/types";
|
||||
import { CmekOrderBy, KmsKeyIntent, TCmek } from "@app/hooks/api/cmeks/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
|
||||
import { CmekDecryptModal } from "./CmekDecryptModal";
|
||||
import { CmekEncryptModal } from "./CmekEncryptModal";
|
||||
import { CmekModal } from "./CmekModal";
|
||||
import { CmekSignModal } from "./CmekSignModal";
|
||||
import { CmekVerifyModal } from "./CmekVerifyModal";
|
||||
import { DeleteCmekModal } from "./DeleteCmekModal";
|
||||
|
||||
const getStatusBadgeProps = (
|
||||
@@ -123,7 +127,9 @@ export const CmekTable = () => {
|
||||
"upsertKey",
|
||||
"deleteKey",
|
||||
"encryptData",
|
||||
"decryptData"
|
||||
"decryptData",
|
||||
"signData",
|
||||
"verifyData"
|
||||
] as const);
|
||||
|
||||
const handleSort = () => {
|
||||
@@ -179,6 +185,15 @@ export const CmekTable = () => {
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
||||
const cannotSignData = permission.cannot(
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
||||
const cannotVerifyData = permission.cannot(
|
||||
ProjectPermissionCmekActions.Verify,
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
return (
|
||||
<motion.div
|
||||
key="kms-keys-tab"
|
||||
@@ -246,6 +261,7 @@ export const CmekTable = () => {
|
||||
</div>
|
||||
</Th>
|
||||
<Th>Key ID</Th>
|
||||
<Th>Key Usage</Th>
|
||||
<Th>Algorithm</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Version</Th>
|
||||
@@ -257,7 +273,8 @@ export const CmekTable = () => {
|
||||
{!isPending &&
|
||||
keys.length > 0 &&
|
||||
keys.map((cmek) => {
|
||||
const { name, id, version, description, encryptionAlgorithm, isDisabled } = cmek;
|
||||
const { name, id, version, description, encryptionAlgorithm, isDisabled, type } =
|
||||
cmek;
|
||||
const { variant, label } = getStatusBadgeProps(isDisabled);
|
||||
|
||||
return (
|
||||
@@ -295,6 +312,14 @@ export const CmekTable = () => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
{kmsKeyUsageOptions[type].label}
|
||||
<Tooltip content={kmsKeyUsageOptions[type].tooltip}>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="uppercase">{encryptionAlgorithm}</Td>
|
||||
<Td>
|
||||
<Badge variant={variant}>{label}</Badge>
|
||||
@@ -314,6 +339,8 @@ export const CmekTable = () => {
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[160px]">
|
||||
{type === KmsKeyIntent.ENCRYPT_DECRYPT && (
|
||||
<>
|
||||
<Tooltip
|
||||
content={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
@@ -358,6 +385,58 @@ export const CmekTable = () => {
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === KmsKeyIntent.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
|
||||
content={cannotEditKey ? "Access Restricted" : ""}
|
||||
position="left"
|
||||
@@ -456,6 +535,16 @@ export const CmekTable = () => {
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("decryptData", isOpen)}
|
||||
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>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
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 handleSignData = async (formData: FormData) => {
|
||||
try {
|
||||
await cmekVerify.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 = 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(handleSignData)}>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
@@ -45,7 +45,9 @@ const CmekPolicyActionSchema = z.object({
|
||||
delete: z.boolean().optional(),
|
||||
create: 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({
|
||||
@@ -421,6 +423,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
const canCreate = action.includes(ProjectPermissionCmekActions.Create);
|
||||
const canEncrypt = action.includes(ProjectPermissionCmekActions.Encrypt);
|
||||
const canDecrypt = action.includes(ProjectPermissionCmekActions.Decrypt);
|
||||
const canSign = action.includes(ProjectPermissionCmekActions.Sign);
|
||||
const canVerify = action.includes(ProjectPermissionCmekActions.Verify);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
@@ -431,6 +435,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
if (canDelete) formVal[subject]![0].delete = true;
|
||||
if (canEncrypt) formVal[subject]![0].encrypt = true;
|
||||
if (canDecrypt) formVal[subject]![0].decrypt = true;
|
||||
if (canSign) formVal[subject]![0].sign = true;
|
||||
if (canVerify) formVal[subject]![0].verify = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -683,7 +689,9 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" },
|
||||
{ label: "Encrypt", value: "encrypt" },
|
||||
{ label: "Decrypt", value: "decrypt" }
|
||||
{ label: "Decrypt", value: "decrypt" },
|
||||
{ label: "Sign", value: "sign" },
|
||||
{ label: "Verify", value: "verify" }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.Kms]: {
|
||||
|
||||
Reference in New Issue
Block a user