mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 06:26:27 +00:00
Merge branch 'Infisical:main' into github-username
This commit is contained in:
+2
-1
@@ -8,7 +8,8 @@ RUN apt-get update && apt-get install -y \
|
|||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
openssh-client
|
openssh-client \
|
||||||
|
openssl
|
||||||
|
|
||||||
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||||
RUN apt-get install -y \
|
RUN apt-get install -y \
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
|
openssl \
|
||||||
curl \
|
curl \
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|
||||||
|
|||||||
Generated
+9
-13
@@ -132,7 +132,7 @@
|
|||||||
"@types/jsrp": "^0.2.6",
|
"@types/jsrp": "^0.2.6",
|
||||||
"@types/libsodium-wrappers": "^0.7.13",
|
"@types/libsodium-wrappers": "^0.7.13",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.9.5",
|
"@types/node": "^20.17.30",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/passport-github": "^1.1.12",
|
"@types/passport-github": "^1.1.12",
|
||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
@@ -9753,11 +9753,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.9.5",
|
"version": "20.17.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
||||||
"integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==",
|
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node-fetch": {
|
"node_modules/@types/node-fetch": {
|
||||||
@@ -20081,11 +20082,6 @@
|
|||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/scim-patch/node_modules/undici-types": {
|
|
||||||
"version": "6.19.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
|
||||||
},
|
|
||||||
"node_modules/scim2-parse-filter": {
|
"node_modules/scim2-parse-filter": {
|
||||||
"version": "0.2.10",
|
"version": "0.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
||||||
@@ -22442,9 +22438,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
},
|
},
|
||||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
"@types/jsrp": "^0.2.6",
|
"@types/jsrp": "^0.2.6",
|
||||||
"@types/libsodium-wrappers": "^0.7.13",
|
"@types/libsodium-wrappers": "^0.7.13",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.9.5",
|
"@types/node": "^20.17.30",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/passport-github": "^1.1.12",
|
"@types/passport-github": "^1.1.12",
|
||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasTypeColumn = await knex.schema.hasColumn(TableName.KmsKey, "type");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||||
|
if (!hasTypeColumn) t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||||
|
t.dropColumn("keyUsage");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@ export const KmsKeysSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
projectId: z.string().nullable().optional()
|
projectId: z.string().nullable().optional(),
|
||||||
|
keyUsage: z.string().default("encrypt-decrypt")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import z from "zod";
|
|||||||
|
|
||||||
import { KmsKeysSchema } from "@app/db/schemas";
|
import { KmsKeysSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -74,7 +74,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
description: "KMIP endpoint for creating managed objects",
|
description: "KMIP endpoint for creating managed objects",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: KmsKeysSchema
|
200: KmsKeysSchema
|
||||||
@@ -433,7 +433,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||||
@@ -255,6 +256,11 @@ export enum EventType {
|
|||||||
GET_CMEK = "get-cmek",
|
GET_CMEK = "get-cmek",
|
||||||
CMEK_ENCRYPT = "cmek-encrypt",
|
CMEK_ENCRYPT = "cmek-encrypt",
|
||||||
CMEK_DECRYPT = "cmek-decrypt",
|
CMEK_DECRYPT = "cmek-decrypt",
|
||||||
|
CMEK_SIGN = "cmek-sign",
|
||||||
|
CMEK_VERIFY = "cmek-verify",
|
||||||
|
CMEK_LIST_SIGNING_ALGORITHMS = "cmek-list-signing-algorithms",
|
||||||
|
CMEK_GET_PUBLIC_KEY = "cmek-get-public-key",
|
||||||
|
|
||||||
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||||
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
||||||
GET_PROJECT_TEMPLATES = "get-project-templates",
|
GET_PROJECT_TEMPLATES = "get-project-templates",
|
||||||
@@ -1997,7 +2003,7 @@ interface CreateCmekEvent {
|
|||||||
keyId: string;
|
keyId: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
encryptionAlgorithm: SymmetricEncryption;
|
encryptionAlgorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2045,6 +2051,39 @@ interface CmekDecryptEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CmekSignEvent {
|
||||||
|
type: EventType.CMEK_SIGN;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CmekVerifyEvent {
|
||||||
|
type: EventType.CMEK_VERIFY;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
signature: string;
|
||||||
|
signatureValid: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CmekListSigningAlgorithmsEvent {
|
||||||
|
type: EventType.CMEK_LIST_SIGNING_ALGORITHMS;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CmekGetPublicKeyEvent {
|
||||||
|
type: EventType.CMEK_GET_PUBLIC_KEY;
|
||||||
|
metadata: {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface GetExternalGroupOrgRoleMappingsEvent {
|
interface GetExternalGroupOrgRoleMappingsEvent {
|
||||||
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
||||||
metadata?: Record<string, never>; // not needed, based off orgId
|
metadata?: Record<string, never>; // not needed, based off orgId
|
||||||
@@ -2639,6 +2678,10 @@ export type Event =
|
|||||||
| GetCmeksEvent
|
| GetCmeksEvent
|
||||||
| CmekEncryptEvent
|
| CmekEncryptEvent
|
||||||
| CmekDecryptEvent
|
| CmekDecryptEvent
|
||||||
|
| CmekSignEvent
|
||||||
|
| CmekVerifyEvent
|
||||||
|
| CmekListSigningAlgorithmsEvent
|
||||||
|
| CmekGetPublicKeyEvent
|
||||||
| GetExternalGroupOrgRoleMappingsEvent
|
| GetExternalGroupOrgRoleMappingsEvent
|
||||||
| UpdateExternalGroupOrgRoleMappingsEvent
|
| UpdateExternalGroupOrgRoleMappingsEvent
|
||||||
| GetProjectTemplatesEvent
|
| GetProjectTemplatesEvent
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/er
|
|||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey, KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
@@ -115,6 +115,7 @@ export const externalKmsServiceFactory = ({
|
|||||||
{
|
{
|
||||||
isReserved: false,
|
isReserved: false,
|
||||||
description,
|
description,
|
||||||
|
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||||
name: kmsName,
|
name: kmsName,
|
||||||
orgId: actorOrgId
|
orgId: actorOrgId
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
|||||||
plaintext: data
|
plaintext: data
|
||||||
});
|
});
|
||||||
if (!encryptedText[0].ciphertext) throw new Error("encryption failed");
|
if (!encryptedText[0].ciphertext) throw new Error("encryption failed");
|
||||||
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext) };
|
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext as Uint8Array) };
|
||||||
};
|
};
|
||||||
|
|
||||||
const decrypt = async (encryptedBlob: Buffer) => {
|
const decrypt = async (encryptedBlob: Buffer) => {
|
||||||
@@ -101,7 +101,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
|||||||
ciphertext: encryptedBlob
|
ciphertext: encryptedBlob
|
||||||
});
|
});
|
||||||
if (!decryptedText[0].plaintext) throw new Error("decryption failed");
|
if (!decryptedText[0].plaintext) throw new Error("decryption failed");
|
||||||
return { data: Buffer.from(decryptedText[0].plaintext) };
|
return { data: Buffer.from(decryptedText[0].plaintext as Uint8Array) };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
|||||||
const decrypt: {
|
const decrypt: {
|
||||||
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
||||||
(encryptedBlob: Buffer): Promise<Buffer>;
|
(encryptedBlob: Buffer): Promise<Buffer>;
|
||||||
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
|
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle): Promise<Buffer> => {
|
||||||
if (!pkcs11 || !isInitialized) {
|
if (!pkcs11 || !isInitialized) {
|
||||||
throw new Error("PKCS#11 module is not initialized");
|
throw new Error("PKCS#11 module is not initialized");
|
||||||
}
|
}
|
||||||
@@ -309,10 +309,10 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
|||||||
|
|
||||||
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
|
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
|
||||||
|
|
||||||
const tempBuffer = Buffer.alloc(encryptedData.length);
|
const tempBuffer: Buffer = Buffer.alloc(encryptedData.length);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
|
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
|
||||||
|
|
||||||
// Create a new buffer from the decrypted data
|
|
||||||
return Buffer.from(decryptedData);
|
return Buffer.from(decryptedData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error, "HSM: Failed to perform decryption");
|
logger.error(error, "HSM: Failed to perform decryption");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
@@ -403,6 +404,7 @@ export const kmipOperationServiceFactory = ({
|
|||||||
algorithm,
|
algorithm,
|
||||||
isReserved: false,
|
isReserved: false,
|
||||||
projectId,
|
projectId,
|
||||||
|
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||||
orgId: project.orgId
|
orgId: project.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ type KmipOperationBaseDTO = {
|
|||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TKmipCreateDTO = {
|
export type TKmipCreateDTO = {
|
||||||
algorithm: SymmetricEncryption;
|
algorithm: SymmetricKeyAlgorithm;
|
||||||
} & KmipOperationBaseDTO;
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
export type TKmipGetDTO = {
|
export type TKmipGetDTO = {
|
||||||
@@ -77,7 +77,7 @@ export type TKmipLocateDTO = KmipOperationBaseDTO;
|
|||||||
export type TKmipRegisterDTO = {
|
export type TKmipRegisterDTO = {
|
||||||
name: string;
|
name: string;
|
||||||
key: string;
|
key: string;
|
||||||
algorithm: SymmetricEncryption;
|
algorithm: SymmetricKeyAlgorithm;
|
||||||
} & KmipOperationBaseDTO;
|
} & KmipOperationBaseDTO;
|
||||||
|
|
||||||
export type TSetupOrgKmipDTO = {
|
export type TSetupOrgKmipDTO = {
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export enum ProjectPermissionCmekActions {
|
|||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
Encrypt = "encrypt",
|
Encrypt = "encrypt",
|
||||||
Decrypt = "decrypt"
|
Decrypt = "decrypt",
|
||||||
|
Sign = "sign",
|
||||||
|
Verify = "verify"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionDynamicSecretActions {
|
export enum ProjectPermissionDynamicSecretActions {
|
||||||
@@ -732,7 +734,9 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionCmekActions.Delete,
|
ProjectPermissionCmekActions.Delete,
|
||||||
ProjectPermissionCmekActions.Read,
|
ProjectPermissionCmekActions.Read,
|
||||||
ProjectPermissionCmekActions.Encrypt,
|
ProjectPermissionCmekActions.Encrypt,
|
||||||
ProjectPermissionCmekActions.Decrypt
|
ProjectPermissionCmekActions.Decrypt,
|
||||||
|
ProjectPermissionCmekActions.Sign,
|
||||||
|
ProjectPermissionCmekActions.Verify
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.Cmek
|
ProjectPermissionSub.Cmek
|
||||||
);
|
);
|
||||||
@@ -935,7 +939,9 @@ const buildMemberPermissionRules = () => {
|
|||||||
ProjectPermissionCmekActions.Delete,
|
ProjectPermissionCmekActions.Delete,
|
||||||
ProjectPermissionCmekActions.Read,
|
ProjectPermissionCmekActions.Read,
|
||||||
ProjectPermissionCmekActions.Encrypt,
|
ProjectPermissionCmekActions.Encrypt,
|
||||||
ProjectPermissionCmekActions.Decrypt
|
ProjectPermissionCmekActions.Decrypt,
|
||||||
|
ProjectPermissionCmekActions.Sign,
|
||||||
|
ProjectPermissionCmekActions.Verify
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.Cmek
|
ProjectPermissionSub.Cmek
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1672,7 +1672,8 @@ export const KMS = {
|
|||||||
projectId: "The ID of the project to create the key in.",
|
projectId: "The ID of the project to create the key in.",
|
||||||
name: "The name of the key to be created. Must be slug-friendly.",
|
name: "The name of the key to be created. Must be slug-friendly.",
|
||||||
description: "An optional description of the key.",
|
description: "An optional description of the key.",
|
||||||
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key.",
|
||||||
|
type: "The type of key to be created, either encrypt-decrypt or sign-verify, based on your intended use for the key."
|
||||||
},
|
},
|
||||||
UPDATE_KEY: {
|
UPDATE_KEY: {
|
||||||
keyId: "The ID of the key to be updated.",
|
keyId: "The ID of the key to be updated.",
|
||||||
@@ -1705,6 +1706,28 @@ export const KMS = {
|
|||||||
DECRYPT: {
|
DECRYPT: {
|
||||||
keyId: "The ID of the key to decrypt the data with.",
|
keyId: "The ID of the key to decrypt the data with.",
|
||||||
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
|
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
|
||||||
|
},
|
||||||
|
|
||||||
|
LIST_SIGNING_ALGORITHMS: {
|
||||||
|
keyId: "The ID of the key to list the signing algorithms for. The key must be for signing and verifying."
|
||||||
|
},
|
||||||
|
|
||||||
|
GET_PUBLIC_KEY: {
|
||||||
|
keyId: "The ID of the key to get the public key for. The key must be for signing and verifying."
|
||||||
|
},
|
||||||
|
|
||||||
|
SIGN: {
|
||||||
|
keyId: "The ID of the key to sign the data with.",
|
||||||
|
data: "The data in string format to be signed (base64 encoded).",
|
||||||
|
isDigest:
|
||||||
|
"Whether the data is already digested or not. Please be aware that if you are passing a digest the algorithm used to create the digest must match the signing algorithm used to sign the digest.",
|
||||||
|
signingAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
||||||
|
},
|
||||||
|
VERIFY: {
|
||||||
|
keyId: "The ID of the key to verify the data with.",
|
||||||
|
data: "The data in string format to be verified (base64 encoded). For data larger than 4096 bytes you must first create a digest of the data and then pass the digest in the data parameter.",
|
||||||
|
signature: "The signature to be verified (base64 encoded).",
|
||||||
|
isDigest: "Whether the data is already digested or not."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
|
import { SymmetricKeyAlgorithm, TSymmetricEncryptionFns } from "./types";
|
||||||
|
|
||||||
const getIvLength = () => {
|
const getIvLength = () => {
|
||||||
return 12;
|
return 12;
|
||||||
@@ -10,7 +10,9 @@ const getTagLength = () => {
|
|||||||
return 16;
|
return 16;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
|
export const symmetricCipherService = (
|
||||||
|
type: SymmetricKeyAlgorithm.AES_GCM_128 | SymmetricKeyAlgorithm.AES_GCM_256
|
||||||
|
): TSymmetricEncryptionFns => {
|
||||||
const IV_LENGTH = getIvLength();
|
const IV_LENGTH = getIvLength();
|
||||||
const TAG_LENGTH = getTagLength();
|
const TAG_LENGTH = getTagLength();
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { symmetricCipherService } from "./cipher";
|
export { symmetricCipherService } from "./cipher";
|
||||||
export { SymmetricEncryption } from "./types";
|
export { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "./types";
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
export enum SymmetricEncryption {
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AsymmetricKeyAlgorithm } from "../sign/types";
|
||||||
|
|
||||||
|
// Supported symmetric encrypt/decrypt algorithms
|
||||||
|
export enum SymmetricKeyAlgorithm {
|
||||||
AES_GCM_256 = "aes-256-gcm",
|
AES_GCM_256 = "aes-256-gcm",
|
||||||
AES_GCM_128 = "aes-128-gcm"
|
AES_GCM_128 = "aes-128-gcm"
|
||||||
}
|
}
|
||||||
|
export const SymmetricKeyAlgorithmEnum = z.enum(Object.values(SymmetricKeyAlgorithm) as [string, ...string[]]).options;
|
||||||
|
|
||||||
|
export const AllowedEncryptionKeyAlgorithms = z.enum([
|
||||||
|
...Object.values(SymmetricKeyAlgorithm),
|
||||||
|
...Object.values(AsymmetricKeyAlgorithm)
|
||||||
|
] as [string, ...string[]]).options;
|
||||||
|
|
||||||
export type TSymmetricEncryptionFns = {
|
export type TSymmetricEncryptionFns = {
|
||||||
encrypt: (text: Buffer, key: Buffer) => Buffer;
|
encrypt: (text: Buffer, key: Buffer) => Buffer;
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { signingService } from "./signing";
|
||||||
|
export { AsymmetricKeyAlgorithm, SigningAlgorithm } from "./types";
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
import { execFile } from "child_process";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { cleanTemporaryDirectory, createTemporaryDirectory, writeToTemporaryFile } from "@app/lib/files";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
interface SigningParams {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm;
|
||||||
|
padding?: number;
|
||||||
|
saltLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SupportedHashAlgorithm {
|
||||||
|
SHA256 = "sha256",
|
||||||
|
SHA384 = "sha384",
|
||||||
|
SHA512 = "sha512"
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMAND_TIMEOUT = 15_000;
|
||||||
|
|
||||||
|
const SHA256_DIGEST_LENGTH = 32;
|
||||||
|
const SHA384_DIGEST_LENGTH = 48;
|
||||||
|
const SHA512_DIGEST_LENGTH = 64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for cryptographic signing and verification operations using asymmetric keys
|
||||||
|
*
|
||||||
|
* @param algorithm The key algorithm itself. The signing algorithm is supplied in the individual sign/verify functions.
|
||||||
|
* @returns Object with sign and verify functions
|
||||||
|
*/
|
||||||
|
export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSignVerifyFns => {
|
||||||
|
const $getSigningParams = (signingAlgorithm: SigningAlgorithm): SigningParams => {
|
||||||
|
switch (signingAlgorithm) {
|
||||||
|
// RSA PSS
|
||||||
|
case SigningAlgorithm.RSASSA_PSS_SHA_512:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA512,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||||
|
saltLength: SHA512_DIGEST_LENGTH
|
||||||
|
};
|
||||||
|
case SigningAlgorithm.RSASSA_PSS_SHA_256:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA256,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||||
|
saltLength: SHA256_DIGEST_LENGTH
|
||||||
|
};
|
||||||
|
case SigningAlgorithm.RSASSA_PSS_SHA_384:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA384,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||||
|
saltLength: SHA384_DIGEST_LENGTH
|
||||||
|
};
|
||||||
|
|
||||||
|
// RSA PKCS#1 v1.5
|
||||||
|
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA512,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||||
|
};
|
||||||
|
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA384,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||||
|
};
|
||||||
|
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256:
|
||||||
|
return {
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm.SHA256,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||||
|
};
|
||||||
|
|
||||||
|
// ECDSA
|
||||||
|
case SigningAlgorithm.ECDSA_SHA_256:
|
||||||
|
return { hashAlgorithm: SupportedHashAlgorithm.SHA256 };
|
||||||
|
case SigningAlgorithm.ECDSA_SHA_384:
|
||||||
|
return { hashAlgorithm: SupportedHashAlgorithm.SHA384 };
|
||||||
|
case SigningAlgorithm.ECDSA_SHA_512:
|
||||||
|
return { hashAlgorithm: SupportedHashAlgorithm.SHA512 };
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported signing algorithm: ${signingAlgorithm as string}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $getEcCurveName = (keyAlgorithm: AsymmetricKeyAlgorithm): { full: string; short: string } => {
|
||||||
|
// We will support more in the future
|
||||||
|
switch (keyAlgorithm) {
|
||||||
|
case AsymmetricKeyAlgorithm.ECC_NIST_P256:
|
||||||
|
return {
|
||||||
|
full: "prime256v1",
|
||||||
|
short: "p256"
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported EC curve: ${keyAlgorithm}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $validateAlgorithmWithKeyType = (signingAlgorithm: SigningAlgorithm) => {
|
||||||
|
const isRsaKey = algorithm.startsWith("RSA");
|
||||||
|
const isEccKey = algorithm.startsWith("ECC");
|
||||||
|
|
||||||
|
const isRsaAlgorithm = signingAlgorithm.startsWith("RSASSA");
|
||||||
|
const isEccAlgorithm = signingAlgorithm.startsWith("ECDSA");
|
||||||
|
|
||||||
|
if (isRsaKey && !isRsaAlgorithm) {
|
||||||
|
throw new BadRequestError({ message: `KMS RSA key cannot be used with ${signingAlgorithm}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEccKey && !isEccAlgorithm) {
|
||||||
|
throw new BadRequestError({ message: `KMS ECC key cannot be used with ${signingAlgorithm}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $signRsaDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
|
||||||
|
const tempDir = await createTemporaryDirectory("kms-rsa-sign");
|
||||||
|
const digestPath = path.join(tempDir, "digest.bin");
|
||||||
|
const sigPath = path.join(tempDir, "signature.bin");
|
||||||
|
const keyPath = path.join(tempDir, "key.pem");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeToTemporaryFile(digestPath, digest);
|
||||||
|
await writeToTemporaryFile(keyPath, privateKey);
|
||||||
|
|
||||||
|
const { stderr } = await execFileAsync(
|
||||||
|
"openssl",
|
||||||
|
[
|
||||||
|
"pkeyutl",
|
||||||
|
"-sign",
|
||||||
|
"-in",
|
||||||
|
digestPath,
|
||||||
|
"-inkey",
|
||||||
|
keyPath,
|
||||||
|
"-pkeyopt",
|
||||||
|
`digest:${hashAlgorithm}`,
|
||||||
|
"-out",
|
||||||
|
sigPath
|
||||||
|
],
|
||||||
|
{
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
timeout: COMMAND_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
logger.error(stderr, "KMS: Failed to sign RSA digest");
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to sign RSA digest due to signing error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const signature = await fs.readFile(sigPath);
|
||||||
|
|
||||||
|
if (!signature) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return signature;
|
||||||
|
} finally {
|
||||||
|
await cleanTemporaryDirectory(tempDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $signEccDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
|
||||||
|
const tempDir = await createTemporaryDirectory("ecc-sign");
|
||||||
|
const digestPath = path.join(tempDir, "digest.bin");
|
||||||
|
const keyPath = path.join(tempDir, "key.pem");
|
||||||
|
const sigPath = path.join(tempDir, "signature.bin");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeToTemporaryFile(digestPath, digest);
|
||||||
|
await writeToTemporaryFile(keyPath, privateKey);
|
||||||
|
|
||||||
|
const { stderr } = await execFileAsync(
|
||||||
|
"openssl",
|
||||||
|
[
|
||||||
|
"pkeyutl",
|
||||||
|
"-sign",
|
||||||
|
"-in",
|
||||||
|
digestPath,
|
||||||
|
"-inkey",
|
||||||
|
keyPath,
|
||||||
|
"-pkeyopt",
|
||||||
|
`digest:${hashAlgorithm}`,
|
||||||
|
"-out",
|
||||||
|
sigPath
|
||||||
|
],
|
||||||
|
{
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
timeout: COMMAND_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
logger.error(stderr, "KMS: Failed to sign ECC digest");
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to sign ECC digest due to signing error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = await fs.readFile(sigPath);
|
||||||
|
|
||||||
|
if (!signature) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return signature;
|
||||||
|
} finally {
|
||||||
|
await cleanTemporaryDirectory(tempDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $verifyEccDigest = async (
|
||||||
|
digest: Buffer,
|
||||||
|
signature: Buffer,
|
||||||
|
publicKey: Buffer,
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm
|
||||||
|
) => {
|
||||||
|
const tempDir = await createTemporaryDirectory("ecc-signature-verification");
|
||||||
|
const publicKeyFile = path.join(tempDir, "public-key.pem");
|
||||||
|
const sigFile = path.join(tempDir, "signature.sig");
|
||||||
|
const digestFile = path.join(tempDir, "digest.bin");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeToTemporaryFile(publicKeyFile, publicKey);
|
||||||
|
await writeToTemporaryFile(sigFile, signature);
|
||||||
|
await writeToTemporaryFile(digestFile, digest);
|
||||||
|
|
||||||
|
await execFileAsync(
|
||||||
|
"openssl",
|
||||||
|
[
|
||||||
|
"pkeyutl",
|
||||||
|
"-verify",
|
||||||
|
"-in",
|
||||||
|
digestFile,
|
||||||
|
"-inkey",
|
||||||
|
publicKeyFile,
|
||||||
|
"-pubin", // Important for EC public keys
|
||||||
|
"-sigfile",
|
||||||
|
sigFile,
|
||||||
|
"-pkeyopt",
|
||||||
|
`digest:${hashAlgorithm}`
|
||||||
|
],
|
||||||
|
{ timeout: COMMAND_TIMEOUT }
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as { stderr: string };
|
||||||
|
|
||||||
|
if (
|
||||||
|
!err?.stderr?.toLowerCase()?.includes("signature verification failure") &&
|
||||||
|
!err?.stderr?.toLowerCase()?.includes("bad signature")
|
||||||
|
) {
|
||||||
|
logger.error(error, "KMS: Failed to verify ECC signature");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
await cleanTemporaryDirectory(tempDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const $verifyRsaDigest = async (
|
||||||
|
digest: Buffer,
|
||||||
|
signature: Buffer,
|
||||||
|
publicKey: Buffer,
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm
|
||||||
|
) => {
|
||||||
|
const tempDir = await createTemporaryDirectory("kms-signature-verification");
|
||||||
|
const publicKeyFile = path.join(tempDir, "public-key.pub");
|
||||||
|
const signatureFile = path.join(tempDir, "signature.sig");
|
||||||
|
const digestFile = path.join(tempDir, "digest.bin");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeToTemporaryFile(publicKeyFile, publicKey);
|
||||||
|
await writeToTemporaryFile(signatureFile, signature);
|
||||||
|
await writeToTemporaryFile(digestFile, digest);
|
||||||
|
|
||||||
|
await execFileAsync(
|
||||||
|
"openssl",
|
||||||
|
[
|
||||||
|
"pkeyutl",
|
||||||
|
"-verify",
|
||||||
|
"-in",
|
||||||
|
digestFile,
|
||||||
|
"-inkey",
|
||||||
|
publicKeyFile,
|
||||||
|
"-pubin",
|
||||||
|
"-sigfile",
|
||||||
|
signatureFile,
|
||||||
|
"-pkeyopt",
|
||||||
|
`digest:${hashAlgorithm}`
|
||||||
|
],
|
||||||
|
{ timeout: COMMAND_TIMEOUT }
|
||||||
|
);
|
||||||
|
|
||||||
|
// it'll throw if the verification was not successful
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as { stdout: string };
|
||||||
|
|
||||||
|
if (!err?.stdout?.toLowerCase()?.includes("signature verification failure")) {
|
||||||
|
logger.error(error, "KMS: Failed to verify signature");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
await cleanTemporaryDirectory(tempDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyDigestFunctionsMap: Record<
|
||||||
|
AsymmetricKeyAlgorithm,
|
||||||
|
(data: Buffer, signature: Buffer, publicKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<boolean>
|
||||||
|
> = {
|
||||||
|
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $verifyEccDigest,
|
||||||
|
[AsymmetricKeyAlgorithm.RSA_4096]: $verifyRsaDigest
|
||||||
|
};
|
||||||
|
|
||||||
|
const signDigestFunctionsMap: Record<
|
||||||
|
AsymmetricKeyAlgorithm,
|
||||||
|
(data: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<Buffer>
|
||||||
|
> = {
|
||||||
|
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest,
|
||||||
|
[AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest
|
||||||
|
};
|
||||||
|
|
||||||
|
const sign = async (
|
||||||
|
data: Buffer,
|
||||||
|
privateKey: Buffer,
|
||||||
|
signingAlgorithm: SigningAlgorithm,
|
||||||
|
isDigest: boolean
|
||||||
|
): Promise<Buffer> => {
|
||||||
|
$validateAlgorithmWithKeyType(signingAlgorithm);
|
||||||
|
|
||||||
|
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
|
||||||
|
|
||||||
|
if (isDigest) {
|
||||||
|
if (signingAlgorithm.startsWith("RSASSA_PSS")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "RSA PSS does not support digested input"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signFunction = signDigestFunctionsMap[algorithm];
|
||||||
|
|
||||||
|
if (!signFunction) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Digested input is not supported for key algorithm ${algorithm}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = await signFunction(data, privateKey, hashAlgorithm);
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKeyObject = crypto.createPrivateKey({
|
||||||
|
key: privateKey,
|
||||||
|
format: "pem",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
|
||||||
|
// For RSA signatures
|
||||||
|
if (signingAlgorithm.startsWith("RSA")) {
|
||||||
|
const signer = crypto.createSign(hashAlgorithm);
|
||||||
|
signer.update(data);
|
||||||
|
|
||||||
|
return signer.sign({
|
||||||
|
key: privateKeyObject,
|
||||||
|
padding,
|
||||||
|
...(signingAlgorithm.includes("PSS") ? { saltLength } : {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (signingAlgorithm.startsWith("ECDSA")) {
|
||||||
|
// For ECDSA signatures
|
||||||
|
const signer = crypto.createSign(hashAlgorithm);
|
||||||
|
signer.update(data);
|
||||||
|
return signer.sign({
|
||||||
|
key: privateKeyObject,
|
||||||
|
dsaEncoding: "der"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Signing algorithm ${signingAlgorithm} not implemented`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const verify = async (
|
||||||
|
data: Buffer,
|
||||||
|
signature: Buffer,
|
||||||
|
publicKey: Buffer,
|
||||||
|
signingAlgorithm: SigningAlgorithm,
|
||||||
|
isDigest: boolean
|
||||||
|
): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
$validateAlgorithmWithKeyType(signingAlgorithm);
|
||||||
|
|
||||||
|
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
|
||||||
|
|
||||||
|
if (isDigest) {
|
||||||
|
if (signingAlgorithm.startsWith("RSASSA_PSS")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "RSA PSS does not support digested input"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyFunction = verifyDigestFunctionsMap[algorithm];
|
||||||
|
|
||||||
|
if (!verifyFunction) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Digested input is not supported for key algorithm ${algorithm}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signatureValid = await verifyFunction(data, signature, publicKey, hashAlgorithm);
|
||||||
|
|
||||||
|
return signatureValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKeyObject = crypto.createPublicKey({
|
||||||
|
key: publicKey,
|
||||||
|
format: "der",
|
||||||
|
type: "spki"
|
||||||
|
});
|
||||||
|
|
||||||
|
// For RSA signatures
|
||||||
|
if (signingAlgorithm.startsWith("RSA")) {
|
||||||
|
const verifier = crypto.createVerify(hashAlgorithm);
|
||||||
|
verifier.update(data);
|
||||||
|
|
||||||
|
return verifier.verify(
|
||||||
|
{
|
||||||
|
key: publicKeyObject,
|
||||||
|
padding,
|
||||||
|
...(signingAlgorithm.includes("PSS") ? { saltLength } : {})
|
||||||
|
},
|
||||||
|
signature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// For ECDSA signatures
|
||||||
|
if (signingAlgorithm.startsWith("ECDSA")) {
|
||||||
|
const verifier = crypto.createVerify(hashAlgorithm);
|
||||||
|
verifier.update(data);
|
||||||
|
return verifier.verify(
|
||||||
|
{
|
||||||
|
key: publicKeyObject,
|
||||||
|
dsaEncoding: "der"
|
||||||
|
},
|
||||||
|
signature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Verification for algorithm ${signingAlgorithm} not implemented`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BadRequestError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
logger.error(error, "KMS: Failed to verify signature");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateAsymmetricPrivateKey = async () => {
|
||||||
|
const { privateKey } = await new Promise<{ privateKey: string }>((resolve, reject) => {
|
||||||
|
if (algorithm.startsWith("RSA")) {
|
||||||
|
crypto.generateKeyPair(
|
||||||
|
"rsa",
|
||||||
|
{
|
||||||
|
modulusLength: Number(algorithm.split("_")[1]),
|
||||||
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||||
|
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||||
|
},
|
||||||
|
(err, _, pk) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve({ privateKey: pk });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { full: namedCurve } = $getEcCurveName(algorithm);
|
||||||
|
|
||||||
|
crypto.generateKeyPair(
|
||||||
|
"ec",
|
||||||
|
{
|
||||||
|
namedCurve,
|
||||||
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||||
|
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||||
|
},
|
||||||
|
(err, _, pk) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
privateKey: pk
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Buffer.from(privateKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublicKeyFromPrivateKey = (privateKey: Buffer) => {
|
||||||
|
const privateKeyObj = crypto.createPrivateKey({
|
||||||
|
key: privateKey,
|
||||||
|
format: "pem",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicKey = crypto.createPublicKey(privateKeyObj).export({
|
||||||
|
type: "spki",
|
||||||
|
format: "der"
|
||||||
|
});
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sign,
|
||||||
|
verify,
|
||||||
|
generateAsymmetricPrivateKey,
|
||||||
|
getPublicKeyFromPrivateKey
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export type TAsymmetricSignVerifyFns = {
|
||||||
|
sign: (data: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm, isDigest: boolean) => Promise<Buffer>;
|
||||||
|
verify: (
|
||||||
|
data: Buffer,
|
||||||
|
signature: Buffer,
|
||||||
|
key: Buffer,
|
||||||
|
signingAlgorithm: SigningAlgorithm,
|
||||||
|
isDigest: boolean
|
||||||
|
) => Promise<boolean>;
|
||||||
|
generateAsymmetricPrivateKey: () => Promise<Buffer>;
|
||||||
|
getPublicKeyFromPrivateKey: (privateKey: Buffer) => Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Supported asymmetric key types
|
||||||
|
export enum AsymmetricKeyAlgorithm {
|
||||||
|
RSA_4096 = "RSA_4096",
|
||||||
|
ECC_NIST_P256 = "ECC_NIST_P256"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AsymmetricKeyAlgorithmEnum = z.enum(
|
||||||
|
Object.values(AsymmetricKeyAlgorithm) as [string, ...string[]]
|
||||||
|
).options;
|
||||||
|
|
||||||
|
export enum SigningAlgorithm {
|
||||||
|
// RSA PSS algorithms
|
||||||
|
// These are NOT deterministic and include randomness.
|
||||||
|
// This means that the output signature is different each time for the same input.
|
||||||
|
RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512",
|
||||||
|
RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384",
|
||||||
|
RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256",
|
||||||
|
|
||||||
|
// RSA PKCS#1 v1.5 algorithms
|
||||||
|
// These are deterministic and the output is the same each time for the same input.
|
||||||
|
RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512",
|
||||||
|
RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384",
|
||||||
|
RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256",
|
||||||
|
|
||||||
|
// ECDSA algorithms
|
||||||
|
// None of these are deterministic and include randomness like RSA PSS.
|
||||||
|
ECDSA_SHA_512 = "ECDSA_SHA_512",
|
||||||
|
ECDSA_SHA_384 = "ECDSA_SHA_384",
|
||||||
|
ECDSA_SHA_256 = "ECDSA_SHA_256"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
const baseDir = path.join(os.tmpdir(), "infisical");
|
||||||
|
const randomPath = () => `${crypto.randomBytes(32).toString("hex")}`;
|
||||||
|
|
||||||
|
export const createTemporaryDirectory = async (name: string) => {
|
||||||
|
const tempDirPath = path.join(baseDir, `${name}-${randomPath()}`);
|
||||||
|
await fs.mkdir(tempDirPath, { recursive: true });
|
||||||
|
|
||||||
|
return tempDirPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeTemporaryBaseDirectory = async () => {
|
||||||
|
await fs.rm(baseDir, { force: true, recursive: true }).catch((err) => {
|
||||||
|
logger.error(err, `Failed to remove temporary base directory [path=${baseDir}]`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanTemporaryDirectory = async (dirPath: string) => {
|
||||||
|
await fs.rm(dirPath, { recursive: true, force: true }).catch((err) => {
|
||||||
|
logger.error(err, `Failed to cleanup temporary directory [path=${dirPath}]`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const writeToTemporaryFile = async (tempDirPath: string, data: string | Buffer) => {
|
||||||
|
await fs.writeFile(tempDirPath, data, { mode: 0o600 }).catch((err) => {
|
||||||
|
logger.error(err, `Failed to write to temporary file [path=${tempDirPath}]`);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./files";
|
||||||
@@ -9,6 +9,7 @@ import { runMigrations } from "./auto-start-migrations";
|
|||||||
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
||||||
import { keyStoreFactory } from "./keystore/keystore";
|
import { keyStoreFactory } from "./keystore/keystore";
|
||||||
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
|
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
|
||||||
|
import { removeTemporaryBaseDirectory } from "./lib/files";
|
||||||
import { initLogger } from "./lib/logger";
|
import { initLogger } from "./lib/logger";
|
||||||
import { queueServiceFactory } from "./queue";
|
import { queueServiceFactory } from "./queue";
|
||||||
import { main } from "./server/app";
|
import { main } from "./server/app";
|
||||||
@@ -21,6 +22,8 @@ const run = async () => {
|
|||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
const envConfig = initEnvConfig(logger);
|
const envConfig = initEnvConfig(logger);
|
||||||
|
|
||||||
|
await removeTemporaryBaseDirectory();
|
||||||
|
|
||||||
const db = initDbConnection({
|
const db = initDbConnection({
|
||||||
dbConnectionUri: envConfig.DB_CONNECTION_URI,
|
dbConnectionUri: envConfig.DB_CONNECTION_URI,
|
||||||
dbRootCert: envConfig.DB_ROOT_CERT,
|
dbRootCert: envConfig.DB_ROOT_CERT,
|
||||||
@@ -71,6 +74,7 @@ const run = async () => {
|
|||||||
process.on("SIGINT", async () => {
|
process.on("SIGINT", async () => {
|
||||||
await server.close();
|
await server.close();
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
|
await removeTemporaryBaseDirectory();
|
||||||
hsmModule.finalize();
|
hsmModule.finalize();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
@@ -79,6 +83,7 @@ const run = async () => {
|
|||||||
process.on("SIGTERM", async () => {
|
process.on("SIGTERM", async () => {
|
||||||
await server.close();
|
await server.close();
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
|
await removeTemporaryBaseDirectory();
|
||||||
hsmModule.finalize();
|
hsmModule.finalize();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
|||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { KMS } from "@app/lib/api-docs";
|
import { KMS } from "@app/lib/api-docs";
|
||||||
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
|
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
|
import { CmekOrderBy, TCmekKeyEncryptionAlgorithm } from "@app/services/cmek/cmek-types";
|
||||||
|
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
|
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
|
||||||
const keyDescriptionSchema = z.string().trim().max(500).optional();
|
const keyDescriptionSchema = z.string().trim().max(500).optional();
|
||||||
@@ -45,16 +47,46 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Create KMS key",
|
description: "Create KMS key",
|
||||||
body: z.object({
|
body: z
|
||||||
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
.object({
|
||||||
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
||||||
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
||||||
encryptionAlgorithm: z
|
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
||||||
.nativeEnum(SymmetricEncryption)
|
keyUsage: z
|
||||||
.optional()
|
.nativeEnum(KmsKeyUsage)
|
||||||
.default(SymmetricEncryption.AES_GCM_256)
|
.optional()
|
||||||
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others
|
.default(KmsKeyUsage.ENCRYPT_DECRYPT)
|
||||||
}),
|
.describe(KMS.CREATE_KEY.type),
|
||||||
|
encryptionAlgorithm: z
|
||||||
|
.enum(AllowedEncryptionKeyAlgorithms)
|
||||||
|
.optional()
|
||||||
|
.default(SymmetricKeyAlgorithm.AES_GCM_256)
|
||||||
|
.describe(KMS.CREATE_KEY.encryptionAlgorithm)
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (
|
||||||
|
data.keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT &&
|
||||||
|
!Object.values(SymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as SymmetricKeyAlgorithm)
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `encryptionAlgorithm must be a valid symmetric encryption algorithm. Valid options are: ${Object.values(
|
||||||
|
SymmetricKeyAlgorithm
|
||||||
|
).join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
data.keyUsage === KmsKeyUsage.SIGN_VERIFY &&
|
||||||
|
!Object.values(AsymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as AsymmetricKeyAlgorithm)
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `encryptionAlgorithm must be a valid asymmetric sign-verify algorithm. Valid options are: ${Object.values(
|
||||||
|
AsymmetricKeyAlgorithm
|
||||||
|
).join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
key: CmekSchema
|
key: CmekSchema
|
||||||
@@ -64,12 +96,19 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const {
|
const {
|
||||||
body: { projectId, name, description, encryptionAlgorithm },
|
body: { projectId, name, description, encryptionAlgorithm, keyUsage },
|
||||||
permission
|
permission
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
const cmek = await server.services.cmek.createCmek(
|
const cmek = await server.services.cmek.createCmek(
|
||||||
{ orgId: permission.orgId, projectId, name, description, encryptionAlgorithm },
|
{
|
||||||
|
orgId: permission.orgId,
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm,
|
||||||
|
keyUsage
|
||||||
|
},
|
||||||
permission
|
permission
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,7 +121,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
keyId: cmek.id,
|
keyId: cmek.id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
encryptionAlgorithm
|
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -126,7 +165,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: permission.orgId,
|
projectId: cmek.projectId!,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.UPDATE_CMEK,
|
type: EventType.UPDATE_CMEK,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -169,7 +208,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: permission.orgId,
|
projectId: cmek.projectId!,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.DELETE_CMEK,
|
type: EventType.DELETE_CMEK,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -282,7 +321,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Get KMS key by Name",
|
description: "Get KMS key by name",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName)
|
keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName)
|
||||||
}),
|
}),
|
||||||
@@ -349,11 +388,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
permission
|
permission
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
|
const { ciphertext, projectId } = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: permission.orgId,
|
projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.CMEK_ENCRYPT,
|
type: EventType.CMEK_ENCRYPT,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -366,6 +405,198 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/keys/:keyId/public-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description:
|
||||||
|
"Get the public key for a KMS key that is used for signing and verifying data. This endpoint is only available for asymmetric keys.",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.GET_PUBLIC_KEY.keyId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
publicKey: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const { publicKey, projectId } = await server.services.cmek.getPublicKey({ keyId }, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CMEK_GET_PUBLIC_KEY,
|
||||||
|
metadata: {
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { publicKey };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/keys/:keyId/signing-algorithms",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List all available signing algorithms for a KMS key",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.LIST_SIGNING_ALGORITHMS.keyId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
signingAlgorithms: z.array(z.nativeEnum(SigningAlgorithm))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { keyId } = req.params;
|
||||||
|
|
||||||
|
const { signingAlgorithms, projectId } = await server.services.cmek.listSigningAlgorithms(
|
||||||
|
{ keyId },
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CMEK_LIST_SIGNING_ALGORITHMS,
|
||||||
|
metadata: {
|
||||||
|
keyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { signingAlgorithms };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/keys/:keyId/sign",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Sign data with a KMS key.",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.SIGN.keyId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
|
||||||
|
isDigest: z.boolean().optional().default(false).describe(KMS.SIGN.isDigest),
|
||||||
|
data: base64Schema.describe(KMS.SIGN.data)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
signature: z.string(),
|
||||||
|
keyId: z.string().uuid(),
|
||||||
|
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId: inputKeyId },
|
||||||
|
body: { data, signingAlgorithm, isDigest },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const { projectId, ...result } = await server.services.cmek.cmekSign(
|
||||||
|
{ keyId: inputKeyId, data, signingAlgorithm, isDigest },
|
||||||
|
permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CMEK_SIGN,
|
||||||
|
metadata: {
|
||||||
|
keyId: inputKeyId,
|
||||||
|
signingAlgorithm,
|
||||||
|
signature: result.signature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/keys/:keyId/verify",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Verify data signatures with a KMS key.",
|
||||||
|
params: z.object({
|
||||||
|
keyId: z.string().uuid().describe(KMS.VERIFY.keyId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
isDigest: z.boolean().optional().default(false).describe(KMS.VERIFY.isDigest),
|
||||||
|
data: base64Schema.describe(KMS.VERIFY.data),
|
||||||
|
signature: base64Schema.describe(KMS.VERIFY.signature),
|
||||||
|
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
signatureValid: z.boolean(),
|
||||||
|
keyId: z.string().uuid(),
|
||||||
|
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
params: { keyId },
|
||||||
|
body: { data, signature, signingAlgorithm, isDigest },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const { projectId, ...result } = await server.services.cmek.cmekVerify(
|
||||||
|
{ keyId, data, signature, signingAlgorithm, isDigest },
|
||||||
|
permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CMEK_VERIFY,
|
||||||
|
metadata: {
|
||||||
|
keyId,
|
||||||
|
signatureValid: result.signatureValid,
|
||||||
|
signingAlgorithm,
|
||||||
|
signature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/keys/:keyId/decrypt",
|
url: "/keys/:keyId/decrypt",
|
||||||
@@ -394,11 +625,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
|||||||
permission
|
permission
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
|
const { plaintext, projectId } = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: permission.orgId,
|
projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.CMEK_DECRYPT,
|
type: EventType.CMEK_DECRYPT,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import {
|
import {
|
||||||
TCmekDecryptDTO,
|
TCmekDecryptDTO,
|
||||||
TCmekEncryptDTO,
|
TCmekEncryptDTO,
|
||||||
|
TCmekGetPublicKeyDTO,
|
||||||
|
TCmekKeyEncryptionAlgorithm,
|
||||||
|
TCmekListSigningAlgorithmsDTO,
|
||||||
|
TCmekSignDTO,
|
||||||
|
TCmekVerifyDTO,
|
||||||
TCreateCmekDTO,
|
TCreateCmekDTO,
|
||||||
TListCmeksByProjectIdDTO,
|
TListCmeksByProjectIdDTO,
|
||||||
TUpdabteCmekByIdDTO
|
TUpdabteCmekByIdDTO
|
||||||
@@ -16,6 +22,7 @@ import {
|
|||||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { KmsKeyUsage } from "../kms/kms-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
|
||||||
type TCmekServiceFactoryDep = {
|
type TCmekServiceFactoryDep = {
|
||||||
@@ -221,7 +228,151 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
|
|
||||||
const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") });
|
const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") });
|
||||||
|
|
||||||
return cipherTextBlob.toString("base64");
|
return {
|
||||||
|
ciphertext: cipherTextBlob.toString("base64"),
|
||||||
|
projectId: key.projectId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const listSigningAlgorithms = async ({ keyId }: TCmekListSigningAlgorithmsDTO, actor: OrgServiceActor) => {
|
||||||
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
|
if (key.keyUsage !== KmsKeyUsage.SIGN_VERIFY) {
|
||||||
|
throw new BadRequestError({ message: `Key with ID '${keyId}' is not intended for signing` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = key.encryptionAlgorithm as TCmekKeyEncryptionAlgorithm;
|
||||||
|
|
||||||
|
const algos = [
|
||||||
|
{
|
||||||
|
keyAlgorithm: "rsa",
|
||||||
|
signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) =>
|
||||||
|
algorithm.toLowerCase().startsWith("rsa")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keyAlgorithm: "ecc",
|
||||||
|
signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) =>
|
||||||
|
algorithm.toLowerCase().startsWith("ecdsa")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedAlgorithm = algos.find((algo) => encryptionAlgorithm.toLowerCase().startsWith(algo.keyAlgorithm));
|
||||||
|
|
||||||
|
if (!selectedAlgorithm) {
|
||||||
|
throw new BadRequestError({ message: `Unsupported encryption algorithm: ${encryptionAlgorithm}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { signingAlgorithms: selectedAlgorithm.signingAlgorithms, projectId: key.projectId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublicKey = async ({ keyId }: TCmekGetPublicKeyDTO, actor: OrgServiceActor) => {
|
||||||
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
|
const publicKey = await kmsService.getPublicKey({ kmsId: keyId });
|
||||||
|
return { publicKey: publicKey.toString("base64"), projectId: key.projectId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const cmekSign = async ({ keyId, data, signingAlgorithm, isDigest }: TCmekSignDTO, actor: OrgServiceActor) => {
|
||||||
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
|
||||||
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
|
||||||
|
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Sign, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
|
const sign = await kmsService.signWithKmsKey({ kmsId: keyId });
|
||||||
|
|
||||||
|
const { signature, algorithm } = await sign({ data: Buffer.from(data, "base64"), signingAlgorithm, isDigest });
|
||||||
|
|
||||||
|
return {
|
||||||
|
signature: signature.toString("base64"),
|
||||||
|
keyId: key.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
signingAlgorithm: algorithm
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const cmekVerify = async (
|
||||||
|
{ keyId, data, signature, signingAlgorithm, isDigest }: TCmekVerifyDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const key = await kmsDAL.findCmekById(keyId);
|
||||||
|
|
||||||
|
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||||
|
|
||||||
|
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||||
|
|
||||||
|
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.KMS
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Verify, ProjectPermissionSub.Cmek);
|
||||||
|
|
||||||
|
const verify = await kmsService.verifyWithKmsKey({ kmsId: keyId, signingAlgorithm });
|
||||||
|
|
||||||
|
const { signatureValid, algorithm } = await verify({
|
||||||
|
isDigest,
|
||||||
|
data: Buffer.from(data, "base64"),
|
||||||
|
signature: Buffer.from(signature, "base64")
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
signatureValid,
|
||||||
|
keyId: key.id,
|
||||||
|
projectId: key.projectId,
|
||||||
|
signingAlgorithm: algorithm
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
|
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
|
||||||
@@ -248,7 +399,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
|
|
||||||
const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") });
|
const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") });
|
||||||
|
|
||||||
return plaintextBlob.toString("base64");
|
return {
|
||||||
|
plaintext: plaintextBlob.toString("base64"),
|
||||||
|
projectId: key.projectId
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -259,6 +413,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
|||||||
cmekEncrypt,
|
cmekEncrypt,
|
||||||
cmekDecrypt,
|
cmekDecrypt,
|
||||||
findCmekById,
|
findCmekById,
|
||||||
findCmekByName
|
findCmekByName,
|
||||||
|
cmekSign,
|
||||||
|
cmekVerify,
|
||||||
|
listSigningAlgorithms,
|
||||||
|
getPublicKey
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { KmsKeyUsage } from "../kms/kms-types";
|
||||||
|
|
||||||
|
export type TCmekKeyEncryptionAlgorithm = SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||||
|
|
||||||
export type TCreateCmekDTO = {
|
export type TCreateCmekDTO = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
encryptionAlgorithm: SymmetricEncryption;
|
encryptionAlgorithm: TCmekKeyEncryptionAlgorithm;
|
||||||
|
keyUsage: KmsKeyUsage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdabteCmekByIdDTO = {
|
export type TUpdabteCmekByIdDTO = {
|
||||||
@@ -38,3 +44,26 @@ export type TCmekDecryptDTO = {
|
|||||||
export enum CmekOrderBy {
|
export enum CmekOrderBy {
|
||||||
Name = "name"
|
Name = "name"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TCmekListSigningAlgorithmsDTO = {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCmekGetPublicKeyDTO = {
|
||||||
|
keyId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCmekSignDTO = {
|
||||||
|
keyId: string;
|
||||||
|
data: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
isDigest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCmekVerifyDTO = {
|
||||||
|
keyId: string;
|
||||||
|
data: string;
|
||||||
|
signature: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
isDigest: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,13 +1,55 @@
|
|||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
|
import { AsymmetricKeyAlgorithm } from "@app/lib/crypto/sign";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { KmsKeyUsage } from "./kms-types";
|
||||||
|
|
||||||
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
|
export const getByteLengthForSymmetricEncryptionAlgorithm = (encryptionAlgorithm: SymmetricKeyAlgorithm) => {
|
||||||
switch (encryptionAlgorithm) {
|
switch (encryptionAlgorithm) {
|
||||||
case SymmetricEncryption.AES_GCM_128:
|
case SymmetricKeyAlgorithm.AES_GCM_128:
|
||||||
return 16;
|
return 16;
|
||||||
case SymmetricEncryption.AES_GCM_256:
|
case SymmetricKeyAlgorithm.AES_GCM_256:
|
||||||
default:
|
default:
|
||||||
return 32;
|
return 32;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const verifyKeyTypeAndAlgorithm = (
|
||||||
|
keyUsage: KmsKeyUsage,
|
||||||
|
algorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm,
|
||||||
|
extra?: {
|
||||||
|
forceType?: KmsKeyUsage;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (extra?.forceType && keyUsage !== extra.forceType) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported key type, expected ${extra.forceType} but got ${keyUsage}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
|
||||||
|
if (!Object.values(SymmetricKeyAlgorithm).includes(algorithm as SymmetricKeyAlgorithm)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported encryption algorithm for encrypt/decrypt key: ${algorithm as string}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyUsage === KmsKeyUsage.SIGN_VERIFY) {
|
||||||
|
if (!Object.values(AsymmetricKeyAlgorithm).includes(algorithm as AsymmetricKeyAlgorithm)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported sign/verify algorithm for sign/verify key: ${algorithm as string}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported key type: ${keyUsage as string}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
|||||||
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { TEnvConfig } from "@app/lib/config/env";
|
import { TEnvConfig } from "@app/lib/config/env";
|
||||||
import { randomSecureBytes } from "@app/lib/crypto";
|
import { randomSecureBytes } from "@app/lib/crypto";
|
||||||
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { symmetricCipherService, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
import { generateHash } from "@app/lib/crypto/encryption";
|
import { generateHash } from "@app/lib/crypto/encryption";
|
||||||
|
import { AsymmetricKeyAlgorithm, signingService } from "@app/lib/crypto/sign";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { getByteLengthForAlgorithm, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns";
|
import {
|
||||||
|
getByteLengthForSymmetricEncryptionAlgorithm,
|
||||||
|
KMS_ROOT_CONFIG_UUID,
|
||||||
|
verifyKeyTypeAndAlgorithm
|
||||||
|
} from "@app/services/kms/kms-fns";
|
||||||
|
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
@@ -29,6 +34,7 @@ import { TKmsKeyDALFactory } from "./kms-key-dal";
|
|||||||
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
||||||
import {
|
import {
|
||||||
KmsDataKey,
|
KmsDataKey,
|
||||||
|
KmsKeyUsage,
|
||||||
KmsType,
|
KmsType,
|
||||||
RootKeyEncryptionStrategy,
|
RootKeyEncryptionStrategy,
|
||||||
TDecryptWithKeyDTO,
|
TDecryptWithKeyDTO,
|
||||||
@@ -38,8 +44,11 @@ import {
|
|||||||
TEncryptWithKmsDTO,
|
TEncryptWithKmsDTO,
|
||||||
TGenerateKMSDTO,
|
TGenerateKMSDTO,
|
||||||
TGetKeyMaterialDTO,
|
TGetKeyMaterialDTO,
|
||||||
|
TGetPublicKeyDTO,
|
||||||
TImportKeyMaterialDTO,
|
TImportKeyMaterialDTO,
|
||||||
TUpdateProjectSecretManagerKmsKeyDTO
|
TSignWithKmsDTO,
|
||||||
|
TUpdateProjectSecretManagerKmsKeyDTO,
|
||||||
|
TVerifyWithKmsDTO
|
||||||
} from "./kms-types";
|
} from "./kms-types";
|
||||||
|
|
||||||
type TKmsServiceFactoryDep = {
|
type TKmsServiceFactoryDep = {
|
||||||
@@ -83,19 +92,42 @@ export const kmsServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
name,
|
name,
|
||||||
projectId,
|
projectId,
|
||||||
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
|
encryptionAlgorithm = SymmetricKeyAlgorithm.AES_GCM_256,
|
||||||
|
keyUsage = KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||||
description
|
description
|
||||||
}: TGenerateKMSDTO) => {
|
}: TGenerateKMSDTO) => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
// daniel: ensure that the key type (sign/encrypt) and the encryption algorithm are compatible.
|
||||||
|
verifyKeyTypeAndAlgorithm(keyUsage, encryptionAlgorithm);
|
||||||
|
|
||||||
const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm));
|
let kmsKeyMaterial: Buffer | null = null;
|
||||||
|
if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
|
||||||
|
kmsKeyMaterial = randomSecureBytes(
|
||||||
|
getByteLengthForSymmetricEncryptionAlgorithm(encryptionAlgorithm as SymmetricKeyAlgorithm)
|
||||||
|
);
|
||||||
|
} else if (keyUsage === KmsKeyUsage.SIGN_VERIFY) {
|
||||||
|
const { generateAsymmetricPrivateKey, getPublicKeyFromPrivateKey } = signingService(
|
||||||
|
encryptionAlgorithm as AsymmetricKeyAlgorithm
|
||||||
|
);
|
||||||
|
kmsKeyMaterial = await generateAsymmetricPrivateKey();
|
||||||
|
|
||||||
|
// daniel: safety check to ensure we're able to extract the public key from the private key before we proceed to key creation
|
||||||
|
getPublicKeyFromPrivateKey(kmsKeyMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kmsKeyMaterial) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid KMS key type. No key material was created for key usage '${keyUsage}' using algorithm '${encryptionAlgorithm}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
||||||
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||||
const dbQuery = async (db: Knex) => {
|
const dbQuery = async (db: Knex) => {
|
||||||
const kmsDoc = await kmsDAL.create(
|
const kmsDoc = await kmsDAL.create(
|
||||||
{
|
{
|
||||||
name: sanitizedName,
|
name: sanitizedName,
|
||||||
|
keyUsage,
|
||||||
orgId,
|
orgId,
|
||||||
isReserved,
|
isReserved,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -115,6 +147,7 @@ export const kmsServiceFactory = ({
|
|||||||
);
|
);
|
||||||
return kmsDoc;
|
return kmsDoc;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tx) return dbQuery(tx);
|
if (tx) return dbQuery(tx);
|
||||||
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
|
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
|
||||||
return doc;
|
return doc;
|
||||||
@@ -134,7 +167,7 @@ export const kmsServiceFactory = ({
|
|||||||
*/
|
*/
|
||||||
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
||||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
|
const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
|
||||||
// Buffer#1 encrypted text + Buffer#2 version number
|
// Buffer#1 encrypted text + Buffer#2 version number
|
||||||
@@ -149,7 +182,7 @@ export const kmsServiceFactory = ({
|
|||||||
* This can be even later exposed directly as api for encryption as function
|
* This can be even later exposed directly as api for encryption as function
|
||||||
*/
|
*/
|
||||||
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
|
||||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||||
@@ -227,7 +260,7 @@ export const kmsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const encryptWithRootKey = () => {
|
const encryptWithRootKey = () => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
|
||||||
return (plainTextBuffer: Buffer) => {
|
return (plainTextBuffer: Buffer) => {
|
||||||
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
|
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||||
@@ -236,7 +269,7 @@ export const kmsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const decryptWithRootKey = () => {
|
const decryptWithRootKey = () => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
|
||||||
return (cipherTextBuffer: Buffer) => {
|
return (cipherTextBuffer: Buffer) => {
|
||||||
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
|
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||||
@@ -255,6 +288,11 @@ export const kmsServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
|
||||||
|
});
|
||||||
|
|
||||||
if (kmsDoc.externalKms) {
|
if (kmsDoc.externalKms) {
|
||||||
let externalKms: TExternalKmsProviderFns;
|
let externalKms: TExternalKmsProviderFns;
|
||||||
|
|
||||||
@@ -316,8 +354,8 @@ export const kmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// internal KMS
|
// internal KMS
|
||||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||||
@@ -345,19 +383,22 @@ export const kmsServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
return kmsKey;
|
return kmsKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
const importKeyMaterial = async (
|
const importKeyMaterial = async (
|
||||||
{ key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO,
|
{ key, algorithm, name, isReserved, projectId, orgId, keyUsage }: TImportKeyMaterialDTO,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
// daniel: currently we only support imports for encrypt/decrypt keys
|
||||||
|
verifyKeyTypeAndAlgorithm(keyUsage, algorithm, { forceType: KmsKeyUsage.ENCRYPT_DECRYPT });
|
||||||
|
|
||||||
const expectedByteLength = getByteLengthForAlgorithm(algorithm);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
|
||||||
|
const expectedByteLength = getByteLengthForSymmetricEncryptionAlgorithm(algorithm as SymmetricKeyAlgorithm);
|
||||||
if (key.byteLength !== expectedByteLength) {
|
if (key.byteLength !== expectedByteLength) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
|
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
|
||||||
@@ -370,6 +411,7 @@ export const kmsServiceFactory = ({
|
|||||||
const kmsDoc = await kmsDAL.create(
|
const kmsDoc = await kmsDAL.create(
|
||||||
{
|
{
|
||||||
name: sanitizedName,
|
name: sanitizedName,
|
||||||
|
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||||
orgId,
|
orgId,
|
||||||
isReserved,
|
isReserved,
|
||||||
projectId
|
projectId
|
||||||
@@ -393,12 +435,85 @@ export const kmsServiceFactory = ({
|
|||||||
return doc;
|
return doc;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPublicKey = async ({ kmsId }: TGetPublicKeyDTO) => {
|
||||||
|
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||||
|
if (!kmsDoc) {
|
||||||
|
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
|
||||||
|
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.SIGN_VERIFY
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
return signingService(encryptionAlgorithm).getPublicKeyFromPrivateKey(kmsKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const signWithKmsKey = async ({ kmsId }: Pick<TSignWithKmsDTO, "kmsId">) => {
|
||||||
|
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||||
|
if (!kmsDoc) {
|
||||||
|
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.SIGN_VERIFY
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
const { sign } = signingService(encryptionAlgorithm);
|
||||||
|
return async ({
|
||||||
|
data,
|
||||||
|
signingAlgorithm,
|
||||||
|
isDigest
|
||||||
|
}: Pick<TSignWithKmsDTO, "data" | "signingAlgorithm" | "isDigest">) => {
|
||||||
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
const signature = await sign(data, kmsKey, signingAlgorithm, isDigest);
|
||||||
|
|
||||||
|
return Promise.resolve({ signature, algorithm: signingAlgorithm });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyWithKmsKey = async ({
|
||||||
|
kmsId,
|
||||||
|
signingAlgorithm
|
||||||
|
}: Pick<TVerifyWithKmsDTO, "kmsId" | "signingAlgorithm">) => {
|
||||||
|
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||||
|
if (!kmsDoc) {
|
||||||
|
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.SIGN_VERIFY
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
const { verify, getPublicKeyFromPrivateKey } = signingService(encryptionAlgorithm);
|
||||||
|
return async ({ data, signature, isDigest }: Pick<TVerifyWithKmsDTO, "data" | "signature" | "isDigest">) => {
|
||||||
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
const publicKey = getPublicKeyFromPrivateKey(kmsKey);
|
||||||
|
const signatureValid = await verify(data, signature, publicKey, signingAlgorithm, isDigest);
|
||||||
|
return Promise.resolve({ signatureValid, algorithm: signingAlgorithm });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
|
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
|
||||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
|
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
|
||||||
if (!kmsDoc) {
|
if (!kmsDoc) {
|
||||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
|
||||||
|
});
|
||||||
|
|
||||||
if (kmsDoc.externalKms) {
|
if (kmsDoc.externalKms) {
|
||||||
let externalKms: TExternalKmsProviderFns;
|
let externalKms: TExternalKmsProviderFns;
|
||||||
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
|
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
|
||||||
@@ -454,8 +569,8 @@ export const kmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// internal KMS
|
// internal KMS
|
||||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||||
const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
|
const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
|
||||||
@@ -729,7 +844,7 @@ export const kmsServiceFactory = ({
|
|||||||
|
|
||||||
// case 2: root key is encrypted with software encryption
|
// case 2: root key is encrypted with software encryption
|
||||||
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
|
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||||
|
|
||||||
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
||||||
@@ -749,7 +864,7 @@ export const kmsServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (strategy === RootKeyEncryptionStrategy.Software) {
|
if (strategy === RootKeyEncryptionStrategy.Software) {
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||||
|
|
||||||
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
|
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
|
||||||
@@ -765,7 +880,7 @@ export const kmsServiceFactory = ({
|
|||||||
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
|
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
|
||||||
const dataKey = await $getDataKey(encryptionContext, trx);
|
const dataKey = await $getDataKey(encryptionContext, trx);
|
||||||
|
|
||||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||||
@@ -966,6 +1081,7 @@ export const kmsServiceFactory = ({
|
|||||||
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
|
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
|
||||||
|
|
||||||
logger.info("KMS: Loading ROOT Key into Memory.");
|
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||||
|
|
||||||
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1014,6 +1130,9 @@ export const kmsServiceFactory = ({
|
|||||||
getKmsById,
|
getKmsById,
|
||||||
createCipherPairWithDataKey,
|
createCipherPairWithDataKey,
|
||||||
getKeyMaterial,
|
getKeyMaterial,
|
||||||
importKeyMaterial
|
importKeyMaterial,
|
||||||
|
signWithKmsKey,
|
||||||
|
verifyWithKmsKey,
|
||||||
|
getPublicKey
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||||
|
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
||||||
|
|
||||||
export enum KmsDataKey {
|
export enum KmsDataKey {
|
||||||
Organization,
|
Organization,
|
||||||
@@ -13,6 +14,11 @@ export enum KmsType {
|
|||||||
Internal = "internal"
|
Internal = "internal"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum KmsKeyUsage {
|
||||||
|
ENCRYPT_DECRYPT = "encrypt-decrypt",
|
||||||
|
SIGN_VERIFY = "sign-verify"
|
||||||
|
}
|
||||||
|
|
||||||
export type TEncryptWithKmsDataKeyDTO =
|
export type TEncryptWithKmsDataKeyDTO =
|
||||||
| { type: KmsDataKey.Organization; orgId: string }
|
| { type: KmsDataKey.Organization; orgId: string }
|
||||||
| { type: KmsDataKey.SecretManager; projectId: string };
|
| { type: KmsDataKey.SecretManager; projectId: string };
|
||||||
@@ -25,7 +31,8 @@ export type TEncryptWithKmsDataKeyDTO =
|
|||||||
export type TGenerateKMSDTO = {
|
export type TGenerateKMSDTO = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
encryptionAlgorithm?: SymmetricEncryption;
|
encryptionAlgorithm?: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||||
|
keyUsage?: KmsKeyUsage;
|
||||||
isReserved?: boolean;
|
isReserved?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -37,6 +44,25 @@ export type TEncryptWithKmsDTO = {
|
|||||||
plainText: Buffer;
|
plainText: Buffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetPublicKeyDTO = {
|
||||||
|
kmsId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSignWithKmsDTO = {
|
||||||
|
kmsId: string;
|
||||||
|
data: Buffer;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
isDigest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyWithKmsDTO = {
|
||||||
|
kmsId: string;
|
||||||
|
data: Buffer;
|
||||||
|
signature: Buffer;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
isDigest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type TEncryptionWithKeyDTO = {
|
export type TEncryptionWithKeyDTO = {
|
||||||
key: Buffer;
|
key: Buffer;
|
||||||
plainText: Buffer;
|
plainText: Buffer;
|
||||||
@@ -67,9 +93,10 @@ export type TGetKeyMaterialDTO = {
|
|||||||
|
|
||||||
export type TImportKeyMaterialDTO = {
|
export type TImportKeyMaterialDTO = {
|
||||||
key: Buffer;
|
key: Buffer;
|
||||||
algorithm: SymmetricEncryption;
|
algorithm: SymmetricKeyAlgorithm;
|
||||||
name?: string;
|
name?: string;
|
||||||
isReserved: boolean;
|
isReserved: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
keyUsage: KmsKeyUsage;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Retrieve Public Key"
|
||||||
|
openapi: "GET /api/v1/kms/keys/{keyId}/public-key"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Sign Data"
|
||||||
|
openapi: "POST /api/v1/kms/keys/{keyId}/sign"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List Signing Algorithms"
|
||||||
|
openapi: "GET /api/v1/kms/keys/{keyId}/signing-algorithms"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Verify Signature"
|
||||||
|
openapi: "POST /api/v1/kms/keys/{keyId}/verify"
|
||||||
|
---
|
||||||
+17
-3
@@ -1319,9 +1319,23 @@
|
|||||||
"api-reference/endpoints/kms/keys/get-by-name",
|
"api-reference/endpoints/kms/keys/get-by-name",
|
||||||
"api-reference/endpoints/kms/keys/create",
|
"api-reference/endpoints/kms/keys/create",
|
||||||
"api-reference/endpoints/kms/keys/update",
|
"api-reference/endpoints/kms/keys/update",
|
||||||
"api-reference/endpoints/kms/keys/delete",
|
"api-reference/endpoints/kms/keys/delete"
|
||||||
"api-reference/endpoints/kms/keys/encrypt",
|
]
|
||||||
"api-reference/endpoints/kms/keys/decrypt"
|
},
|
||||||
|
{
|
||||||
|
"group": "Encryption",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/kms/encryption/encrypt",
|
||||||
|
"api-reference/endpoints/kms/encryption/decrypt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Signing",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/kms/signing/sign",
|
||||||
|
"api-reference/endpoints/kms/signing/verify",
|
||||||
|
"api-reference/endpoints/kms/signing/public-key",
|
||||||
|
"api-reference/endpoints/kms/signing/signing-algorithms"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ export enum ProjectPermissionCmekActions {
|
|||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
Encrypt = "encrypt",
|
Encrypt = "encrypt",
|
||||||
Decrypt = "decrypt"
|
Decrypt = "decrypt",
|
||||||
|
Sign = "sign",
|
||||||
|
Verify = "verify"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionKmipActions {
|
export enum ProjectPermissionKmipActions {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { AsymmetricKeyAlgorithm, KmsKeyUsage, SymmetricKeyAlgorithm } from "@app/hooks/api/cmeks";
|
||||||
|
|
||||||
|
export const kmsKeyUsageOptions: Record<
|
||||||
|
KmsKeyUsage,
|
||||||
|
{
|
||||||
|
label: string;
|
||||||
|
tooltip: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
[KmsKeyUsage.ENCRYPT_DECRYPT]: {
|
||||||
|
label: "Encrypt/Decrypt",
|
||||||
|
tooltip: "Use the key only to encrypt and decrypt data."
|
||||||
|
},
|
||||||
|
[KmsKeyUsage.SIGN_VERIFY]: {
|
||||||
|
label: "Sign/Verify",
|
||||||
|
tooltip:
|
||||||
|
"Key pairs for digital signing. Uses the private key for signing and the public key for verification."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const keyUsageDefaultOption: Record<
|
||||||
|
KmsKeyUsage,
|
||||||
|
SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm
|
||||||
|
> = {
|
||||||
|
[KmsKeyUsage.ENCRYPT_DECRYPT]: SymmetricKeyAlgorithm.AES_GCM_256,
|
||||||
|
[KmsKeyUsage.SIGN_VERIFY]: AsymmetricKeyAlgorithm.RSA_4096
|
||||||
|
};
|
||||||
@@ -106,6 +106,10 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.GET_CMEK]: "Get KMS key",
|
[EventType.GET_CMEK]: "Get KMS key",
|
||||||
[EventType.CMEK_ENCRYPT]: "Encrypt with KMS key",
|
[EventType.CMEK_ENCRYPT]: "Encrypt with KMS key",
|
||||||
[EventType.CMEK_DECRYPT]: "Decrypt with KMS key",
|
[EventType.CMEK_DECRYPT]: "Decrypt with KMS key",
|
||||||
|
[EventType.CMEK_SIGN]: "Sign with KMS key",
|
||||||
|
[EventType.CMEK_VERIFY]: "Verify with KMS key",
|
||||||
|
[EventType.CMEK_LIST_SIGNING_ALGORITHMS]: "List signing algorithms for KMS key",
|
||||||
|
[EventType.CMEK_GET_PUBLIC_KEY]: "Get public key for KMS key",
|
||||||
[EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]:
|
[EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]:
|
||||||
"Update SSO group to organization role mapping",
|
"Update SSO group to organization role mapping",
|
||||||
[EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]: "List SSO group to organization role mapping",
|
[EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]: "List SSO group to organization role mapping",
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ export enum EventType {
|
|||||||
GET_CMEK = "get-cmek",
|
GET_CMEK = "get-cmek",
|
||||||
CMEK_ENCRYPT = "cmek-encrypt",
|
CMEK_ENCRYPT = "cmek-encrypt",
|
||||||
CMEK_DECRYPT = "cmek-decrypt",
|
CMEK_DECRYPT = "cmek-decrypt",
|
||||||
|
CMEK_SIGN = "cmek-sign",
|
||||||
|
CMEK_VERIFY = "cmek-verify",
|
||||||
|
CMEK_LIST_SIGNING_ALGORITHMS = "cmek-list-signing-algorithms",
|
||||||
|
CMEK_GET_PUBLIC_KEY = "cmek-get-public-key",
|
||||||
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||||
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
||||||
GET_PROJECT_TEMPLATES = "get-project-templates",
|
GET_PROJECT_TEMPLATES = "get-project-templates",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import {
|
|||||||
TCmekDecryptResponse,
|
TCmekDecryptResponse,
|
||||||
TCmekEncrypt,
|
TCmekEncrypt,
|
||||||
TCmekEncryptResponse,
|
TCmekEncryptResponse,
|
||||||
|
TCmekSign,
|
||||||
|
TCmekSignResponse,
|
||||||
|
TCmekVerify,
|
||||||
|
TCmekVerifyResponse,
|
||||||
TCreateCmek,
|
TCreateCmek,
|
||||||
TDeleteCmek,
|
TDeleteCmek,
|
||||||
TUpdateCmek
|
TUpdateCmek
|
||||||
@@ -74,6 +78,44 @@ export const useCmekEncrypt = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCmekSign = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
keyId,
|
||||||
|
data,
|
||||||
|
signingAlgorithm,
|
||||||
|
isBase64Encoded
|
||||||
|
}: TCmekSign & { isBase64Encoded: boolean }) => {
|
||||||
|
const res = await apiRequest.post<TCmekSignResponse>(`/api/v1/kms/keys/${keyId}/sign`, {
|
||||||
|
data: isBase64Encoded ? data : encodeBase64(Buffer.from(data)),
|
||||||
|
signingAlgorithm
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCmekVerify = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
keyId,
|
||||||
|
data,
|
||||||
|
signature,
|
||||||
|
signingAlgorithm,
|
||||||
|
isBase64Encoded
|
||||||
|
}: TCmekVerify & { isBase64Encoded: boolean }) => {
|
||||||
|
const res = await apiRequest.post<TCmekVerifyResponse>(`/api/v1/kms/keys/${keyId}/verify`, {
|
||||||
|
data: isBase64Encoded ? data : encodeBase64(Buffer.from(data)),
|
||||||
|
signature,
|
||||||
|
signingAlgorithm
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useCmekDecrypt = () => {
|
export const useCmekDecrypt = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => {
|
mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => {
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
|
export enum KmsKeyUsage {
|
||||||
|
ENCRYPT_DECRYPT = "encrypt-decrypt",
|
||||||
|
SIGN_VERIFY = "sign-verify"
|
||||||
|
}
|
||||||
|
|
||||||
export type TCmek = {
|
export type TCmek = {
|
||||||
id: string;
|
id: string;
|
||||||
|
keyUsage: KmsKeyUsage;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
encryptionAlgorithm: EncryptionAlgorithm;
|
encryptionAlgorithm: AsymmetricKeyAlgorithm | SymmetricKeyAlgorithm;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
isReserved: boolean;
|
isReserved: boolean;
|
||||||
@@ -17,7 +25,8 @@ export type TCmek = {
|
|||||||
type ProjectRef = { projectId: string };
|
type ProjectRef = { projectId: string };
|
||||||
type KeyRef = { keyId: string };
|
type KeyRef = { keyId: string };
|
||||||
|
|
||||||
export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm"> & ProjectRef;
|
export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm" | "keyUsage"> &
|
||||||
|
ProjectRef;
|
||||||
export type TUpdateCmek = KeyRef &
|
export type TUpdateCmek = KeyRef &
|
||||||
Partial<Pick<TCmek, "name" | "description" | "isDisabled">> &
|
Partial<Pick<TCmek, "name" | "description" | "isDisabled">> &
|
||||||
ProjectRef;
|
ProjectRef;
|
||||||
@@ -26,6 +35,13 @@ export type TDeleteCmek = KeyRef & ProjectRef;
|
|||||||
export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean };
|
export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean };
|
||||||
export type TCmekDecrypt = KeyRef & { ciphertext: string };
|
export type TCmekDecrypt = KeyRef & { ciphertext: string };
|
||||||
|
|
||||||
|
export type TCmekSign = KeyRef & { data: string; signingAlgorithm: SigningAlgorithm };
|
||||||
|
export type TCmekVerify = KeyRef & {
|
||||||
|
data: string;
|
||||||
|
signature: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
};
|
||||||
|
|
||||||
export type TProjectCmeksList = {
|
export type TProjectCmeksList = {
|
||||||
keys: TCmek[];
|
keys: TCmek[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
@@ -44,6 +60,18 @@ export type TCmekEncryptResponse = {
|
|||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCmekSignResponse = {
|
||||||
|
signature: string;
|
||||||
|
keyId: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCmekVerifyResponse = {
|
||||||
|
signatureValid: boolean;
|
||||||
|
keyId: string;
|
||||||
|
signingAlgorithm: SigningAlgorithm;
|
||||||
|
};
|
||||||
|
|
||||||
export type TCmekDecryptResponse = {
|
export type TCmekDecryptResponse = {
|
||||||
plaintext: string;
|
plaintext: string;
|
||||||
};
|
};
|
||||||
@@ -52,7 +80,35 @@ export enum CmekOrderBy {
|
|||||||
Name = "name"
|
Name = "name"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EncryptionAlgorithm {
|
export enum AsymmetricKeyAlgorithm {
|
||||||
|
RSA_4096 = "RSA_4096",
|
||||||
|
ECC_NIST_P256 = "ECC_NIST_P256"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported symmetric encrypt/decrypt algorithms
|
||||||
|
export enum SymmetricKeyAlgorithm {
|
||||||
AES_GCM_256 = "aes-256-gcm",
|
AES_GCM_256 = "aes-256-gcm",
|
||||||
AES_GCM_128 = "aes-128-gcm"
|
AES_GCM_128 = "aes-128-gcm"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AllowedEncryptionKeyAlgorithms = z.enum([
|
||||||
|
...Object.values(SymmetricKeyAlgorithm),
|
||||||
|
...Object.values(AsymmetricKeyAlgorithm)
|
||||||
|
] as [string, ...string[]]).options;
|
||||||
|
|
||||||
|
export enum SigningAlgorithm {
|
||||||
|
// RSA PSS algorithms
|
||||||
|
RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256",
|
||||||
|
RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384",
|
||||||
|
RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512",
|
||||||
|
|
||||||
|
// RSA PKCS#1 v1.5 algorithms
|
||||||
|
RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256",
|
||||||
|
RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384",
|
||||||
|
RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512",
|
||||||
|
|
||||||
|
// ECDSA algorithms
|
||||||
|
ECDSA_SHA_256 = "ECDSA_SHA_256",
|
||||||
|
ECDSA_SHA_384 = "ECDSA_SHA_384",
|
||||||
|
ECDSA_SHA_512 = "ECDSA_SHA_512"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
const base64WithPadding =
|
||||||
|
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
|
||||||
|
|
||||||
|
export const isBase64 = (str: string): boolean => {
|
||||||
|
if (typeof str !== "string") {
|
||||||
|
throw new TypeError("Expected a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str === "") return true;
|
||||||
|
|
||||||
|
const regex = base64WithPadding;
|
||||||
|
|
||||||
|
return regex.test(str);
|
||||||
|
};
|
||||||
@@ -15,13 +15,23 @@ import {
|
|||||||
TextArea
|
TextArea
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { EncryptionAlgorithm, TCmek, useCreateCmek, useUpdateCmek } from "@app/hooks/api/cmeks";
|
import { keyUsageDefaultOption, kmsKeyUsageOptions } from "@app/helpers/kms";
|
||||||
|
import {
|
||||||
|
AllowedEncryptionKeyAlgorithms,
|
||||||
|
AsymmetricKeyAlgorithm,
|
||||||
|
KmsKeyUsage,
|
||||||
|
SymmetricKeyAlgorithm,
|
||||||
|
TCmek,
|
||||||
|
useCreateCmek,
|
||||||
|
useUpdateCmek
|
||||||
|
} from "@app/hooks/api/cmeks";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: slugSchema({ min: 1, max: 32, field: "Name" }),
|
name: slugSchema({ min: 1, max: 32, field: "Name" }),
|
||||||
description: z.string().max(500).optional(),
|
description: z.string().max(500).optional(),
|
||||||
encryptionAlgorithm: z.nativeEnum(EncryptionAlgorithm)
|
encryptionAlgorithm: z.enum(AllowedEncryptionKeyAlgorithms),
|
||||||
|
keyUsage: z.nativeEnum(KmsKeyUsage)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof formSchema>;
|
export type FormData = z.infer<typeof formSchema>;
|
||||||
@@ -47,24 +57,33 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
|
|||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
formState: { isSubmitting, errors }
|
formState: { isSubmitting, errors }
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: cmek?.name,
|
name: cmek?.name,
|
||||||
description: cmek?.description,
|
description: cmek?.description,
|
||||||
encryptionAlgorithm: EncryptionAlgorithm.AES_GCM_256
|
encryptionAlgorithm: SymmetricKeyAlgorithm.AES_GCM_256,
|
||||||
|
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateCmek = async ({ encryptionAlgorithm, name, description }: FormData) => {
|
const handleCreateCmek = async ({
|
||||||
|
encryptionAlgorithm,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
keyUsage
|
||||||
|
}: FormData) => {
|
||||||
const mutation = isUpdate
|
const mutation = isUpdate
|
||||||
? updateCmek.mutateAsync({ keyId: cmek.id, projectId, name, description })
|
? updateCmek.mutateAsync({ keyId: cmek.id, projectId, name, description })
|
||||||
: createCmek.mutateAsync({
|
: createCmek.mutateAsync({
|
||||||
projectId,
|
projectId,
|
||||||
encryptionAlgorithm,
|
|
||||||
name,
|
name,
|
||||||
description
|
description,
|
||||||
|
keyUsage,
|
||||||
|
encryptionAlgorithm: encryptionAlgorithm as AsymmetricKeyAlgorithm | SymmetricKeyAlgorithm
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -83,6 +102,8 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedKeyUsage = watch("keyUsage");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleCreateCmek)}>
|
<form onSubmit={handleSubmit(handleCreateCmek)}>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -93,23 +114,97 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
|
|||||||
>
|
>
|
||||||
<Input autoFocus placeholder="my-secret-key" {...register("name")} />
|
<Input autoFocus placeholder="my-secret-key" {...register("name")} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{!isUpdate && (
|
<div className="flex w-full items-center gap-2">
|
||||||
<Controller
|
{!isUpdate && (
|
||||||
control={control}
|
<>
|
||||||
name="encryptionAlgorithm"
|
<Controller
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
control={control}
|
||||||
<FormControl label="Algorithm" errorText={error?.message} isError={Boolean(error)}>
|
name="keyUsage"
|
||||||
<Select defaultValue={field.value} onValueChange={onChange} className="w-full">
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
{Object.entries(EncryptionAlgorithm)?.map(([key, value]) => (
|
<FormControl
|
||||||
<SelectItem value={value} key={`source-environment-${key}`}>
|
className="w-full"
|
||||||
{key.replaceAll("_", "-")}
|
tooltipText={
|
||||||
</SelectItem>
|
<div className="space-y-4">
|
||||||
))}
|
{Object.entries(KmsKeyUsage).map(([key, value]) => (
|
||||||
</Select>
|
<div key={`key-usage-${key}`}>
|
||||||
</FormControl>
|
<p className="font-bold">{kmsKeyUsageOptions[value].label}</p>
|
||||||
)}
|
<p>{kmsKeyUsageOptions[value].tooltip}</p>
|
||||||
/>
|
</div>
|
||||||
)}
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
label="Key Usage"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
if (keyUsageDefaultOption[e as KmsKeyUsage]) {
|
||||||
|
setValue("encryptionAlgorithm", keyUsageDefaultOption[e as KmsKeyUsage], {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{Object.entries(KmsKeyUsage)?.map(([key, value]) => (
|
||||||
|
<SelectItem value={value} key={`key-usage-${key}`}>
|
||||||
|
{kmsKeyUsageOptions[value].label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="encryptionAlgorithm"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="w-full"
|
||||||
|
label="Algorithm"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{Object.entries(AllowedEncryptionKeyAlgorithms)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
?.filter(([_, value]) => {
|
||||||
|
if (selectedKeyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
|
||||||
|
return Object.values(SymmetricKeyAlgorithm).includes(
|
||||||
|
value as unknown as SymmetricKeyAlgorithm
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (selectedKeyUsage === KmsKeyUsage.SIGN_VERIFY) {
|
||||||
|
return Object.values(AsymmetricKeyAlgorithm).includes(
|
||||||
|
value as unknown as AsymmetricKeyAlgorithm
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
.map(([_, value]) => (
|
||||||
|
<SelectItem value={value} key={`encryption-algorithm-${value}`}>
|
||||||
|
<span className="uppercase">{value.replaceAll("-", " ")}</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Description (optional)"
|
label="Description (optional)"
|
||||||
errorText={errors.description?.message}
|
errorText={errors.description?.message}
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheckCircle, faFileSignature, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch,
|
||||||
|
TextArea,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useTimedReset } from "@app/hooks";
|
||||||
|
import { SigningAlgorithm, TCmek, useCmekSign } from "@app/hooks/api/cmeks";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
data: z.string(),
|
||||||
|
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
|
||||||
|
isBase64Encoded: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
cmek: TCmek;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormProps = Pick<Props, "cmek">;
|
||||||
|
|
||||||
|
const SignForm = ({ cmek }: FormProps) => {
|
||||||
|
const cmekSign = useCmekSign();
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting, errors }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
signingAlgorithm: cmek?.encryptionAlgorithm?.startsWith("RSA")
|
||||||
|
? SigningAlgorithm.RSASSA_PSS_SHA_512
|
||||||
|
: SigningAlgorithm.ECDSA_SHA_256,
|
||||||
|
isBase64Encoded: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [copySignature, isCopyingSignature, setCopySignature] = useTimedReset<string>({
|
||||||
|
initialState: "Copy to Clipboard"
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSignData = async (formData: FormData) => {
|
||||||
|
try {
|
||||||
|
await cmekSign.mutateAsync({ ...formData, keyId: cmek.id });
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully signed data",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to sign data",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const signature = cmekSign.data?.signature;
|
||||||
|
|
||||||
|
const handleCopyToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(signature ?? "");
|
||||||
|
|
||||||
|
setCopySignature("Copied to Clipboard");
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowedSigningAlgorithms = Object.values(SigningAlgorithm).filter((a) =>
|
||||||
|
cmek?.encryptionAlgorithm?.startsWith("RSA")
|
||||||
|
? a.toLowerCase().startsWith("rsa")
|
||||||
|
: a.toLowerCase().startsWith("ecdsa")
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleSignData)}>
|
||||||
|
{signature ? (
|
||||||
|
<FormControl label="Data Signature">
|
||||||
|
<TextArea
|
||||||
|
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
|
||||||
|
isDisabled
|
||||||
|
value={signature}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FormControl
|
||||||
|
label="Data to Sign"
|
||||||
|
errorText={errors.data?.message}
|
||||||
|
isError={Boolean(errors.data)}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...register("data")}
|
||||||
|
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<div className="mb-6 flex w-full items-center justify-between gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="signingAlgorithm"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<FormControl label="Signing Algorithm">
|
||||||
|
<Select onValueChange={onChange} value={value} className="w-full">
|
||||||
|
{allowedSigningAlgorithms.map((a) => (
|
||||||
|
<SelectItem key={a} value={a}>
|
||||||
|
{a.replaceAll("_", " ")}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="isBase64Encoded"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch id="encode-base-64" isChecked={value} onCheckedChange={onChange}>
|
||||||
|
Data is Base64 encoded{" "}
|
||||||
|
<Tooltip content="Toggle this switch on if your data is already Base64 encoded to avoid redundant encoding.">
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className={`mr-4 ${signature ? "w-44" : ""}`}
|
||||||
|
size="sm"
|
||||||
|
leftIcon={
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
signature ? (
|
||||||
|
isCopyingSignature ? (
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={faFileSignature} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={faFileSignature} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={signature ? handleCopyToClipboard : undefined}
|
||||||
|
type={signature ? "button" : "submit"}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{signature ? copySignature : "Sign"}
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
{signature ? "Close" : "Cancel"}
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CmekSignModal = ({ isOpen, onOpenChange, cmek }: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
title="Sign Data"
|
||||||
|
subTitle={
|
||||||
|
<>
|
||||||
|
Sign data using <span className="font-bold">{cmek?.name}</span>. Returns a Base64
|
||||||
|
encoded signature.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SignForm cmek={cmek} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
faCopy,
|
faCopy,
|
||||||
faEdit,
|
faEdit,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
|
faFileSignature,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faKey,
|
faKey,
|
||||||
faLock,
|
faLock,
|
||||||
@@ -51,14 +52,17 @@ import {
|
|||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
|
import { kmsKeyUsageOptions } from "@app/helpers/kms";
|
||||||
import { usePagination, usePopUp, useResetPageHelper, useTimedReset } from "@app/hooks";
|
import { usePagination, usePopUp, useResetPageHelper, useTimedReset } from "@app/hooks";
|
||||||
import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks";
|
import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks";
|
||||||
import { CmekOrderBy, TCmek } from "@app/hooks/api/cmeks/types";
|
import { CmekOrderBy, KmsKeyUsage, TCmek } from "@app/hooks/api/cmeks/types";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
import { CmekDecryptModal } from "./CmekDecryptModal";
|
import { CmekDecryptModal } from "./CmekDecryptModal";
|
||||||
import { CmekEncryptModal } from "./CmekEncryptModal";
|
import { CmekEncryptModal } from "./CmekEncryptModal";
|
||||||
import { CmekModal } from "./CmekModal";
|
import { CmekModal } from "./CmekModal";
|
||||||
|
import { CmekSignModal } from "./CmekSignModal";
|
||||||
|
import { CmekVerifyModal } from "./CmekVerifyModal";
|
||||||
import { DeleteCmekModal } from "./DeleteCmekModal";
|
import { DeleteCmekModal } from "./DeleteCmekModal";
|
||||||
|
|
||||||
const getStatusBadgeProps = (
|
const getStatusBadgeProps = (
|
||||||
@@ -123,7 +127,9 @@ export const CmekTable = () => {
|
|||||||
"upsertKey",
|
"upsertKey",
|
||||||
"deleteKey",
|
"deleteKey",
|
||||||
"encryptData",
|
"encryptData",
|
||||||
"decryptData"
|
"decryptData",
|
||||||
|
"signData",
|
||||||
|
"verifyData"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const handleSort = () => {
|
const handleSort = () => {
|
||||||
@@ -179,6 +185,15 @@ export const CmekTable = () => {
|
|||||||
ProjectPermissionSub.Cmek
|
ProjectPermissionSub.Cmek
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const cannotSignData = permission.cannot(
|
||||||
|
ProjectPermissionCmekActions.Sign,
|
||||||
|
ProjectPermissionSub.Cmek
|
||||||
|
);
|
||||||
|
|
||||||
|
const cannotVerifyData = permission.cannot(
|
||||||
|
ProjectPermissionCmekActions.Verify,
|
||||||
|
ProjectPermissionSub.Cmek
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="kms-keys-tab"
|
key="kms-keys-tab"
|
||||||
@@ -246,6 +261,7 @@ export const CmekTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
<Th>Key ID</Th>
|
<Th>Key ID</Th>
|
||||||
|
<Th>Key Usage</Th>
|
||||||
<Th>Algorithm</Th>
|
<Th>Algorithm</Th>
|
||||||
<Th>Status</Th>
|
<Th>Status</Th>
|
||||||
<Th>Version</Th>
|
<Th>Version</Th>
|
||||||
@@ -257,7 +273,15 @@ export const CmekTable = () => {
|
|||||||
{!isPending &&
|
{!isPending &&
|
||||||
keys.length > 0 &&
|
keys.length > 0 &&
|
||||||
keys.map((cmek) => {
|
keys.map((cmek) => {
|
||||||
const { name, id, version, description, encryptionAlgorithm, isDisabled } = cmek;
|
const {
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
description,
|
||||||
|
encryptionAlgorithm,
|
||||||
|
isDisabled,
|
||||||
|
keyUsage
|
||||||
|
} = cmek;
|
||||||
const { variant, label } = getStatusBadgeProps(isDisabled);
|
const { variant, label } = getStatusBadgeProps(isDisabled);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -295,6 +319,14 @@ export const CmekTable = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{kmsKeyUsageOptions[keyUsage].label}
|
||||||
|
<Tooltip content={kmsKeyUsageOptions[keyUsage].tooltip}>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
<Td className="uppercase">{encryptionAlgorithm}</Td>
|
<Td className="uppercase">{encryptionAlgorithm}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Badge variant={variant}>{label}</Badge>
|
<Badge variant={variant}>{label}</Badge>
|
||||||
@@ -314,50 +346,104 @@ export const CmekTable = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="min-w-[160px]">
|
<DropdownMenuContent className="min-w-[160px]">
|
||||||
<Tooltip
|
{keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT && (
|
||||||
content={
|
<>
|
||||||
// eslint-disable-next-line no-nested-ternary
|
<Tooltip
|
||||||
cannotEncryptData
|
content={
|
||||||
? "Access Restricted"
|
// eslint-disable-next-line no-nested-ternary
|
||||||
: isDisabled
|
cannotEncryptData
|
||||||
? "Key Disabled"
|
? "Access Restricted"
|
||||||
: ""
|
: isDisabled
|
||||||
}
|
? "Key Disabled"
|
||||||
position="left"
|
: ""
|
||||||
>
|
}
|
||||||
<div>
|
position="left"
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handlePopUpOpen("encryptData", cmek)}
|
|
||||||
icon={<FontAwesomeIcon icon={faLock} />}
|
|
||||||
iconPos="left"
|
|
||||||
isDisabled={cannotEncryptData || isDisabled}
|
|
||||||
>
|
>
|
||||||
Encrypt Data
|
<div>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem
|
||||||
</div>
|
onClick={() => handlePopUpOpen("encryptData", cmek)}
|
||||||
</Tooltip>
|
icon={<FontAwesomeIcon icon={faLock} />}
|
||||||
<Tooltip
|
iconPos="left"
|
||||||
content={
|
isDisabled={cannotEncryptData || isDisabled}
|
||||||
// eslint-disable-next-line no-nested-ternary
|
>
|
||||||
cannotDecryptData
|
Encrypt Data
|
||||||
? "Access Restricted"
|
</DropdownMenuItem>
|
||||||
: isDisabled
|
</div>
|
||||||
? "Key Disabled"
|
</Tooltip>
|
||||||
: ""
|
<Tooltip
|
||||||
}
|
content={
|
||||||
position="left"
|
// eslint-disable-next-line no-nested-ternary
|
||||||
>
|
cannotDecryptData
|
||||||
<div>
|
? "Access Restricted"
|
||||||
<DropdownMenuItem
|
: isDisabled
|
||||||
onClick={() => handlePopUpOpen("decryptData", cmek)}
|
? "Key Disabled"
|
||||||
icon={<FontAwesomeIcon icon={faLockOpen} />}
|
: ""
|
||||||
iconPos="left"
|
}
|
||||||
isDisabled={cannotDecryptData || isDisabled}
|
position="left"
|
||||||
>
|
>
|
||||||
Decrypt Data
|
<div>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem
|
||||||
</div>
|
onClick={() => handlePopUpOpen("decryptData", cmek)}
|
||||||
</Tooltip>
|
icon={<FontAwesomeIcon icon={faLockOpen} />}
|
||||||
|
iconPos="left"
|
||||||
|
isDisabled={cannotDecryptData || isDisabled}
|
||||||
|
>
|
||||||
|
Decrypt Data
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{keyUsage === KmsKeyUsage.SIGN_VERIFY && (
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
cannotSignData
|
||||||
|
? "Access Restricted"
|
||||||
|
: isDisabled
|
||||||
|
? "Key Disabled"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
position="left"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handlePopUpOpen("signData", cmek)}
|
||||||
|
icon={<FontAwesomeIcon icon={faFileSignature} />}
|
||||||
|
iconPos="left"
|
||||||
|
isDisabled={cannotSignData || isDisabled}
|
||||||
|
>
|
||||||
|
Sign Data
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
cannotVerifyData
|
||||||
|
? "Access Restricted"
|
||||||
|
: isDisabled
|
||||||
|
? "Key Disabled"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
position="left"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handlePopUpOpen("verifyData", cmek)}
|
||||||
|
icon={<FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="left"
|
||||||
|
isDisabled={cannotVerifyData || isDisabled}
|
||||||
|
>
|
||||||
|
Verify Data
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={cannotEditKey ? "Access Restricted" : ""}
|
content={cannotEditKey ? "Access Restricted" : ""}
|
||||||
position="left"
|
position="left"
|
||||||
@@ -456,6 +542,16 @@ export const CmekTable = () => {
|
|||||||
onOpenChange={(isOpen) => handlePopUpToggle("decryptData", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("decryptData", isOpen)}
|
||||||
cmek={popUp.decryptData.data as TCmek}
|
cmek={popUp.decryptData.data as TCmek}
|
||||||
/>
|
/>
|
||||||
|
<CmekSignModal
|
||||||
|
isOpen={popUp.signData.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("signData", isOpen)}
|
||||||
|
cmek={popUp.signData.data as TCmek}
|
||||||
|
/>
|
||||||
|
<CmekVerifyModal
|
||||||
|
isOpen={popUp.verifyData.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("verifyData", isOpen)}
|
||||||
|
cmek={popUp.verifyData.data as TCmek}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faFileSignature, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { decodeBase64 } from "tweetnacl-util";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch,
|
||||||
|
TextArea,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { SigningAlgorithm, TCmek, useCmekVerify } from "@app/hooks/api/cmeks";
|
||||||
|
import { isBase64 } from "@app/lib/fn/base64";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
data: z.string().min(1, { message: "Data cannot be empty" }),
|
||||||
|
signature: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Signature cannot be empty" })
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!isBase64(val)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Signature must be base64-encoded"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
|
||||||
|
isBase64Encoded: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
cmek: TCmek;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormProps = Pick<Props, "cmek">;
|
||||||
|
|
||||||
|
const VerifyForm = ({ cmek }: FormProps) => {
|
||||||
|
const cmekVerify = useCmekVerify();
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
watch,
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting, errors }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
signingAlgorithm: cmek?.encryptionAlgorithm?.startsWith("RSA")
|
||||||
|
? SigningAlgorithm.RSASSA_PSS_SHA_512
|
||||||
|
: SigningAlgorithm.ECDSA_SHA_256,
|
||||||
|
isBase64Encoded: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleVerifyData = async (formData: FormData) => {
|
||||||
|
try {
|
||||||
|
const result = await cmekVerify.mutateAsync({ ...formData, keyId: cmek.id });
|
||||||
|
|
||||||
|
if (result.signatureValid) {
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully verified signature",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createNotification({
|
||||||
|
title: "Signature Verification Failed",
|
||||||
|
text: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to sign data",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const signature = watch("signature");
|
||||||
|
const data = watch("data");
|
||||||
|
const isBase64Encoded = watch("isBase64Encoded");
|
||||||
|
|
||||||
|
const signatureValid = cmekVerify.data?.signatureValid;
|
||||||
|
const signingAlgorithm = cmekVerify.data?.signingAlgorithm;
|
||||||
|
|
||||||
|
const allowedSigningAlgorithms = Object.values(SigningAlgorithm).filter((a) =>
|
||||||
|
cmek?.encryptionAlgorithm?.startsWith("RSA")
|
||||||
|
? a.toLowerCase().startsWith("rsa")
|
||||||
|
: a.toLowerCase().startsWith("ecdsa")
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleVerifyData)}>
|
||||||
|
{signatureValid !== undefined ? (
|
||||||
|
<div className="mb-6 flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between space-x-2">
|
||||||
|
<span className="text-sm opacity-60">Signature Status:</span>
|
||||||
|
<Badge variant={signatureValid ? "success" : "danger"}>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
signatureValid
|
||||||
|
? "The signature is valid. signature was created using the same signing algorithm and key as the one used to sign the data."
|
||||||
|
: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with."
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{signatureValid ? (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<p>Valid</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<p>Invalid</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-sm opacity-60">Signing Algorithm:</span>
|
||||||
|
<Badge variant="primary">{signingAlgorithm}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<span className="text-sm opacity-60">Signature:</span>{" "}
|
||||||
|
<div className="whitespace-pre-wrap break-words rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 text-sm">
|
||||||
|
{signature}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm opacity-60">Data:</span>{" "}
|
||||||
|
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 text-sm">
|
||||||
|
{isBase64Encoded ? decodeBase64(data).toString() : data}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FormControl
|
||||||
|
label="Data to Verify"
|
||||||
|
errorText={errors.data?.message}
|
||||||
|
isError={Boolean(errors.data)}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...register("data")}
|
||||||
|
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl
|
||||||
|
label="Signature of Data"
|
||||||
|
tooltipText="Must be base64-encoded, like the signature you received when you signed the data."
|
||||||
|
errorText={errors.signature?.message}
|
||||||
|
isError={Boolean(errors.signature)}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...register("signature")}
|
||||||
|
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<div className="mb-6 flex w-full items-center justify-between gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="signingAlgorithm"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<FormControl label="Signing Algorithm">
|
||||||
|
<Select onValueChange={onChange} value={value} className="w-full">
|
||||||
|
{allowedSigningAlgorithms.map((a) => (
|
||||||
|
<SelectItem key={a} value={a}>
|
||||||
|
{a.replaceAll("_", " ")}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="isBase64Encoded"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch id="encode-base-64" isChecked={value} onCheckedChange={onChange}>
|
||||||
|
Data is Base64 encoded{" "}
|
||||||
|
<Tooltip content="Toggle this switch on if your data is already Base64 encoded to avoid redundant encoding.">
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center">
|
||||||
|
{signatureValid === undefined && (
|
||||||
|
<Button
|
||||||
|
className="mr-4 w-44"
|
||||||
|
size="sm"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faFileSignature} />}
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button
|
||||||
|
colorSchema={signatureValid === undefined ? "secondary" : "primary"}
|
||||||
|
variant={signatureValid === undefined ? "plain" : undefined}
|
||||||
|
>
|
||||||
|
{signatureValid !== undefined ? "Close" : "Cancel"}
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CmekVerifyModal = ({ isOpen, onOpenChange, cmek }: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
title="Verify Signature"
|
||||||
|
subTitle={
|
||||||
|
<>
|
||||||
|
Verify a signature using <span className="font-bold">{cmek?.name}</span>.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<VerifyForm cmek={cmek} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+10
-2
@@ -47,7 +47,9 @@ const CmekPolicyActionSchema = z.object({
|
|||||||
delete: z.boolean().optional(),
|
delete: z.boolean().optional(),
|
||||||
create: z.boolean().optional(),
|
create: z.boolean().optional(),
|
||||||
encrypt: z.boolean().optional(),
|
encrypt: z.boolean().optional(),
|
||||||
decrypt: z.boolean().optional()
|
decrypt: z.boolean().optional(),
|
||||||
|
sign: z.boolean().optional(),
|
||||||
|
verify: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const DynamicSecretPolicyActionSchema = z.object({
|
const DynamicSecretPolicyActionSchema = z.object({
|
||||||
@@ -482,6 +484,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
const canCreate = action.includes(ProjectPermissionCmekActions.Create);
|
const canCreate = action.includes(ProjectPermissionCmekActions.Create);
|
||||||
const canEncrypt = action.includes(ProjectPermissionCmekActions.Encrypt);
|
const canEncrypt = action.includes(ProjectPermissionCmekActions.Encrypt);
|
||||||
const canDecrypt = action.includes(ProjectPermissionCmekActions.Decrypt);
|
const canDecrypt = action.includes(ProjectPermissionCmekActions.Decrypt);
|
||||||
|
const canSign = action.includes(ProjectPermissionCmekActions.Sign);
|
||||||
|
const canVerify = action.includes(ProjectPermissionCmekActions.Verify);
|
||||||
|
|
||||||
if (!formVal[subject]) formVal[subject] = [{}];
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
|
||||||
@@ -492,6 +496,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
if (canDelete) formVal[subject]![0].delete = true;
|
if (canDelete) formVal[subject]![0].delete = true;
|
||||||
if (canEncrypt) formVal[subject]![0].encrypt = true;
|
if (canEncrypt) formVal[subject]![0].encrypt = true;
|
||||||
if (canDecrypt) formVal[subject]![0].decrypt = true;
|
if (canDecrypt) formVal[subject]![0].decrypt = true;
|
||||||
|
if (canSign) formVal[subject]![0].sign = true;
|
||||||
|
if (canVerify) formVal[subject]![0].verify = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,7 +775,9 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
{ label: "Modify", value: "edit" },
|
{ label: "Modify", value: "edit" },
|
||||||
{ label: "Remove", value: "delete" },
|
{ label: "Remove", value: "delete" },
|
||||||
{ label: "Encrypt", value: "encrypt" },
|
{ label: "Encrypt", value: "encrypt" },
|
||||||
{ label: "Decrypt", value: "decrypt" }
|
{ label: "Decrypt", value: "decrypt" },
|
||||||
|
{ label: "Sign", value: "sign" },
|
||||||
|
{ label: "Verify", value: "verify" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[ProjectPermissionSub.Kms]: {
|
[ProjectPermissionSub.Kms]: {
|
||||||
|
|||||||
Reference in New Issue
Block a user