mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-28 18:55:53 +00:00
Compare commits
30 Commits
native-int
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
ac8b3aca60 | ||
|
4ea0cc62e3 | ||
|
bdab16f64b | ||
|
3c07204532 | ||
|
c0926bec69 | ||
|
b9d74e0aed | ||
|
f3078040fc | ||
|
f2fead7a51 | ||
|
3483ed85ff | ||
|
85627eb825 | ||
|
fcc6f812d5 | ||
|
7c38932878 | ||
|
966ca1a3c6 | ||
|
65f78c556f | ||
|
4a9e24884d | ||
|
5bc8e4729f | ||
|
041fac7f42 | ||
|
5ce738bba0 | ||
|
894633143d | ||
|
ac0f4aa8bd | ||
|
8fa8117fa1 | ||
|
939b77b050 | ||
|
83206aad93 | ||
|
cd83efb060 | ||
|
53b5497271 | ||
|
c7416c825c | ||
|
fe172e39bf | ||
|
fda77fe464 | ||
|
c4c065ea9e | ||
|
c6ca668db9 |
@@ -8,7 +8,8 @@ RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
openssh-client
|
||||
openssh-client \
|
||||
openssl
|
||||
|
||||
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||
RUN apt-get install -y \
|
||||
|
@@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y \
|
||||
make \
|
||||
g++ \
|
||||
openssh-client \
|
||||
openssl \
|
||||
curl \
|
||||
pkg-config
|
||||
|
||||
|
22
backend/package-lock.json
generated
22
backend/package-lock.json
generated
@@ -132,7 +132,7 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20.9.5",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-github": "^1.1.12",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
@@ -9753,11 +9753,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz",
|
||||
"integrity": "sha512-Uq2xbNq0chGg+/WQEU0LJTSs/1nKxz6u1iemLcGomkSnKokbW1fbLqc3HOqCf2JP7KjlL4QkS7oZZTrOQHQYgQ==",
|
||||
"version": "20.17.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
||||
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
@@ -20081,11 +20082,6 @@
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/scim-patch/node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"node_modules/scim2-parse-filter": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
|
||||
@@ -22442,9 +22438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.0",
|
||||
|
@@ -89,7 +89,7 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20.9.5",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-github": "^1.1.12",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
|
@@ -0,0 +1,25 @@
|
||||
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 hasKeyUsageColumn = await knex.schema.hasColumn(TableName.KmsKey, "keyUsage");
|
||||
|
||||
if (!hasKeyUsageColumn) {
|
||||
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||
t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasKeyUsageColumn = await knex.schema.hasColumn(TableName.KmsKey, "keyUsage");
|
||||
|
||||
if (hasKeyUsageColumn) {
|
||||
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||
t.dropColumn("keyUsage");
|
||||
});
|
||||
}
|
||||
}
|
@@ -16,7 +16,8 @@ export const KmsKeysSchema = z.object({
|
||||
name: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
projectId: z.string().nullable().optional()
|
||||
projectId: z.string().nullable().optional(),
|
||||
keyUsage: z.string().default("encrypt-decrypt")
|
||||
});
|
||||
|
||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||
|
@@ -2,7 +2,7 @@ import z from "zod";
|
||||
|
||||
import { KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -74,7 +74,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
description: "KMIP endpoint for creating managed objects",
|
||||
body: z.object({
|
||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
|
||||
}),
|
||||
response: {
|
||||
200: KmsKeysSchema
|
||||
@@ -433,7 +433,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
key: z.string(),
|
||||
name: z.string(),
|
||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||
algorithm: z.nativeEnum(SymmetricKeyAlgorithm)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -136,11 +136,12 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||
url: "/login/error",
|
||||
method: "GET",
|
||||
handler: async (req, res) => {
|
||||
const failureMessage = req.session.get<any>("messages");
|
||||
await req.session.destroy();
|
||||
|
||||
return res.status(500).send({
|
||||
error: "Authentication error",
|
||||
details: req.query
|
||||
details: failureMessage ?? req.query
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -12,7 +12,8 @@ import {
|
||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||
@@ -255,6 +256,11 @@ export enum EventType {
|
||||
GET_CMEK = "get-cmek",
|
||||
CMEK_ENCRYPT = "cmek-encrypt",
|
||||
CMEK_DECRYPT = "cmek-decrypt",
|
||||
CMEK_SIGN = "cmek-sign",
|
||||
CMEK_VERIFY = "cmek-verify",
|
||||
CMEK_LIST_SIGNING_ALGORITHMS = "cmek-list-signing-algorithms",
|
||||
CMEK_GET_PUBLIC_KEY = "cmek-get-public-key",
|
||||
|
||||
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
||||
GET_PROJECT_TEMPLATES = "get-project-templates",
|
||||
@@ -1997,7 +2003,7 @@ interface CreateCmekEvent {
|
||||
keyId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
encryptionAlgorithm: SymmetricEncryption;
|
||||
encryptionAlgorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2045,6 +2051,39 @@ interface CmekDecryptEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CmekSignEvent {
|
||||
type: EventType.CMEK_SIGN;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
signature: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CmekVerifyEvent {
|
||||
type: EventType.CMEK_VERIFY;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
signature: string;
|
||||
signatureValid: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface CmekListSigningAlgorithmsEvent {
|
||||
type: EventType.CMEK_LIST_SIGNING_ALGORITHMS;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CmekGetPublicKeyEvent {
|
||||
type: EventType.CMEK_GET_PUBLIC_KEY;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetExternalGroupOrgRoleMappingsEvent {
|
||||
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
||||
metadata?: Record<string, never>; // not needed, based off orgId
|
||||
@@ -2639,6 +2678,10 @@ export type Event =
|
||||
| GetCmeksEvent
|
||||
| CmekEncryptEvent
|
||||
| CmekDecryptEvent
|
||||
| CmekSignEvent
|
||||
| CmekVerifyEvent
|
||||
| CmekListSigningAlgorithmsEvent
|
||||
| CmekGetPublicKeyEvent
|
||||
| GetExternalGroupOrgRoleMappingsEvent
|
||||
| UpdateExternalGroupOrgRoleMappingsEvent
|
||||
| GetProjectTemplatesEvent
|
||||
|
@@ -7,7 +7,7 @@ import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/er
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { KmsDataKey, KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@@ -115,6 +115,7 @@ export const externalKmsServiceFactory = ({
|
||||
{
|
||||
isReserved: false,
|
||||
description,
|
||||
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||
name: kmsName,
|
||||
orgId: actorOrgId
|
||||
},
|
||||
|
@@ -92,7 +92,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
||||
plaintext: data
|
||||
});
|
||||
if (!encryptedText[0].ciphertext) throw new Error("encryption failed");
|
||||
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext) };
|
||||
return { encryptedBlob: Buffer.from(encryptedText[0].ciphertext as Uint8Array) };
|
||||
};
|
||||
|
||||
const decrypt = async (encryptedBlob: Buffer) => {
|
||||
@@ -101,7 +101,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
||||
ciphertext: encryptedBlob
|
||||
});
|
||||
if (!decryptedText[0].plaintext) throw new Error("decryption failed");
|
||||
return { data: Buffer.from(decryptedText[0].plaintext) };
|
||||
return { data: Buffer.from(decryptedText[0].plaintext as Uint8Array) };
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -258,7 +258,7 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
||||
const decrypt: {
|
||||
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
|
||||
(encryptedBlob: Buffer): Promise<Buffer>;
|
||||
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
|
||||
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle): Promise<Buffer> => {
|
||||
if (!pkcs11 || !isInitialized) {
|
||||
throw new Error("PKCS#11 module is not initialized");
|
||||
}
|
||||
@@ -309,10 +309,10 @@ export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 }, envCon
|
||||
|
||||
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
|
||||
|
||||
const tempBuffer = Buffer.alloc(encryptedData.length);
|
||||
const tempBuffer: Buffer = Buffer.alloc(encryptedData.length);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
|
||||
|
||||
// Create a new buffer from the decrypted data
|
||||
return Buffer.from(decryptedData);
|
||||
} catch (error) {
|
||||
logger.error(error, "HSM: Failed to perform decryption");
|
||||
|
@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@@ -403,6 +404,7 @@ export const kmipOperationServiceFactory = ({
|
||||
algorithm,
|
||||
isReserved: false,
|
||||
projectId,
|
||||
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||
orgId: project.orgId
|
||||
});
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
@@ -49,7 +49,7 @@ type KmipOperationBaseDTO = {
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TKmipCreateDTO = {
|
||||
algorithm: SymmetricEncryption;
|
||||
algorithm: SymmetricKeyAlgorithm;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipGetDTO = {
|
||||
@@ -77,7 +77,7 @@ export type TKmipLocateDTO = KmipOperationBaseDTO;
|
||||
export type TKmipRegisterDTO = {
|
||||
name: string;
|
||||
key: string;
|
||||
algorithm: SymmetricEncryption;
|
||||
algorithm: SymmetricKeyAlgorithm;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TSetupOrgKmipDTO = {
|
||||
|
@@ -32,7 +32,9 @@ export enum ProjectPermissionCmekActions {
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Encrypt = "encrypt",
|
||||
Decrypt = "decrypt"
|
||||
Decrypt = "decrypt",
|
||||
Sign = "sign",
|
||||
Verify = "verify"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionDynamicSecretActions {
|
||||
@@ -732,7 +734,9 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
@@ -935,7 +939,9 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
@@ -1672,7 +1672,8 @@ export const KMS = {
|
||||
projectId: "The ID of the project to create the key in.",
|
||||
name: "The name of the key to be created. Must be slug-friendly.",
|
||||
description: "An optional description of the key.",
|
||||
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
||||
encryptionAlgorithm: "The algorithm to use when performing cryptographic operations with the key.",
|
||||
type: "The type of key to be created, either encrypt-decrypt or sign-verify, based on your intended use for the key."
|
||||
},
|
||||
UPDATE_KEY: {
|
||||
keyId: "The ID of the key to be updated.",
|
||||
@@ -1705,6 +1706,28 @@ export const KMS = {
|
||||
DECRYPT: {
|
||||
keyId: "The ID of the key to decrypt the data with.",
|
||||
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
|
||||
},
|
||||
|
||||
LIST_SIGNING_ALGORITHMS: {
|
||||
keyId: "The ID of the key to list the signing algorithms for. The key must be for signing and verifying."
|
||||
},
|
||||
|
||||
GET_PUBLIC_KEY: {
|
||||
keyId: "The ID of the key to get the public key for. The key must be for signing and verifying."
|
||||
},
|
||||
|
||||
SIGN: {
|
||||
keyId: "The ID of the key to sign the data with.",
|
||||
data: "The data in string format to be signed (base64 encoded).",
|
||||
isDigest:
|
||||
"Whether the data is already digested or not. Please be aware that if you are passing a digest the algorithm used to create the digest must match the signing algorithm used to sign the digest.",
|
||||
signingAlgorithm: "The algorithm to use when performing cryptographic operations with the key."
|
||||
},
|
||||
VERIFY: {
|
||||
keyId: "The ID of the key to verify the data with.",
|
||||
data: "The data in string format to be verified (base64 encoded). For data larger than 4096 bytes you must first create a digest of the data and then pass the digest in the data parameter.",
|
||||
signature: "The signature to be verified (base64 encoded).",
|
||||
isDigest: "Whether the data is already digested or not."
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
|
||||
import { SymmetricKeyAlgorithm, TSymmetricEncryptionFns } from "./types";
|
||||
|
||||
const getIvLength = () => {
|
||||
return 12;
|
||||
@@ -10,7 +10,9 @@ const getTagLength = () => {
|
||||
return 16;
|
||||
};
|
||||
|
||||
export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
|
||||
export const symmetricCipherService = (
|
||||
type: SymmetricKeyAlgorithm.AES_GCM_128 | SymmetricKeyAlgorithm.AES_GCM_256
|
||||
): TSymmetricEncryptionFns => {
|
||||
const IV_LENGTH = getIvLength();
|
||||
const TAG_LENGTH = getTagLength();
|
||||
|
||||
|
@@ -1,2 +1,2 @@
|
||||
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_128 = "aes-128-gcm"
|
||||
}
|
||||
export const SymmetricKeyAlgorithmEnum = z.enum(Object.values(SymmetricKeyAlgorithm) as [string, ...string[]]).options;
|
||||
|
||||
export const AllowedEncryptionKeyAlgorithms = z.enum([
|
||||
...Object.values(SymmetricKeyAlgorithm),
|
||||
...Object.values(AsymmetricKeyAlgorithm)
|
||||
] as [string, ...string[]]).options;
|
||||
|
||||
export type TSymmetricEncryptionFns = {
|
||||
encrypt: (text: Buffer, key: Buffer) => Buffer;
|
||||
|
2
backend/src/lib/crypto/sign/index.ts
Normal file
2
backend/src/lib/crypto/sign/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { signingService } from "./signing";
|
||||
export { AsymmetricKeyAlgorithm, SigningAlgorithm } from "./types";
|
539
backend/src/lib/crypto/sign/signing.ts
Normal file
539
backend/src/lib/crypto/sign/signing.ts
Normal file
@@ -0,0 +1,539 @@
|
||||
import { execFile } from "child_process";
|
||||
import crypto from "crypto";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { cleanTemporaryDirectory, createTemporaryDirectory, writeToTemporaryFile } from "@app/lib/files";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm, TAsymmetricSignVerifyFns } from "./types";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
interface SigningParams {
|
||||
hashAlgorithm: SupportedHashAlgorithm;
|
||||
padding?: number;
|
||||
saltLength?: number;
|
||||
}
|
||||
|
||||
enum SupportedHashAlgorithm {
|
||||
SHA256 = "sha256",
|
||||
SHA384 = "sha384",
|
||||
SHA512 = "sha512"
|
||||
}
|
||||
|
||||
const COMMAND_TIMEOUT = 15_000;
|
||||
|
||||
const SHA256_DIGEST_LENGTH = 32;
|
||||
const SHA384_DIGEST_LENGTH = 48;
|
||||
const SHA512_DIGEST_LENGTH = 64;
|
||||
|
||||
/**
|
||||
* Service for cryptographic signing and verification operations using asymmetric keys
|
||||
*
|
||||
* @param algorithm The key algorithm itself. The signing algorithm is supplied in the individual sign/verify functions.
|
||||
* @returns Object with sign and verify functions
|
||||
*/
|
||||
export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSignVerifyFns => {
|
||||
const $getSigningParams = (signingAlgorithm: SigningAlgorithm): SigningParams => {
|
||||
switch (signingAlgorithm) {
|
||||
// RSA PSS
|
||||
case SigningAlgorithm.RSASSA_PSS_SHA_512:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA512,
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: SHA512_DIGEST_LENGTH
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PSS_SHA_256:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA256,
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: SHA256_DIGEST_LENGTH
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PSS_SHA_384:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA384,
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: SHA384_DIGEST_LENGTH
|
||||
};
|
||||
|
||||
// RSA PKCS#1 v1.5
|
||||
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_512:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA512,
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_384:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA384,
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
};
|
||||
case SigningAlgorithm.RSASSA_PKCS1_V1_5_SHA_256:
|
||||
return {
|
||||
hashAlgorithm: SupportedHashAlgorithm.SHA256,
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
};
|
||||
|
||||
// ECDSA
|
||||
case SigningAlgorithm.ECDSA_SHA_256:
|
||||
return { hashAlgorithm: SupportedHashAlgorithm.SHA256 };
|
||||
case SigningAlgorithm.ECDSA_SHA_384:
|
||||
return { hashAlgorithm: SupportedHashAlgorithm.SHA384 };
|
||||
case SigningAlgorithm.ECDSA_SHA_512:
|
||||
return { hashAlgorithm: SupportedHashAlgorithm.SHA512 };
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported signing algorithm: ${signingAlgorithm as string}`);
|
||||
}
|
||||
};
|
||||
|
||||
const $getEcCurveName = (keyAlgorithm: AsymmetricKeyAlgorithm): { full: string; short: string } => {
|
||||
// We will support more in the future
|
||||
switch (keyAlgorithm) {
|
||||
case AsymmetricKeyAlgorithm.ECC_NIST_P256:
|
||||
return {
|
||||
full: "prime256v1",
|
||||
short: "p256"
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unsupported EC curve: ${keyAlgorithm}`);
|
||||
}
|
||||
};
|
||||
|
||||
const $validateAlgorithmWithKeyType = (signingAlgorithm: SigningAlgorithm) => {
|
||||
const isRsaKey = algorithm.startsWith("RSA");
|
||||
const isEccKey = algorithm.startsWith("ECC");
|
||||
|
||||
const isRsaAlgorithm = signingAlgorithm.startsWith("RSASSA");
|
||||
const isEccAlgorithm = signingAlgorithm.startsWith("ECDSA");
|
||||
|
||||
if (isRsaKey && !isRsaAlgorithm) {
|
||||
throw new BadRequestError({ message: `KMS RSA key cannot be used with ${signingAlgorithm}` });
|
||||
}
|
||||
|
||||
if (isEccKey && !isEccAlgorithm) {
|
||||
throw new BadRequestError({ message: `KMS ECC key cannot be used with ${signingAlgorithm}` });
|
||||
}
|
||||
};
|
||||
|
||||
const $signRsaDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
|
||||
const tempDir = await createTemporaryDirectory("kms-rsa-sign");
|
||||
const digestPath = path.join(tempDir, "digest.bin");
|
||||
const sigPath = path.join(tempDir, "signature.bin");
|
||||
const keyPath = path.join(tempDir, "key.pem");
|
||||
|
||||
try {
|
||||
await writeToTemporaryFile(digestPath, digest);
|
||||
await writeToTemporaryFile(keyPath, privateKey);
|
||||
|
||||
const { stderr } = await execFileAsync(
|
||||
"openssl",
|
||||
[
|
||||
"pkeyutl",
|
||||
"-sign",
|
||||
"-in",
|
||||
digestPath,
|
||||
"-inkey",
|
||||
keyPath,
|
||||
"-pkeyopt",
|
||||
`digest:${hashAlgorithm}`,
|
||||
"-out",
|
||||
sigPath
|
||||
],
|
||||
{
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
timeout: COMMAND_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
if (stderr) {
|
||||
logger.error(stderr, "KMS: Failed to sign RSA digest");
|
||||
throw new BadRequestError({
|
||||
message: "Failed to sign RSA digest due to signing error"
|
||||
});
|
||||
}
|
||||
const signature = await fs.readFile(sigPath);
|
||||
|
||||
if (!signature) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest."
|
||||
});
|
||||
}
|
||||
|
||||
return signature;
|
||||
} finally {
|
||||
await cleanTemporaryDirectory(tempDir);
|
||||
}
|
||||
};
|
||||
|
||||
const $signEccDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
|
||||
const tempDir = await createTemporaryDirectory("ecc-sign");
|
||||
const digestPath = path.join(tempDir, "digest.bin");
|
||||
const keyPath = path.join(tempDir, "key.pem");
|
||||
const sigPath = path.join(tempDir, "signature.bin");
|
||||
|
||||
try {
|
||||
await writeToTemporaryFile(digestPath, digest);
|
||||
await writeToTemporaryFile(keyPath, privateKey);
|
||||
|
||||
const { stderr } = await execFileAsync(
|
||||
"openssl",
|
||||
[
|
||||
"pkeyutl",
|
||||
"-sign",
|
||||
"-in",
|
||||
digestPath,
|
||||
"-inkey",
|
||||
keyPath,
|
||||
"-pkeyopt",
|
||||
`digest:${hashAlgorithm}`,
|
||||
"-out",
|
||||
sigPath
|
||||
],
|
||||
{
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
timeout: COMMAND_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
if (stderr) {
|
||||
logger.error(stderr, "KMS: Failed to sign ECC digest");
|
||||
throw new BadRequestError({
|
||||
message: "Failed to sign ECC digest due to signing error"
|
||||
});
|
||||
}
|
||||
|
||||
const signature = await fs.readFile(sigPath);
|
||||
|
||||
if (!signature) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"No signature was created. Make sure you are using an appropriate signing algorithm that uses the same hashing algorithm as the one used to create the digest."
|
||||
});
|
||||
}
|
||||
|
||||
return signature;
|
||||
} finally {
|
||||
await cleanTemporaryDirectory(tempDir);
|
||||
}
|
||||
};
|
||||
|
||||
const $verifyEccDigest = async (
|
||||
digest: Buffer,
|
||||
signature: Buffer,
|
||||
publicKey: Buffer,
|
||||
hashAlgorithm: SupportedHashAlgorithm
|
||||
) => {
|
||||
const tempDir = await createTemporaryDirectory("ecc-signature-verification");
|
||||
const publicKeyFile = path.join(tempDir, "public-key.pem");
|
||||
const sigFile = path.join(tempDir, "signature.sig");
|
||||
const digestFile = path.join(tempDir, "digest.bin");
|
||||
|
||||
try {
|
||||
await writeToTemporaryFile(publicKeyFile, publicKey);
|
||||
await writeToTemporaryFile(sigFile, signature);
|
||||
await writeToTemporaryFile(digestFile, digest);
|
||||
|
||||
await execFileAsync(
|
||||
"openssl",
|
||||
[
|
||||
"pkeyutl",
|
||||
"-verify",
|
||||
"-in",
|
||||
digestFile,
|
||||
"-inkey",
|
||||
publicKeyFile,
|
||||
"-pubin", // Important for EC public keys
|
||||
"-sigfile",
|
||||
sigFile,
|
||||
"-pkeyopt",
|
||||
`digest:${hashAlgorithm}`
|
||||
],
|
||||
{ timeout: COMMAND_TIMEOUT }
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as { stderr: string };
|
||||
|
||||
if (
|
||||
!err?.stderr?.toLowerCase()?.includes("signature verification failure") &&
|
||||
!err?.stderr?.toLowerCase()?.includes("bad signature")
|
||||
) {
|
||||
logger.error(error, "KMS: Failed to verify ECC signature");
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
await cleanTemporaryDirectory(tempDir);
|
||||
}
|
||||
};
|
||||
|
||||
const $verifyRsaDigest = async (
|
||||
digest: Buffer,
|
||||
signature: Buffer,
|
||||
publicKey: Buffer,
|
||||
hashAlgorithm: SupportedHashAlgorithm
|
||||
) => {
|
||||
const tempDir = await createTemporaryDirectory("kms-signature-verification");
|
||||
const publicKeyFile = path.join(tempDir, "public-key.pub");
|
||||
const signatureFile = path.join(tempDir, "signature.sig");
|
||||
const digestFile = path.join(tempDir, "digest.bin");
|
||||
|
||||
try {
|
||||
await writeToTemporaryFile(publicKeyFile, publicKey);
|
||||
await writeToTemporaryFile(signatureFile, signature);
|
||||
await writeToTemporaryFile(digestFile, digest);
|
||||
|
||||
await execFileAsync(
|
||||
"openssl",
|
||||
[
|
||||
"pkeyutl",
|
||||
"-verify",
|
||||
"-in",
|
||||
digestFile,
|
||||
"-inkey",
|
||||
publicKeyFile,
|
||||
"-pubin",
|
||||
"-sigfile",
|
||||
signatureFile,
|
||||
"-pkeyopt",
|
||||
`digest:${hashAlgorithm}`
|
||||
],
|
||||
{ timeout: COMMAND_TIMEOUT }
|
||||
);
|
||||
|
||||
// it'll throw if the verification was not successful
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as { stdout: string };
|
||||
|
||||
if (!err?.stdout?.toLowerCase()?.includes("signature verification failure")) {
|
||||
logger.error(error, "KMS: Failed to verify signature");
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
await cleanTemporaryDirectory(tempDir);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyDigestFunctionsMap: Record<
|
||||
AsymmetricKeyAlgorithm,
|
||||
(data: Buffer, signature: Buffer, publicKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<boolean>
|
||||
> = {
|
||||
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $verifyEccDigest,
|
||||
[AsymmetricKeyAlgorithm.RSA_4096]: $verifyRsaDigest
|
||||
};
|
||||
|
||||
const signDigestFunctionsMap: Record<
|
||||
AsymmetricKeyAlgorithm,
|
||||
(data: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<Buffer>
|
||||
> = {
|
||||
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest,
|
||||
[AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest
|
||||
};
|
||||
|
||||
const sign = async (
|
||||
data: Buffer,
|
||||
privateKey: Buffer,
|
||||
signingAlgorithm: SigningAlgorithm,
|
||||
isDigest: boolean
|
||||
): Promise<Buffer> => {
|
||||
$validateAlgorithmWithKeyType(signingAlgorithm);
|
||||
|
||||
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
|
||||
|
||||
if (isDigest) {
|
||||
if (signingAlgorithm.startsWith("RSASSA_PSS")) {
|
||||
throw new BadRequestError({
|
||||
message: "RSA PSS does not support digested input"
|
||||
});
|
||||
}
|
||||
|
||||
const signFunction = signDigestFunctionsMap[algorithm];
|
||||
|
||||
if (!signFunction) {
|
||||
throw new BadRequestError({
|
||||
message: `Digested input is not supported for key algorithm ${algorithm}`
|
||||
});
|
||||
}
|
||||
|
||||
const signature = await signFunction(data, privateKey, hashAlgorithm);
|
||||
return signature;
|
||||
}
|
||||
|
||||
const privateKeyObject = crypto.createPrivateKey({
|
||||
key: privateKey,
|
||||
format: "pem",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
// For RSA signatures
|
||||
if (signingAlgorithm.startsWith("RSA")) {
|
||||
const signer = crypto.createSign(hashAlgorithm);
|
||||
signer.update(data);
|
||||
|
||||
return signer.sign({
|
||||
key: privateKeyObject,
|
||||
padding,
|
||||
...(signingAlgorithm.includes("PSS") ? { saltLength } : {})
|
||||
});
|
||||
}
|
||||
if (signingAlgorithm.startsWith("ECDSA")) {
|
||||
// For ECDSA signatures
|
||||
const signer = crypto.createSign(hashAlgorithm);
|
||||
signer.update(data);
|
||||
return signer.sign({
|
||||
key: privateKeyObject,
|
||||
dsaEncoding: "der"
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Signing algorithm ${signingAlgorithm} not implemented`
|
||||
});
|
||||
};
|
||||
|
||||
const verify = async (
|
||||
data: Buffer,
|
||||
signature: Buffer,
|
||||
publicKey: Buffer,
|
||||
signingAlgorithm: SigningAlgorithm,
|
||||
isDigest: boolean
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
$validateAlgorithmWithKeyType(signingAlgorithm);
|
||||
|
||||
const { hashAlgorithm, padding, saltLength } = $getSigningParams(signingAlgorithm);
|
||||
|
||||
if (isDigest) {
|
||||
if (signingAlgorithm.startsWith("RSASSA_PSS")) {
|
||||
throw new BadRequestError({
|
||||
message: "RSA PSS does not support digested input"
|
||||
});
|
||||
}
|
||||
|
||||
const verifyFunction = verifyDigestFunctionsMap[algorithm];
|
||||
|
||||
if (!verifyFunction) {
|
||||
throw new BadRequestError({
|
||||
message: `Digested input is not supported for key algorithm ${algorithm}`
|
||||
});
|
||||
}
|
||||
|
||||
const signatureValid = await verifyFunction(data, signature, publicKey, hashAlgorithm);
|
||||
|
||||
return signatureValid;
|
||||
}
|
||||
|
||||
const publicKeyObject = crypto.createPublicKey({
|
||||
key: publicKey,
|
||||
format: "der",
|
||||
type: "spki"
|
||||
});
|
||||
|
||||
// For RSA signatures
|
||||
if (signingAlgorithm.startsWith("RSA")) {
|
||||
const verifier = crypto.createVerify(hashAlgorithm);
|
||||
verifier.update(data);
|
||||
|
||||
return verifier.verify(
|
||||
{
|
||||
key: publicKeyObject,
|
||||
padding,
|
||||
...(signingAlgorithm.includes("PSS") ? { saltLength } : {})
|
||||
},
|
||||
signature
|
||||
);
|
||||
}
|
||||
// For ECDSA signatures
|
||||
if (signingAlgorithm.startsWith("ECDSA")) {
|
||||
const verifier = crypto.createVerify(hashAlgorithm);
|
||||
verifier.update(data);
|
||||
return verifier.verify(
|
||||
{
|
||||
key: publicKeyObject,
|
||||
dsaEncoding: "der"
|
||||
},
|
||||
signature
|
||||
);
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Verification for algorithm ${signingAlgorithm} not implemented`
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error(error, "KMS: Failed to verify signature");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const generateAsymmetricPrivateKey = async () => {
|
||||
const { privateKey } = await new Promise<{ privateKey: string }>((resolve, reject) => {
|
||||
if (algorithm.startsWith("RSA")) {
|
||||
crypto.generateKeyPair(
|
||||
"rsa",
|
||||
{
|
||||
modulusLength: Number(algorithm.split("_")[1]),
|
||||
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||
},
|
||||
(err, _, pk) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ privateKey: pk });
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const { full: namedCurve } = $getEcCurveName(algorithm);
|
||||
|
||||
crypto.generateKeyPair(
|
||||
"ec",
|
||||
{
|
||||
namedCurve,
|
||||
publicKeyEncoding: { type: "spki", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
||||
},
|
||||
(err, _, pk) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({
|
||||
privateKey: pk
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Buffer.from(privateKey);
|
||||
};
|
||||
|
||||
const getPublicKeyFromPrivateKey = (privateKey: Buffer) => {
|
||||
const privateKeyObj = crypto.createPrivateKey({
|
||||
key: privateKey,
|
||||
format: "pem",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
const publicKey = crypto.createPublicKey(privateKeyObj).export({
|
||||
type: "spki",
|
||||
format: "der"
|
||||
});
|
||||
|
||||
return publicKey;
|
||||
};
|
||||
|
||||
return {
|
||||
sign,
|
||||
verify,
|
||||
generateAsymmetricPrivateKey,
|
||||
getPublicKeyFromPrivateKey
|
||||
};
|
||||
};
|
45
backend/src/lib/crypto/sign/types.ts
Normal file
45
backend/src/lib/crypto/sign/types.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type TAsymmetricSignVerifyFns = {
|
||||
sign: (data: Buffer, key: Buffer, signingAlgorithm: SigningAlgorithm, isDigest: boolean) => Promise<Buffer>;
|
||||
verify: (
|
||||
data: Buffer,
|
||||
signature: Buffer,
|
||||
key: Buffer,
|
||||
signingAlgorithm: SigningAlgorithm,
|
||||
isDigest: boolean
|
||||
) => Promise<boolean>;
|
||||
generateAsymmetricPrivateKey: () => Promise<Buffer>;
|
||||
getPublicKeyFromPrivateKey: (privateKey: Buffer) => Buffer;
|
||||
};
|
||||
|
||||
// Supported asymmetric key types
|
||||
export enum AsymmetricKeyAlgorithm {
|
||||
RSA_4096 = "RSA_4096",
|
||||
ECC_NIST_P256 = "ECC_NIST_P256"
|
||||
}
|
||||
|
||||
export const AsymmetricKeyAlgorithmEnum = z.enum(
|
||||
Object.values(AsymmetricKeyAlgorithm) as [string, ...string[]]
|
||||
).options;
|
||||
|
||||
export enum SigningAlgorithm {
|
||||
// RSA PSS algorithms
|
||||
// These are NOT deterministic and include randomness.
|
||||
// This means that the output signature is different each time for the same input.
|
||||
RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512",
|
||||
RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384",
|
||||
RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256",
|
||||
|
||||
// RSA PKCS#1 v1.5 algorithms
|
||||
// These are deterministic and the output is the same each time for the same input.
|
||||
RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512",
|
||||
RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384",
|
||||
RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256",
|
||||
|
||||
// ECDSA algorithms
|
||||
// None of these are deterministic and include randomness like RSA PSS.
|
||||
ECDSA_SHA_512 = "ECDSA_SHA_512",
|
||||
ECDSA_SHA_384 = "ECDSA_SHA_384",
|
||||
ECDSA_SHA_256 = "ECDSA_SHA_256"
|
||||
}
|
35
backend/src/lib/files/files.ts
Normal file
35
backend/src/lib/files/files.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import crypto from "crypto";
|
||||
import fs from "fs/promises";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
const baseDir = path.join(os.tmpdir(), "infisical");
|
||||
const randomPath = () => `${crypto.randomBytes(32).toString("hex")}`;
|
||||
|
||||
export const createTemporaryDirectory = async (name: string) => {
|
||||
const tempDirPath = path.join(baseDir, `${name}-${randomPath()}`);
|
||||
await fs.mkdir(tempDirPath, { recursive: true });
|
||||
|
||||
return tempDirPath;
|
||||
};
|
||||
|
||||
export const removeTemporaryBaseDirectory = async () => {
|
||||
await fs.rm(baseDir, { force: true, recursive: true }).catch((err) => {
|
||||
logger.error(err, `Failed to remove temporary base directory [path=${baseDir}]`);
|
||||
});
|
||||
};
|
||||
|
||||
export const cleanTemporaryDirectory = async (dirPath: string) => {
|
||||
await fs.rm(dirPath, { recursive: true, force: true }).catch((err) => {
|
||||
logger.error(err, `Failed to cleanup temporary directory [path=${dirPath}]`);
|
||||
});
|
||||
};
|
||||
|
||||
export const writeToTemporaryFile = async (tempDirPath: string, data: string | Buffer) => {
|
||||
await fs.writeFile(tempDirPath, data, { mode: 0o600 }).catch((err) => {
|
||||
logger.error(err, `Failed to write to temporary file [path=${tempDirPath}]`);
|
||||
throw err;
|
||||
});
|
||||
};
|
1
backend/src/lib/files/index.ts
Normal file
1
backend/src/lib/files/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./files";
|
@@ -9,6 +9,7 @@ import { runMigrations } from "./auto-start-migrations";
|
||||
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
||||
import { keyStoreFactory } from "./keystore/keystore";
|
||||
import { formatSmtpConfig, initEnvConfig } from "./lib/config/env";
|
||||
import { removeTemporaryBaseDirectory } from "./lib/files";
|
||||
import { initLogger } from "./lib/logger";
|
||||
import { queueServiceFactory } from "./queue";
|
||||
import { main } from "./server/app";
|
||||
@@ -21,6 +22,8 @@ const run = async () => {
|
||||
const logger = initLogger();
|
||||
const envConfig = initEnvConfig(logger);
|
||||
|
||||
await removeTemporaryBaseDirectory();
|
||||
|
||||
const db = initDbConnection({
|
||||
dbConnectionUri: envConfig.DB_CONNECTION_URI,
|
||||
dbRootCert: envConfig.DB_ROOT_CERT,
|
||||
@@ -71,6 +74,7 @@ const run = async () => {
|
||||
process.on("SIGINT", async () => {
|
||||
await server.close();
|
||||
await db.destroy();
|
||||
await removeTemporaryBaseDirectory();
|
||||
hsmModule.finalize();
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -79,6 +83,7 @@ const run = async () => {
|
||||
process.on("SIGTERM", async () => {
|
||||
await server.close();
|
||||
await db.destroy();
|
||||
await removeTemporaryBaseDirectory();
|
||||
hsmModule.finalize();
|
||||
process.exit(0);
|
||||
});
|
||||
|
@@ -4,13 +4,15 @@ import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { KMS } from "@app/lib/api-docs";
|
||||
import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { AllowedEncryptionKeyAlgorithms, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
|
||||
import { CmekOrderBy, TCmekKeyEncryptionAlgorithm } from "@app/services/cmek/cmek-types";
|
||||
import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
||||
|
||||
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
|
||||
const keyDescriptionSchema = z.string().trim().max(500).optional();
|
||||
@@ -45,16 +47,46 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
description: "Create KMS key",
|
||||
body: z.object({
|
||||
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
||||
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
||||
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
||||
encryptionAlgorithm: z
|
||||
.nativeEnum(SymmetricEncryption)
|
||||
.optional()
|
||||
.default(SymmetricEncryption.AES_GCM_256)
|
||||
.describe(KMS.CREATE_KEY.encryptionAlgorithm) // eventually will support others
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
projectId: z.string().describe(KMS.CREATE_KEY.projectId),
|
||||
name: keyNameSchema.describe(KMS.CREATE_KEY.name),
|
||||
description: keyDescriptionSchema.describe(KMS.CREATE_KEY.description),
|
||||
keyUsage: z
|
||||
.nativeEnum(KmsKeyUsage)
|
||||
.optional()
|
||||
.default(KmsKeyUsage.ENCRYPT_DECRYPT)
|
||||
.describe(KMS.CREATE_KEY.type),
|
||||
encryptionAlgorithm: z
|
||||
.enum(AllowedEncryptionKeyAlgorithms)
|
||||
.optional()
|
||||
.default(SymmetricKeyAlgorithm.AES_GCM_256)
|
||||
.describe(KMS.CREATE_KEY.encryptionAlgorithm)
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (
|
||||
data.keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT &&
|
||||
!Object.values(SymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as SymmetricKeyAlgorithm)
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `encryptionAlgorithm must be a valid symmetric encryption algorithm. Valid options are: ${Object.values(
|
||||
SymmetricKeyAlgorithm
|
||||
).join(", ")}`
|
||||
});
|
||||
}
|
||||
if (
|
||||
data.keyUsage === KmsKeyUsage.SIGN_VERIFY &&
|
||||
!Object.values(AsymmetricKeyAlgorithm).includes(data.encryptionAlgorithm as AsymmetricKeyAlgorithm)
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `encryptionAlgorithm must be a valid asymmetric sign-verify algorithm. Valid options are: ${Object.values(
|
||||
AsymmetricKeyAlgorithm
|
||||
).join(", ")}`
|
||||
});
|
||||
}
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
key: CmekSchema
|
||||
@@ -64,12 +96,19 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
body: { projectId, name, description, encryptionAlgorithm },
|
||||
body: { projectId, name, description, encryptionAlgorithm, keyUsage },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const cmek = await server.services.cmek.createCmek(
|
||||
{ orgId: permission.orgId, projectId, name, description, encryptionAlgorithm },
|
||||
{
|
||||
orgId: permission.orgId,
|
||||
projectId,
|
||||
name,
|
||||
description,
|
||||
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm,
|
||||
keyUsage
|
||||
},
|
||||
permission
|
||||
);
|
||||
|
||||
@@ -82,7 +121,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
keyId: cmek.id,
|
||||
name,
|
||||
description,
|
||||
encryptionAlgorithm
|
||||
encryptionAlgorithm: encryptionAlgorithm as TCmekKeyEncryptionAlgorithm
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -126,7 +165,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: permission.orgId,
|
||||
projectId: cmek.projectId!,
|
||||
event: {
|
||||
type: EventType.UPDATE_CMEK,
|
||||
metadata: {
|
||||
@@ -169,7 +208,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: permission.orgId,
|
||||
projectId: cmek.projectId!,
|
||||
event: {
|
||||
type: EventType.DELETE_CMEK,
|
||||
metadata: {
|
||||
@@ -282,7 +321,7 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get KMS key by Name",
|
||||
description: "Get KMS key by name",
|
||||
params: z.object({
|
||||
keyName: slugSchema({ field: "Key name" }).describe(KMS.GET_KEY_BY_NAME.keyName)
|
||||
}),
|
||||
@@ -349,11 +388,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const ciphertext = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
|
||||
const { ciphertext, projectId } = await server.services.cmek.cmekEncrypt({ keyId, plaintext }, permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: permission.orgId,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_ENCRYPT,
|
||||
metadata: {
|
||||
@@ -366,6 +405,198 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/keys/:keyId/public-key",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description:
|
||||
"Get the public key for a KMS key that is used for signing and verifying data. This endpoint is only available for asymmetric keys.",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.GET_PUBLIC_KEY.keyId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
publicKey: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { keyId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const { publicKey, projectId } = await server.services.cmek.getPublicKey({ keyId }, permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_GET_PUBLIC_KEY,
|
||||
metadata: {
|
||||
keyId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { publicKey };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/keys/:keyId/signing-algorithms",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List all available signing algorithms for a KMS key",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.LIST_SIGNING_ALGORITHMS.keyId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
signingAlgorithms: z.array(z.nativeEnum(SigningAlgorithm))
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { keyId } = req.params;
|
||||
|
||||
const { signingAlgorithms, projectId } = await server.services.cmek.listSigningAlgorithms(
|
||||
{ keyId },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_LIST_SIGNING_ALGORITHMS,
|
||||
metadata: {
|
||||
keyId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { signingAlgorithms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/keys/:keyId/sign",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Sign data with a KMS key.",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.SIGN.keyId)
|
||||
}),
|
||||
body: z.object({
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
|
||||
isDigest: z.boolean().optional().default(false).describe(KMS.SIGN.isDigest),
|
||||
data: base64Schema.describe(KMS.SIGN.data)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
signature: z.string(),
|
||||
keyId: z.string().uuid(),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { keyId: inputKeyId },
|
||||
body: { data, signingAlgorithm, isDigest },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const { projectId, ...result } = await server.services.cmek.cmekSign(
|
||||
{ keyId: inputKeyId, data, signingAlgorithm, isDigest },
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_SIGN,
|
||||
metadata: {
|
||||
keyId: inputKeyId,
|
||||
signingAlgorithm,
|
||||
signature: result.signature
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/keys/:keyId/verify",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Verify data signatures with a KMS key.",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.VERIFY.keyId)
|
||||
}),
|
||||
body: z.object({
|
||||
isDigest: z.boolean().optional().default(false).describe(KMS.VERIFY.isDigest),
|
||||
data: base64Schema.describe(KMS.VERIFY.data),
|
||||
signature: base64Schema.describe(KMS.VERIFY.signature),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
signatureValid: z.boolean(),
|
||||
keyId: z.string().uuid(),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { keyId },
|
||||
body: { data, signature, signingAlgorithm, isDigest },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const { projectId, ...result } = await server.services.cmek.cmekVerify(
|
||||
{ keyId, data, signature, signingAlgorithm, isDigest },
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_VERIFY,
|
||||
metadata: {
|
||||
keyId,
|
||||
signatureValid: result.signatureValid,
|
||||
signingAlgorithm,
|
||||
signature
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/keys/:keyId/decrypt",
|
||||
@@ -394,11 +625,11 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const plaintext = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
|
||||
const { plaintext, projectId } = await server.services.cmek.cmekDecrypt({ keyId, ciphertext }, permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: permission.orgId,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CMEK_DECRYPT,
|
||||
metadata: {
|
||||
|
@@ -108,7 +108,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile.displayName,
|
||||
firstName: profile.displayName || profile.username || "",
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITHUB,
|
||||
callbackPort
|
||||
@@ -145,7 +145,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
const email = profile.emails[0].value;
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile.displayName,
|
||||
firstName: profile.displayName || profile.username || "",
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITLAB,
|
||||
callbackPort
|
||||
|
@@ -3,12 +3,18 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import {
|
||||
TCmekDecryptDTO,
|
||||
TCmekEncryptDTO,
|
||||
TCmekGetPublicKeyDTO,
|
||||
TCmekKeyEncryptionAlgorithm,
|
||||
TCmekListSigningAlgorithmsDTO,
|
||||
TCmekSignDTO,
|
||||
TCmekVerifyDTO,
|
||||
TCreateCmekDTO,
|
||||
TListCmeksByProjectIdDTO,
|
||||
TUpdabteCmekByIdDTO
|
||||
@@ -16,6 +22,7 @@ import {
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { KmsKeyUsage } from "../kms/kms-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
|
||||
type TCmekServiceFactoryDep = {
|
||||
@@ -221,7 +228,151 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
|
||||
const { cipherTextBlob } = await encrypt({ plainText: Buffer.from(plaintext, "base64") });
|
||||
|
||||
return cipherTextBlob.toString("base64");
|
||||
return {
|
||||
ciphertext: cipherTextBlob.toString("base64"),
|
||||
projectId: key.projectId
|
||||
};
|
||||
};
|
||||
|
||||
const listSigningAlgorithms = async ({ keyId }: TCmekListSigningAlgorithmsDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: key.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
|
||||
if (key.keyUsage !== KmsKeyUsage.SIGN_VERIFY) {
|
||||
throw new BadRequestError({ message: `Key with ID '${keyId}' is not intended for signing` });
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = key.encryptionAlgorithm as TCmekKeyEncryptionAlgorithm;
|
||||
|
||||
const algos = [
|
||||
{
|
||||
keyAlgorithm: "rsa",
|
||||
signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) =>
|
||||
algorithm.toLowerCase().startsWith("rsa")
|
||||
)
|
||||
},
|
||||
{
|
||||
keyAlgorithm: "ecc",
|
||||
signingAlgorithms: Object.values(SigningAlgorithm).filter((algorithm) =>
|
||||
algorithm.toLowerCase().startsWith("ecdsa")
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const selectedAlgorithm = algos.find((algo) => encryptionAlgorithm.toLowerCase().startsWith(algo.keyAlgorithm));
|
||||
|
||||
if (!selectedAlgorithm) {
|
||||
throw new BadRequestError({ message: `Unsupported encryption algorithm: ${encryptionAlgorithm}` });
|
||||
}
|
||||
|
||||
return { signingAlgorithms: selectedAlgorithm.signingAlgorithms, projectId: key.projectId };
|
||||
};
|
||||
|
||||
const getPublicKey = async ({ keyId }: TCmekGetPublicKeyDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: key.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
|
||||
const publicKey = await kmsService.getPublicKey({ kmsId: keyId });
|
||||
return { publicKey: publicKey.toString("base64"), projectId: key.projectId };
|
||||
};
|
||||
|
||||
const cmekSign = async ({ keyId, data, signingAlgorithm, isDigest }: TCmekSignDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: key.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Sign, ProjectPermissionSub.Cmek);
|
||||
|
||||
const sign = await kmsService.signWithKmsKey({ kmsId: keyId });
|
||||
|
||||
const { signature, algorithm } = await sign({ data: Buffer.from(data, "base64"), signingAlgorithm, isDigest });
|
||||
|
||||
return {
|
||||
signature: signature.toString("base64"),
|
||||
keyId: key.id,
|
||||
projectId: key.projectId,
|
||||
signingAlgorithm: algorithm
|
||||
};
|
||||
};
|
||||
|
||||
const cmekVerify = async (
|
||||
{ keyId, data, signature, signingAlgorithm, isDigest }: TCmekVerifyDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: key.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Verify, ProjectPermissionSub.Cmek);
|
||||
|
||||
const verify = await kmsService.verifyWithKmsKey({ kmsId: keyId, signingAlgorithm });
|
||||
|
||||
const { signatureValid, algorithm } = await verify({
|
||||
isDigest,
|
||||
data: Buffer.from(data, "base64"),
|
||||
signature: Buffer.from(signature, "base64")
|
||||
});
|
||||
|
||||
return {
|
||||
signatureValid,
|
||||
keyId: key.id,
|
||||
projectId: key.projectId,
|
||||
signingAlgorithm: algorithm
|
||||
};
|
||||
};
|
||||
|
||||
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
|
||||
@@ -248,7 +399,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
|
||||
const plaintextBlob = await decrypt({ cipherTextBlob: Buffer.from(ciphertext, "base64") });
|
||||
|
||||
return plaintextBlob.toString("base64");
|
||||
return {
|
||||
plaintext: plaintextBlob.toString("base64"),
|
||||
projectId: key.projectId
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -259,6 +413,10 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
cmekEncrypt,
|
||||
cmekDecrypt,
|
||||
findCmekById,
|
||||
findCmekByName
|
||||
findCmekByName,
|
||||
cmekSign,
|
||||
cmekVerify,
|
||||
listSigningAlgorithms,
|
||||
getPublicKey
|
||||
};
|
||||
};
|
||||
|
@@ -1,12 +1,18 @@
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
|
||||
import { KmsKeyUsage } from "../kms/kms-types";
|
||||
|
||||
export type TCmekKeyEncryptionAlgorithm = SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||
|
||||
export type TCreateCmekDTO = {
|
||||
orgId: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
encryptionAlgorithm: SymmetricEncryption;
|
||||
encryptionAlgorithm: TCmekKeyEncryptionAlgorithm;
|
||||
keyUsage: KmsKeyUsage;
|
||||
};
|
||||
|
||||
export type TUpdabteCmekByIdDTO = {
|
||||
@@ -38,3 +44,26 @@ export type TCmekDecryptDTO = {
|
||||
export enum CmekOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
export type TCmekListSigningAlgorithmsDTO = {
|
||||
keyId: string;
|
||||
};
|
||||
|
||||
export type TCmekGetPublicKeyDTO = {
|
||||
keyId: string;
|
||||
};
|
||||
|
||||
export type TCmekSignDTO = {
|
||||
keyId: string;
|
||||
data: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
isDigest: boolean;
|
||||
};
|
||||
|
||||
export type TCmekVerifyDTO = {
|
||||
keyId: string;
|
||||
data: string;
|
||||
signature: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
isDigest: boolean;
|
||||
};
|
||||
|
@@ -1,13 +1,55 @@
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeyAlgorithm } from "@app/lib/crypto/sign";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { KmsKeyUsage } from "./kms-types";
|
||||
|
||||
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
|
||||
export const getByteLengthForSymmetricEncryptionAlgorithm = (encryptionAlgorithm: SymmetricKeyAlgorithm) => {
|
||||
switch (encryptionAlgorithm) {
|
||||
case SymmetricEncryption.AES_GCM_128:
|
||||
case SymmetricKeyAlgorithm.AES_GCM_128:
|
||||
return 16;
|
||||
case SymmetricEncryption.AES_GCM_256:
|
||||
case SymmetricKeyAlgorithm.AES_GCM_256:
|
||||
default:
|
||||
return 32;
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyKeyTypeAndAlgorithm = (
|
||||
keyUsage: KmsKeyUsage,
|
||||
algorithm: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm,
|
||||
extra?: {
|
||||
forceType?: KmsKeyUsage;
|
||||
}
|
||||
) => {
|
||||
if (extra?.forceType && keyUsage !== extra.forceType) {
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported key type, expected ${extra.forceType} but got ${keyUsage}`
|
||||
});
|
||||
}
|
||||
|
||||
if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
|
||||
if (!Object.values(SymmetricKeyAlgorithm).includes(algorithm as SymmetricKeyAlgorithm)) {
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported encryption algorithm for encrypt/decrypt key: ${algorithm as string}`
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyUsage === KmsKeyUsage.SIGN_VERIFY) {
|
||||
if (!Object.values(AsymmetricKeyAlgorithm).includes(algorithm as AsymmetricKeyAlgorithm)) {
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported sign/verify algorithm for sign/verify key: ${algorithm as string}`
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported key type: ${keyUsage as string}`
|
||||
});
|
||||
};
|
||||
|
@@ -15,12 +15,17 @@ import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { TEnvConfig } from "@app/lib/config/env";
|
||||
import { randomSecureBytes } from "@app/lib/crypto";
|
||||
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { symmetricCipherService, SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { AsymmetricKeyAlgorithm, signingService } from "@app/lib/crypto/sign";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { getByteLengthForAlgorithm, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns";
|
||||
import {
|
||||
getByteLengthForSymmetricEncryptionAlgorithm,
|
||||
KMS_ROOT_CONFIG_UUID,
|
||||
verifyKeyTypeAndAlgorithm
|
||||
} from "@app/services/kms/kms-fns";
|
||||
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
@@ -29,6 +34,7 @@ import { TKmsKeyDALFactory } from "./kms-key-dal";
|
||||
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
|
||||
import {
|
||||
KmsDataKey,
|
||||
KmsKeyUsage,
|
||||
KmsType,
|
||||
RootKeyEncryptionStrategy,
|
||||
TDecryptWithKeyDTO,
|
||||
@@ -38,8 +44,11 @@ import {
|
||||
TEncryptWithKmsDTO,
|
||||
TGenerateKMSDTO,
|
||||
TGetKeyMaterialDTO,
|
||||
TGetPublicKeyDTO,
|
||||
TImportKeyMaterialDTO,
|
||||
TUpdateProjectSecretManagerKmsKeyDTO
|
||||
TSignWithKmsDTO,
|
||||
TUpdateProjectSecretManagerKmsKeyDTO,
|
||||
TVerifyWithKmsDTO
|
||||
} from "./kms-types";
|
||||
|
||||
type TKmsServiceFactoryDep = {
|
||||
@@ -83,19 +92,42 @@ export const kmsServiceFactory = ({
|
||||
tx,
|
||||
name,
|
||||
projectId,
|
||||
encryptionAlgorithm = SymmetricEncryption.AES_GCM_256,
|
||||
encryptionAlgorithm = SymmetricKeyAlgorithm.AES_GCM_256,
|
||||
keyUsage = KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||
description
|
||||
}: TGenerateKMSDTO) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
// daniel: ensure that the key type (sign/encrypt) and the encryption algorithm are compatible.
|
||||
verifyKeyTypeAndAlgorithm(keyUsage, encryptionAlgorithm);
|
||||
|
||||
const kmsKeyMaterial = randomSecureBytes(getByteLengthForAlgorithm(encryptionAlgorithm));
|
||||
let kmsKeyMaterial: Buffer | null = null;
|
||||
if (keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT) {
|
||||
kmsKeyMaterial = randomSecureBytes(
|
||||
getByteLengthForSymmetricEncryptionAlgorithm(encryptionAlgorithm as SymmetricKeyAlgorithm)
|
||||
);
|
||||
} else if (keyUsage === KmsKeyUsage.SIGN_VERIFY) {
|
||||
const { generateAsymmetricPrivateKey, getPublicKeyFromPrivateKey } = signingService(
|
||||
encryptionAlgorithm as AsymmetricKeyAlgorithm
|
||||
);
|
||||
kmsKeyMaterial = await generateAsymmetricPrivateKey();
|
||||
|
||||
// daniel: safety check to ensure we're able to extract the public key from the private key before we proceed to key creation
|
||||
getPublicKeyFromPrivateKey(kmsKeyMaterial);
|
||||
}
|
||||
|
||||
if (!kmsKeyMaterial) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid KMS key type. No key material was created for key usage '${keyUsage}' using algorithm '${encryptionAlgorithm}'`
|
||||
});
|
||||
}
|
||||
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
|
||||
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
const dbQuery = async (db: Knex) => {
|
||||
const kmsDoc = await kmsDAL.create(
|
||||
{
|
||||
name: sanitizedName,
|
||||
keyUsage,
|
||||
orgId,
|
||||
isReserved,
|
||||
projectId,
|
||||
@@ -115,6 +147,7 @@ export const kmsServiceFactory = ({
|
||||
);
|
||||
return kmsDoc;
|
||||
};
|
||||
|
||||
if (tx) return dbQuery(tx);
|
||||
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
|
||||
return doc;
|
||||
@@ -134,7 +167,7 @@ export const kmsServiceFactory = ({
|
||||
*/
|
||||
const encryptWithInputKey = async ({ key }: Omit<TEncryptionWithKeyDTO, "plainText">) => {
|
||||
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, key);
|
||||
// Buffer#1 encrypted text + Buffer#2 version number
|
||||
@@ -149,7 +182,7 @@ export const kmsServiceFactory = ({
|
||||
* This can be even later exposed directly as api for encryption as function
|
||||
*/
|
||||
const decryptWithInputKey = async ({ key }: Omit<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
|
||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKeyDTO, "cipherTextBlob">) => {
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
@@ -227,7 +260,7 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
|
||||
const encryptWithRootKey = () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
|
||||
return (plainTextBuffer: Buffer) => {
|
||||
const encryptedBuffer = cipher.encrypt(plainTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||
@@ -236,7 +269,7 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
|
||||
const decryptWithRootKey = () => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
|
||||
return (cipherTextBuffer: Buffer) => {
|
||||
return cipher.decrypt(cipherTextBuffer, ROOT_ENCRYPTION_KEY);
|
||||
@@ -315,9 +348,14 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
|
||||
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
|
||||
});
|
||||
|
||||
// internal KMS
|
||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return ({ cipherTextBlob: versionedCipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
@@ -345,19 +383,22 @@ export const kmsServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return kmsKey;
|
||||
};
|
||||
|
||||
const importKeyMaterial = async (
|
||||
{ key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO,
|
||||
{ key, algorithm, name, isReserved, projectId, orgId, keyUsage }: TImportKeyMaterialDTO,
|
||||
tx?: Knex
|
||||
) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
// daniel: currently we only support imports for encrypt/decrypt keys
|
||||
verifyKeyTypeAndAlgorithm(keyUsage, algorithm, { forceType: KmsKeyUsage.ENCRYPT_DECRYPT });
|
||||
|
||||
const expectedByteLength = getByteLengthForAlgorithm(algorithm);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
|
||||
const expectedByteLength = getByteLengthForSymmetricEncryptionAlgorithm(algorithm as SymmetricKeyAlgorithm);
|
||||
if (key.byteLength !== expectedByteLength) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
|
||||
@@ -370,6 +411,7 @@ export const kmsServiceFactory = ({
|
||||
const kmsDoc = await kmsDAL.create(
|
||||
{
|
||||
name: sanitizedName,
|
||||
keyUsage: KmsKeyUsage.ENCRYPT_DECRYPT,
|
||||
orgId,
|
||||
isReserved,
|
||||
projectId
|
||||
@@ -393,6 +435,74 @@ export const kmsServiceFactory = ({
|
||||
return doc;
|
||||
};
|
||||
|
||||
const getPublicKey = async ({ kmsId }: TGetPublicKeyDTO) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
|
||||
|
||||
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||
forceType: KmsKeyUsage.SIGN_VERIFY
|
||||
});
|
||||
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return signingService(encryptionAlgorithm).getPublicKeyFromPrivateKey(kmsKey);
|
||||
};
|
||||
|
||||
const signWithKmsKey = async ({ kmsId }: Pick<TSignWithKmsDTO, "kmsId">) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
|
||||
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||
forceType: KmsKeyUsage.SIGN_VERIFY
|
||||
});
|
||||
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const { sign } = signingService(encryptionAlgorithm);
|
||||
return async ({
|
||||
data,
|
||||
signingAlgorithm,
|
||||
isDigest
|
||||
}: Pick<TSignWithKmsDTO, "data" | "signingAlgorithm" | "isDigest">) => {
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
const signature = await sign(data, kmsKey, signingAlgorithm, isDigest);
|
||||
|
||||
return Promise.resolve({ signature, algorithm: signingAlgorithm });
|
||||
};
|
||||
};
|
||||
|
||||
const verifyWithKmsKey = async ({
|
||||
kmsId,
|
||||
signingAlgorithm
|
||||
}: Pick<TVerifyWithKmsDTO, "kmsId" | "signingAlgorithm">) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as AsymmetricKeyAlgorithm;
|
||||
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||
forceType: KmsKeyUsage.SIGN_VERIFY
|
||||
});
|
||||
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const { verify, getPublicKeyFromPrivateKey } = signingService(encryptionAlgorithm);
|
||||
return async ({ data, signature, isDigest }: Pick<TVerifyWithKmsDTO, "data" | "signature" | "isDigest">) => {
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
const publicKey = getPublicKeyFromPrivateKey(kmsKey);
|
||||
const signatureValid = await verify(data, signature, publicKey, signingAlgorithm, isDigest);
|
||||
return Promise.resolve({ signatureValid, algorithm: signingAlgorithm });
|
||||
};
|
||||
};
|
||||
|
||||
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
|
||||
if (!kmsDoc) {
|
||||
@@ -453,9 +563,14 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
}
|
||||
|
||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
|
||||
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
|
||||
});
|
||||
|
||||
// internal KMS
|
||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(kmsDoc.internalKms?.encryptionAlgorithm as SymmetricEncryption);
|
||||
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||
return ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
const encryptedPlainTextBlob = dataCipher.encrypt(plainText, kmsKey);
|
||||
@@ -729,7 +844,7 @@ export const kmsServiceFactory = ({
|
||||
|
||||
// case 2: root key is encrypted with software encryption
|
||||
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||
|
||||
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
||||
@@ -749,7 +864,7 @@ export const kmsServiceFactory = ({
|
||||
}
|
||||
|
||||
if (strategy === RootKeyEncryptionStrategy.Software) {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
const encryptionKeyBuffer = $getBasicEncryptionKey();
|
||||
|
||||
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
|
||||
@@ -765,7 +880,7 @@ export const kmsServiceFactory = ({
|
||||
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
|
||||
const dataKey = await $getDataKey(encryptionContext, trx);
|
||||
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||
|
||||
return {
|
||||
encryptor: ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
@@ -966,6 +1081,7 @@ export const kmsServiceFactory = ({
|
||||
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
|
||||
|
||||
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||
|
||||
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||
};
|
||||
|
||||
@@ -1014,6 +1130,9 @@ export const kmsServiceFactory = ({
|
||||
getKmsById,
|
||||
createCipherPairWithDataKey,
|
||||
getKeyMaterial,
|
||||
importKeyMaterial
|
||||
importKeyMaterial,
|
||||
signWithKmsKey,
|
||||
verifyWithKmsKey,
|
||||
getPublicKey
|
||||
};
|
||||
};
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { SymmetricKeyAlgorithm } from "@app/lib/crypto/cipher";
|
||||
import { AsymmetricKeyAlgorithm, SigningAlgorithm } from "@app/lib/crypto/sign/types";
|
||||
|
||||
export enum KmsDataKey {
|
||||
Organization,
|
||||
@@ -13,6 +14,11 @@ export enum KmsType {
|
||||
Internal = "internal"
|
||||
}
|
||||
|
||||
export enum KmsKeyUsage {
|
||||
ENCRYPT_DECRYPT = "encrypt-decrypt",
|
||||
SIGN_VERIFY = "sign-verify"
|
||||
}
|
||||
|
||||
export type TEncryptWithKmsDataKeyDTO =
|
||||
| { type: KmsDataKey.Organization; orgId: string }
|
||||
| { type: KmsDataKey.SecretManager; projectId: string };
|
||||
@@ -25,7 +31,8 @@ export type TEncryptWithKmsDataKeyDTO =
|
||||
export type TGenerateKMSDTO = {
|
||||
orgId: string;
|
||||
projectId?: string;
|
||||
encryptionAlgorithm?: SymmetricEncryption;
|
||||
encryptionAlgorithm?: SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm;
|
||||
keyUsage?: KmsKeyUsage;
|
||||
isReserved?: boolean;
|
||||
name?: string;
|
||||
description?: string;
|
||||
@@ -37,6 +44,25 @@ export type TEncryptWithKmsDTO = {
|
||||
plainText: Buffer;
|
||||
};
|
||||
|
||||
export type TGetPublicKeyDTO = {
|
||||
kmsId: string;
|
||||
};
|
||||
|
||||
export type TSignWithKmsDTO = {
|
||||
kmsId: string;
|
||||
data: Buffer;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
isDigest: boolean;
|
||||
};
|
||||
|
||||
export type TVerifyWithKmsDTO = {
|
||||
kmsId: string;
|
||||
data: Buffer;
|
||||
signature: Buffer;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
isDigest: boolean;
|
||||
};
|
||||
|
||||
export type TEncryptionWithKeyDTO = {
|
||||
key: Buffer;
|
||||
plainText: Buffer;
|
||||
@@ -67,9 +93,10 @@ export type TGetKeyMaterialDTO = {
|
||||
|
||||
export type TImportKeyMaterialDTO = {
|
||||
key: Buffer;
|
||||
algorithm: SymmetricEncryption;
|
||||
algorithm: SymmetricKeyAlgorithm;
|
||||
name?: string;
|
||||
isReserved: boolean;
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
keyUsage: KmsKeyUsage;
|
||||
};
|
||||
|
4
docs/api-reference/endpoints/kms/signing/public-key.mdx
Normal file
4
docs/api-reference/endpoints/kms/signing/public-key.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Retrieve Public Key"
|
||||
openapi: "GET /api/v1/kms/keys/{keyId}/public-key"
|
||||
---
|
4
docs/api-reference/endpoints/kms/signing/sign.mdx
Normal file
4
docs/api-reference/endpoints/kms/signing/sign.mdx
Normal file
@@ -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"
|
||||
---
|
4
docs/api-reference/endpoints/kms/signing/verify.mdx
Normal file
4
docs/api-reference/endpoints/kms/signing/verify.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Verify Signature"
|
||||
openapi: "POST /api/v1/kms/keys/{keyId}/verify"
|
||||
---
|
BIN
docs/images/integrations/external/backstage/backstage-plugin-infisical.png
vendored
Normal file
BIN
docs/images/integrations/external/backstage/backstage-plugin-infisical.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 548 KiB |
123
docs/integrations/external/backstage.mdx
vendored
Normal file
123
docs/integrations/external/backstage.mdx
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: Backstage Infisical Plugin
|
||||
description: A powerful plugin that integrates Infisical secrets management into your Backstage developer portal.
|
||||
---
|
||||
|
||||
Integrate secrets management into your developer portal with the Backstage Infisical plugin suite. This plugin provides a seamless interface to manage your [Infisical](https://infisical.com) secrets directly within Backstage, including full support for environments and folder structure.
|
||||
|
||||
## Features
|
||||
|
||||
- **Secrets Management**: View, create, update, and delete secrets from Infisical
|
||||
- **Folder Navigation**: Explore the full folder structure of your Infisical projects
|
||||
- **Multi-Environment Support**: Easily switch between and manage different environments
|
||||
- **Entity Linking**: Map Backstage entities to specific Infisical projects via annotations
|
||||
|
||||
---
|
||||
## Installation
|
||||
|
||||
### Frontend Plugin
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn --cwd packages/app add @infisical/backstage-plugin-infisical
|
||||
```
|
||||
|
||||
### Backend Plugin
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn --cwd packages/backend add @infisical/backstage-backend-plugin-infisical
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Backend
|
||||
|
||||
Update your `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
infisical:
|
||||
baseUrl: https://app.infisical.com
|
||||
|
||||
authentication:
|
||||
# Option 1: API Token Authentication
|
||||
auth_token:
|
||||
token: ${INFISICAL_API_TOKEN}
|
||||
|
||||
# Option 2: Client Credentials Authentication
|
||||
universalAuth:
|
||||
clientId: ${INFISICAL_CLIENT_ID}
|
||||
clientSecret: ${INFISICAL_CLIENT_SECRET}
|
||||
```
|
||||
|
||||
<Tip>
|
||||
If you have not created a machine identity yet, you can do so in [Identities](/documentation/platform/identities/machine-identities)
|
||||
</Tip>
|
||||
|
||||
Register the plugin in `packages/backend/src/index.ts`:
|
||||
|
||||
```ts
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(import('@infisical/backstage-backend-plugin-infisical'));
|
||||
|
||||
backend.start();
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
Update `packages/app/src/App.tsx` to include the plugin:
|
||||
|
||||
```tsx
|
||||
import { infisicalPlugin } from '@infisical/backstage-plugin-infisical';
|
||||
|
||||
const app = createApp({
|
||||
plugins: [
|
||||
infisicalPlugin,
|
||||
// ...other plugins
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Modify `packages/app/src/components/catalog/EntityPage.tsx`:
|
||||
|
||||
```tsx
|
||||
import { EntityInfisicalContent } from '@infisical/backstage-plugin-infisical';
|
||||
|
||||
const serviceEntityPage = (
|
||||
<EntityLayout>
|
||||
{/* ...other tabs */}
|
||||
<EntityLayout.Route path="/infisical" title="Secrets">
|
||||
<EntityInfisicalContent />
|
||||
</EntityLayout.Route>
|
||||
</EntityLayout>
|
||||
);
|
||||
```
|
||||
|
||||
### Entity Annotation
|
||||
|
||||
Add the Infisical project ID to your entity yaml settings:
|
||||
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: example-service
|
||||
annotations:
|
||||
infisical/projectId: <your-infisical-project-id>
|
||||
```
|
||||
|
||||
> Replace `<your-infisical-project-id>` with the actual project ID from Infisical.
|
||||
|
||||
## Usage
|
||||
|
||||
Once installed and configured, you can:
|
||||
|
||||
1. **View and manage secrets** in Infisical from within Backstage
|
||||
2. **Create, update, and delete** secrets using the Infisical tab in entity pages
|
||||
3. **Navigate environments and folders**
|
||||
4. **Search and filter** secrets by key, value, or comments
|
||||
|
||||

|
@@ -552,6 +552,12 @@
|
||||
"group": "Build Tool Integrations",
|
||||
"pages": ["integrations/build-tools/gradle"]
|
||||
},
|
||||
{
|
||||
"group": "Others",
|
||||
"pages": [
|
||||
"integrations/external/backstage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "",
|
||||
"pages": ["sdks/overview"]
|
||||
@@ -1319,9 +1325,23 @@
|
||||
"api-reference/endpoints/kms/keys/get-by-name",
|
||||
"api-reference/endpoints/kms/keys/create",
|
||||
"api-reference/endpoints/kms/keys/update",
|
||||
"api-reference/endpoints/kms/keys/delete",
|
||||
"api-reference/endpoints/kms/keys/encrypt",
|
||||
"api-reference/endpoints/kms/keys/decrypt"
|
||||
"api-reference/endpoints/kms/keys/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
Delete = "delete",
|
||||
Encrypt = "encrypt",
|
||||
Decrypt = "decrypt"
|
||||
Decrypt = "decrypt",
|
||||
Sign = "sign",
|
||||
Verify = "verify"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionKmipActions {
|
||||
|
27
frontend/src/helpers/kms.ts
Normal file
27
frontend/src/helpers/kms.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { AsymmetricKeyAlgorithm, KmsKeyUsage, SymmetricKeyAlgorithm } from "@app/hooks/api/cmeks";
|
||||
|
||||
export const kmsKeyUsageOptions: Record<
|
||||
KmsKeyUsage,
|
||||
{
|
||||
label: string;
|
||||
tooltip: string;
|
||||
}
|
||||
> = {
|
||||
[KmsKeyUsage.ENCRYPT_DECRYPT]: {
|
||||
label: "Encrypt/Decrypt",
|
||||
tooltip: "Use the key only to encrypt and decrypt data."
|
||||
},
|
||||
[KmsKeyUsage.SIGN_VERIFY]: {
|
||||
label: "Sign/Verify",
|
||||
tooltip:
|
||||
"Key pairs for digital signing. Uses the private key for signing and the public key for verification."
|
||||
}
|
||||
};
|
||||
|
||||
export const keyUsageDefaultOption: Record<
|
||||
KmsKeyUsage,
|
||||
SymmetricKeyAlgorithm | AsymmetricKeyAlgorithm
|
||||
> = {
|
||||
[KmsKeyUsage.ENCRYPT_DECRYPT]: SymmetricKeyAlgorithm.AES_GCM_256,
|
||||
[KmsKeyUsage.SIGN_VERIFY]: AsymmetricKeyAlgorithm.RSA_4096
|
||||
};
|
@@ -106,6 +106,10 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_CMEK]: "Get KMS key",
|
||||
[EventType.CMEK_ENCRYPT]: "Encrypt 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]:
|
||||
"Update 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",
|
||||
CMEK_ENCRYPT = "cmek-encrypt",
|
||||
CMEK_DECRYPT = "cmek-decrypt",
|
||||
CMEK_SIGN = "cmek-sign",
|
||||
CMEK_VERIFY = "cmek-verify",
|
||||
CMEK_LIST_SIGNING_ALGORITHMS = "cmek-list-signing-algorithms",
|
||||
CMEK_GET_PUBLIC_KEY = "cmek-get-public-key",
|
||||
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
|
||||
GET_PROJECT_TEMPLATES = "get-project-templates",
|
||||
|
@@ -8,6 +8,10 @@ import {
|
||||
TCmekDecryptResponse,
|
||||
TCmekEncrypt,
|
||||
TCmekEncryptResponse,
|
||||
TCmekSign,
|
||||
TCmekSignResponse,
|
||||
TCmekVerify,
|
||||
TCmekVerifyResponse,
|
||||
TCreateCmek,
|
||||
TDeleteCmek,
|
||||
TUpdateCmek
|
||||
@@ -74,6 +78,44 @@ export const useCmekEncrypt = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCmekSign = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
keyId,
|
||||
data,
|
||||
signingAlgorithm,
|
||||
isBase64Encoded
|
||||
}: TCmekSign & { isBase64Encoded: boolean }) => {
|
||||
const res = await apiRequest.post<TCmekSignResponse>(`/api/v1/kms/keys/${keyId}/sign`, {
|
||||
data: isBase64Encoded ? data : encodeBase64(Buffer.from(data)),
|
||||
signingAlgorithm
|
||||
});
|
||||
|
||||
return res.data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCmekVerify = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
keyId,
|
||||
data,
|
||||
signature,
|
||||
signingAlgorithm,
|
||||
isBase64Encoded
|
||||
}: TCmekVerify & { isBase64Encoded: boolean }) => {
|
||||
const res = await apiRequest.post<TCmekVerifyResponse>(`/api/v1/kms/keys/${keyId}/verify`, {
|
||||
data: isBase64Encoded ? data : encodeBase64(Buffer.from(data)),
|
||||
signature,
|
||||
signingAlgorithm
|
||||
});
|
||||
|
||||
return res.data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCmekDecrypt = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => {
|
||||
|
@@ -1,10 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
|
||||
export enum KmsKeyUsage {
|
||||
ENCRYPT_DECRYPT = "encrypt-decrypt",
|
||||
SIGN_VERIFY = "sign-verify"
|
||||
}
|
||||
|
||||
export type TCmek = {
|
||||
id: string;
|
||||
keyUsage: KmsKeyUsage;
|
||||
name: string;
|
||||
description?: string;
|
||||
encryptionAlgorithm: EncryptionAlgorithm;
|
||||
encryptionAlgorithm: AsymmetricKeyAlgorithm | SymmetricKeyAlgorithm;
|
||||
projectId: string;
|
||||
isDisabled: boolean;
|
||||
isReserved: boolean;
|
||||
@@ -17,7 +25,8 @@ export type TCmek = {
|
||||
type ProjectRef = { projectId: string };
|
||||
type KeyRef = { keyId: string };
|
||||
|
||||
export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm"> & ProjectRef;
|
||||
export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm" | "keyUsage"> &
|
||||
ProjectRef;
|
||||
export type TUpdateCmek = KeyRef &
|
||||
Partial<Pick<TCmek, "name" | "description" | "isDisabled">> &
|
||||
ProjectRef;
|
||||
@@ -26,6 +35,13 @@ export type TDeleteCmek = KeyRef & ProjectRef;
|
||||
export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean };
|
||||
export type TCmekDecrypt = KeyRef & { ciphertext: string };
|
||||
|
||||
export type TCmekSign = KeyRef & { data: string; signingAlgorithm: SigningAlgorithm };
|
||||
export type TCmekVerify = KeyRef & {
|
||||
data: string;
|
||||
signature: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
};
|
||||
|
||||
export type TProjectCmeksList = {
|
||||
keys: TCmek[];
|
||||
totalCount: number;
|
||||
@@ -44,6 +60,18 @@ export type TCmekEncryptResponse = {
|
||||
ciphertext: string;
|
||||
};
|
||||
|
||||
export type TCmekSignResponse = {
|
||||
signature: string;
|
||||
keyId: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
};
|
||||
|
||||
export type TCmekVerifyResponse = {
|
||||
signatureValid: boolean;
|
||||
keyId: string;
|
||||
signingAlgorithm: SigningAlgorithm;
|
||||
};
|
||||
|
||||
export type TCmekDecryptResponse = {
|
||||
plaintext: string;
|
||||
};
|
||||
@@ -52,7 +80,35 @@ export enum CmekOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
export enum EncryptionAlgorithm {
|
||||
export enum 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_128 = "aes-128-gcm"
|
||||
}
|
||||
|
||||
export const AllowedEncryptionKeyAlgorithms = z.enum([
|
||||
...Object.values(SymmetricKeyAlgorithm),
|
||||
...Object.values(AsymmetricKeyAlgorithm)
|
||||
] as [string, ...string[]]).options;
|
||||
|
||||
export enum SigningAlgorithm {
|
||||
// RSA PSS algorithms
|
||||
RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256",
|
||||
RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384",
|
||||
RSASSA_PSS_SHA_512 = "RSASSA_PSS_SHA_512",
|
||||
|
||||
// RSA PKCS#1 v1.5 algorithms
|
||||
RSASSA_PKCS1_V1_5_SHA_256 = "RSASSA_PKCS1_V1_5_SHA_256",
|
||||
RSASSA_PKCS1_V1_5_SHA_384 = "RSASSA_PKCS1_V1_5_SHA_384",
|
||||
RSASSA_PKCS1_V1_5_SHA_512 = "RSASSA_PKCS1_V1_5_SHA_512",
|
||||
|
||||
// ECDSA algorithms
|
||||
ECDSA_SHA_256 = "ECDSA_SHA_256",
|
||||
ECDSA_SHA_384 = "ECDSA_SHA_384",
|
||||
ECDSA_SHA_512 = "ECDSA_SHA_512"
|
||||
}
|
||||
|
14
frontend/src/lib/fn/base64.ts
Normal file
14
frontend/src/lib/fn/base64.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const base64WithPadding =
|
||||
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
|
||||
|
||||
export const isBase64 = (str: string): boolean => {
|
||||
if (typeof str !== "string") {
|
||||
throw new TypeError("Expected a string");
|
||||
}
|
||||
|
||||
if (str === "") return true;
|
||||
|
||||
const regex = base64WithPadding;
|
||||
|
||||
return regex.test(str);
|
||||
};
|
@@ -15,13 +15,23 @@ import {
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { EncryptionAlgorithm, TCmek, useCreateCmek, useUpdateCmek } from "@app/hooks/api/cmeks";
|
||||
import { keyUsageDefaultOption, kmsKeyUsageOptions } from "@app/helpers/kms";
|
||||
import {
|
||||
AllowedEncryptionKeyAlgorithms,
|
||||
AsymmetricKeyAlgorithm,
|
||||
KmsKeyUsage,
|
||||
SymmetricKeyAlgorithm,
|
||||
TCmek,
|
||||
useCreateCmek,
|
||||
useUpdateCmek
|
||||
} from "@app/hooks/api/cmeks";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: slugSchema({ min: 1, max: 32, field: "Name" }),
|
||||
description: z.string().max(500).optional(),
|
||||
encryptionAlgorithm: z.nativeEnum(EncryptionAlgorithm)
|
||||
encryptionAlgorithm: z.enum(AllowedEncryptionKeyAlgorithms),
|
||||
keyUsage: z.nativeEnum(KmsKeyUsage)
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof formSchema>;
|
||||
@@ -47,24 +57,33 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { isSubmitting, errors }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: cmek?.name,
|
||||
description: cmek?.description,
|
||||
encryptionAlgorithm: EncryptionAlgorithm.AES_GCM_256
|
||||
encryptionAlgorithm: 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
|
||||
? updateCmek.mutateAsync({ keyId: cmek.id, projectId, name, description })
|
||||
: createCmek.mutateAsync({
|
||||
projectId,
|
||||
encryptionAlgorithm,
|
||||
name,
|
||||
description
|
||||
description,
|
||||
keyUsage,
|
||||
encryptionAlgorithm: encryptionAlgorithm as AsymmetricKeyAlgorithm | SymmetricKeyAlgorithm
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -83,6 +102,8 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const selectedKeyUsage = watch("keyUsage");
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateCmek)}>
|
||||
<FormControl
|
||||
@@ -93,23 +114,97 @@ const CmekForm = ({ onComplete, cmek }: FormProps) => {
|
||||
>
|
||||
<Input autoFocus placeholder="my-secret-key" {...register("name")} />
|
||||
</FormControl>
|
||||
{!isUpdate && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="encryptionAlgorithm"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Algorithm" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select defaultValue={field.value} onValueChange={onChange} className="w-full">
|
||||
{Object.entries(EncryptionAlgorithm)?.map(([key, value]) => (
|
||||
<SelectItem value={value} key={`source-environment-${key}`}>
|
||||
{key.replaceAll("_", "-")}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{!isUpdate && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyUsage"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-full"
|
||||
tooltipText={
|
||||
<div className="space-y-4">
|
||||
{Object.entries(KmsKeyUsage).map(([key, value]) => (
|
||||
<div key={`key-usage-${key}`}>
|
||||
<p className="font-bold">{kmsKeyUsageOptions[value].label}</p>
|
||||
<p>{kmsKeyUsageOptions[value].tooltip}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
label="Key Usage"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
onValueChange={(e) => {
|
||||
if (keyUsageDefaultOption[e as 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
|
||||
label="Description (optional)"
|
||||
errorText={errors.description?.message}
|
||||
|
195
frontend/src/pages/kms/OverviewPage/components/CmekSignModal.tsx
Normal file
195
frontend/src/pages/kms/OverviewPage/components/CmekSignModal.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheckCircle, faFileSignature, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { SigningAlgorithm, TCmek, useCmekSign } from "@app/hooks/api/cmeks";
|
||||
|
||||
const formSchema = z.object({
|
||||
data: z.string(),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
|
||||
isBase64Encoded: z.boolean()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
cmek: TCmek;
|
||||
};
|
||||
|
||||
type FormProps = Pick<Props, "cmek">;
|
||||
|
||||
const SignForm = ({ cmek }: FormProps) => {
|
||||
const cmekSign = useCmekSign();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
control,
|
||||
formState: { isSubmitting, errors }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
signingAlgorithm: cmek?.encryptionAlgorithm?.startsWith("RSA")
|
||||
? SigningAlgorithm.RSASSA_PSS_SHA_512
|
||||
: SigningAlgorithm.ECDSA_SHA_256,
|
||||
isBase64Encoded: false
|
||||
}
|
||||
});
|
||||
|
||||
const [copySignature, isCopyingSignature, setCopySignature] = useTimedReset<string>({
|
||||
initialState: "Copy to Clipboard"
|
||||
});
|
||||
|
||||
const handleSignData = async (formData: FormData) => {
|
||||
try {
|
||||
await cmekSign.mutateAsync({ ...formData, keyId: cmek.id });
|
||||
createNotification({
|
||||
text: "Successfully signed data",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to sign data",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const signature = cmekSign.data?.signature;
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
navigator.clipboard.writeText(signature ?? "");
|
||||
|
||||
setCopySignature("Copied to Clipboard");
|
||||
};
|
||||
|
||||
const allowedSigningAlgorithms = Object.values(SigningAlgorithm).filter((a) =>
|
||||
cmek?.encryptionAlgorithm?.startsWith("RSA")
|
||||
? a.toLowerCase().startsWith("rsa")
|
||||
: a.toLowerCase().startsWith("ecdsa")
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleSignData)}>
|
||||
{signature ? (
|
||||
<FormControl label="Data Signature">
|
||||
<TextArea
|
||||
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
|
||||
isDisabled
|
||||
value={signature}
|
||||
/>
|
||||
</FormControl>
|
||||
) : (
|
||||
<>
|
||||
<FormControl
|
||||
label="Data to Sign"
|
||||
errorText={errors.data?.message}
|
||||
isError={Boolean(errors.data)}
|
||||
>
|
||||
<TextArea
|
||||
{...register("data")}
|
||||
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="mb-6 flex w-full items-center justify-between gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="signingAlgorithm"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormControl label="Signing Algorithm">
|
||||
<Select onValueChange={onChange} value={value} className="w-full">
|
||||
{allowedSigningAlgorithms.map((a) => (
|
||||
<SelectItem key={a} value={a}>
|
||||
{a.replaceAll("_", " ")}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="isBase64Encoded"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch id="encode-base-64" isChecked={value} onCheckedChange={onChange}>
|
||||
Data is Base64 encoded{" "}
|
||||
<Tooltip content="Toggle this switch on if your data is already Base64 encoded to avoid redundant encoding.">
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||
</Tooltip>
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className={`mr-4 ${signature ? "w-44" : ""}`}
|
||||
size="sm"
|
||||
leftIcon={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
signature ? (
|
||||
isCopyingSignature ? (
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faFileSignature} />
|
||||
)
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faFileSignature} />
|
||||
)
|
||||
}
|
||||
onClick={signature ? handleCopyToClipboard : undefined}
|
||||
type={signature ? "button" : "submit"}
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{signature ? copySignature : "Sign"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
{signature ? "Close" : "Cancel"}
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const CmekSignModal = ({ isOpen, onOpenChange, cmek }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Sign Data"
|
||||
subTitle={
|
||||
<>
|
||||
Sign data using <span className="font-bold">{cmek?.name}</span>. Returns a Base64
|
||||
encoded signature.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SignForm cmek={cmek} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@@ -8,6 +8,7 @@ import {
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsis,
|
||||
faFileSignature,
|
||||
faInfoCircle,
|
||||
faKey,
|
||||
faLock,
|
||||
@@ -51,14 +52,17 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { kmsKeyUsageOptions } from "@app/helpers/kms";
|
||||
import { usePagination, usePopUp, useResetPageHelper, useTimedReset } from "@app/hooks";
|
||||
import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks";
|
||||
import { CmekOrderBy, TCmek } from "@app/hooks/api/cmeks/types";
|
||||
import { CmekOrderBy, KmsKeyUsage, TCmek } from "@app/hooks/api/cmeks/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
|
||||
import { CmekDecryptModal } from "./CmekDecryptModal";
|
||||
import { CmekEncryptModal } from "./CmekEncryptModal";
|
||||
import { CmekModal } from "./CmekModal";
|
||||
import { CmekSignModal } from "./CmekSignModal";
|
||||
import { CmekVerifyModal } from "./CmekVerifyModal";
|
||||
import { DeleteCmekModal } from "./DeleteCmekModal";
|
||||
|
||||
const getStatusBadgeProps = (
|
||||
@@ -123,7 +127,9 @@ export const CmekTable = () => {
|
||||
"upsertKey",
|
||||
"deleteKey",
|
||||
"encryptData",
|
||||
"decryptData"
|
||||
"decryptData",
|
||||
"signData",
|
||||
"verifyData"
|
||||
] as const);
|
||||
|
||||
const handleSort = () => {
|
||||
@@ -179,6 +185,15 @@ export const CmekTable = () => {
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
||||
const cannotSignData = permission.cannot(
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
||||
const cannotVerifyData = permission.cannot(
|
||||
ProjectPermissionCmekActions.Verify,
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
return (
|
||||
<motion.div
|
||||
key="kms-keys-tab"
|
||||
@@ -246,6 +261,7 @@ export const CmekTable = () => {
|
||||
</div>
|
||||
</Th>
|
||||
<Th>Key ID</Th>
|
||||
<Th>Key Usage</Th>
|
||||
<Th>Algorithm</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Version</Th>
|
||||
@@ -257,7 +273,15 @@ export const CmekTable = () => {
|
||||
{!isPending &&
|
||||
keys.length > 0 &&
|
||||
keys.map((cmek) => {
|
||||
const { name, id, version, description, encryptionAlgorithm, isDisabled } = cmek;
|
||||
const {
|
||||
name,
|
||||
id,
|
||||
version,
|
||||
description,
|
||||
encryptionAlgorithm,
|
||||
isDisabled,
|
||||
keyUsage
|
||||
} = cmek;
|
||||
const { variant, label } = getStatusBadgeProps(isDisabled);
|
||||
|
||||
return (
|
||||
@@ -295,6 +319,14 @@ export const CmekTable = () => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</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>
|
||||
<Badge variant={variant}>{label}</Badge>
|
||||
@@ -314,50 +346,104 @@ export const CmekTable = () => {
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[160px]">
|
||||
<Tooltip
|
||||
content={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
cannotEncryptData
|
||||
? "Access Restricted"
|
||||
: isDisabled
|
||||
? "Key Disabled"
|
||||
: ""
|
||||
}
|
||||
position="left"
|
||||
>
|
||||
<div>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handlePopUpOpen("encryptData", cmek)}
|
||||
icon={<FontAwesomeIcon icon={faLock} />}
|
||||
iconPos="left"
|
||||
isDisabled={cannotEncryptData || isDisabled}
|
||||
{keyUsage === KmsKeyUsage.ENCRYPT_DECRYPT && (
|
||||
<>
|
||||
<Tooltip
|
||||
content={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
cannotEncryptData
|
||||
? "Access Restricted"
|
||||
: isDisabled
|
||||
? "Key Disabled"
|
||||
: ""
|
||||
}
|
||||
position="left"
|
||||
>
|
||||
Encrypt Data
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
cannotDecryptData
|
||||
? "Access Restricted"
|
||||
: isDisabled
|
||||
? "Key Disabled"
|
||||
: ""
|
||||
}
|
||||
position="left"
|
||||
>
|
||||
<div>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handlePopUpOpen("decryptData", cmek)}
|
||||
icon={<FontAwesomeIcon icon={faLockOpen} />}
|
||||
iconPos="left"
|
||||
isDisabled={cannotDecryptData || isDisabled}
|
||||
<div>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handlePopUpOpen("encryptData", cmek)}
|
||||
icon={<FontAwesomeIcon icon={faLock} />}
|
||||
iconPos="left"
|
||||
isDisabled={cannotEncryptData || isDisabled}
|
||||
>
|
||||
Encrypt Data
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
cannotDecryptData
|
||||
? "Access Restricted"
|
||||
: isDisabled
|
||||
? "Key Disabled"
|
||||
: ""
|
||||
}
|
||||
position="left"
|
||||
>
|
||||
Decrypt Data
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handlePopUpOpen("decryptData", cmek)}
|
||||
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
|
||||
content={cannotEditKey ? "Access Restricted" : ""}
|
||||
position="left"
|
||||
@@ -456,6 +542,16 @@ export const CmekTable = () => {
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("decryptData", isOpen)}
|
||||
cmek={popUp.decryptData.data as TCmek}
|
||||
/>
|
||||
<CmekSignModal
|
||||
isOpen={popUp.signData.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("signData", isOpen)}
|
||||
cmek={popUp.signData.data as TCmek}
|
||||
/>
|
||||
<CmekVerifyModal
|
||||
isOpen={popUp.verifyData.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("verifyData", isOpen)}
|
||||
cmek={popUp.verifyData.data as TCmek}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
@@ -0,0 +1,251 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faFileSignature, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { decodeBase64 } from "tweetnacl-util";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { SigningAlgorithm, TCmek, useCmekVerify } from "@app/hooks/api/cmeks";
|
||||
import { isBase64 } from "@app/lib/fn/base64";
|
||||
|
||||
const formSchema = z.object({
|
||||
data: z.string().min(1, { message: "Data cannot be empty" }),
|
||||
signature: z
|
||||
.string()
|
||||
.min(1, { message: "Signature cannot be empty" })
|
||||
.superRefine((val, ctx) => {
|
||||
if (!isBase64(val)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Signature must be base64-encoded"
|
||||
});
|
||||
}
|
||||
}),
|
||||
signingAlgorithm: z.nativeEnum(SigningAlgorithm),
|
||||
isBase64Encoded: z.boolean()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
cmek: TCmek;
|
||||
};
|
||||
|
||||
type FormProps = Pick<Props, "cmek">;
|
||||
|
||||
const VerifyForm = ({ cmek }: FormProps) => {
|
||||
const cmekVerify = useCmekVerify();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
watch,
|
||||
control,
|
||||
formState: { isSubmitting, errors }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
signingAlgorithm: cmek?.encryptionAlgorithm?.startsWith("RSA")
|
||||
? SigningAlgorithm.RSASSA_PSS_SHA_512
|
||||
: SigningAlgorithm.ECDSA_SHA_256,
|
||||
isBase64Encoded: false
|
||||
}
|
||||
});
|
||||
|
||||
const handleVerifyData = async (formData: FormData) => {
|
||||
try {
|
||||
const result = await cmekVerify.mutateAsync({ ...formData, keyId: cmek.id });
|
||||
|
||||
if (result.signatureValid) {
|
||||
createNotification({
|
||||
text: "Successfully verified signature",
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
title: "Signature Verification Failed",
|
||||
text: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to sign data",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const signature = watch("signature");
|
||||
const data = watch("data");
|
||||
const isBase64Encoded = watch("isBase64Encoded");
|
||||
|
||||
const signatureValid = cmekVerify.data?.signatureValid;
|
||||
const signingAlgorithm = cmekVerify.data?.signingAlgorithm;
|
||||
|
||||
const allowedSigningAlgorithms = Object.values(SigningAlgorithm).filter((a) =>
|
||||
cmek?.encryptionAlgorithm?.startsWith("RSA")
|
||||
? a.toLowerCase().startsWith("rsa")
|
||||
: a.toLowerCase().startsWith("ecdsa")
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleVerifyData)}>
|
||||
{signatureValid !== undefined ? (
|
||||
<div className="mb-6 flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<span className="text-sm opacity-60">Signature Status:</span>
|
||||
<Badge variant={signatureValid ? "success" : "danger"}>
|
||||
<Tooltip
|
||||
content={
|
||||
signatureValid
|
||||
? "The signature is valid. signature was created using the same signing algorithm and key as the one used to sign the data."
|
||||
: "The signature is invalid. The signature was not created using the same signing algorithm and key as the one used to sign the data. The data and signature may have been tampered with."
|
||||
}
|
||||
>
|
||||
{signatureValid ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<p>Valid</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<p>Invalid</p>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm opacity-60">Signing Algorithm:</span>
|
||||
<Badge variant="primary">{signingAlgorithm}</Badge>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<span className="text-sm opacity-60">Signature:</span>{" "}
|
||||
<div className="whitespace-pre-wrap break-words rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 text-sm">
|
||||
{signature}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm opacity-60">Data:</span>{" "}
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 text-sm">
|
||||
{isBase64Encoded ? decodeBase64(data).toString() : data}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<FormControl
|
||||
label="Data to Verify"
|
||||
errorText={errors.data?.message}
|
||||
isError={Boolean(errors.data)}
|
||||
>
|
||||
<TextArea
|
||||
{...register("data")}
|
||||
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Signature of Data"
|
||||
tooltipText="Must be base64-encoded, like the signature you received when you signed the data."
|
||||
errorText={errors.signature?.message}
|
||||
isError={Boolean(errors.signature)}
|
||||
>
|
||||
<TextArea
|
||||
{...register("signature")}
|
||||
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="mb-6 flex w-full items-center justify-between gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="signingAlgorithm"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormControl label="Signing Algorithm">
|
||||
<Select onValueChange={onChange} value={value} className="w-full">
|
||||
{allowedSigningAlgorithms.map((a) => (
|
||||
<SelectItem key={a} value={a}>
|
||||
{a.replaceAll("_", " ")}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="isBase64Encoded"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch id="encode-base-64" isChecked={value} onCheckedChange={onChange}>
|
||||
Data is Base64 encoded{" "}
|
||||
<Tooltip content="Toggle this switch on if your data is already Base64 encoded to avoid redundant encoding.">
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||
</Tooltip>
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
{signatureValid === undefined && (
|
||||
<Button
|
||||
className="mr-4 w-44"
|
||||
size="sm"
|
||||
leftIcon={<FontAwesomeIcon icon={faFileSignature} />}
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
)}
|
||||
<ModalClose asChild>
|
||||
<Button
|
||||
colorSchema={signatureValid === undefined ? "secondary" : "primary"}
|
||||
variant={signatureValid === undefined ? "plain" : undefined}
|
||||
>
|
||||
{signatureValid !== undefined ? "Close" : "Cancel"}
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const CmekVerifyModal = ({ isOpen, onOpenChange, cmek }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Verify Signature"
|
||||
subTitle={
|
||||
<>
|
||||
Verify a signature using <span className="font-bold">{cmek?.name}</span>.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<VerifyForm cmek={cmek} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@@ -47,7 +47,9 @@ const CmekPolicyActionSchema = z.object({
|
||||
delete: z.boolean().optional(),
|
||||
create: z.boolean().optional(),
|
||||
encrypt: z.boolean().optional(),
|
||||
decrypt: z.boolean().optional()
|
||||
decrypt: z.boolean().optional(),
|
||||
sign: z.boolean().optional(),
|
||||
verify: z.boolean().optional()
|
||||
});
|
||||
|
||||
const DynamicSecretPolicyActionSchema = z.object({
|
||||
@@ -482,6 +484,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
const canCreate = action.includes(ProjectPermissionCmekActions.Create);
|
||||
const canEncrypt = action.includes(ProjectPermissionCmekActions.Encrypt);
|
||||
const canDecrypt = action.includes(ProjectPermissionCmekActions.Decrypt);
|
||||
const canSign = action.includes(ProjectPermissionCmekActions.Sign);
|
||||
const canVerify = action.includes(ProjectPermissionCmekActions.Verify);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
@@ -492,6 +496,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
if (canDelete) formVal[subject]![0].delete = true;
|
||||
if (canEncrypt) formVal[subject]![0].encrypt = true;
|
||||
if (canDecrypt) formVal[subject]![0].decrypt = true;
|
||||
if (canSign) formVal[subject]![0].sign = true;
|
||||
if (canVerify) formVal[subject]![0].verify = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -769,7 +775,9 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" },
|
||||
{ label: "Encrypt", value: "encrypt" },
|
||||
{ label: "Decrypt", value: "decrypt" }
|
||||
{ label: "Decrypt", value: "decrypt" },
|
||||
{ label: "Sign", value: "sign" },
|
||||
{ label: "Verify", value: "verify" }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.Kms]: {
|
||||
|
Reference in New Issue
Block a user