From b4646476a8546f2f8d01ec12f868cf088f9811f7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 16 Sep 2025 15:22:33 +0400 Subject: [PATCH 1/3] feat: kms support --- src/api/endpoints/kms.ts | 61 +++++++++++++++ src/api/types/kms.ts | 135 +++++++++++++++++++++++++++++++++ src/custom/kms.ts | 124 +++++++++++++++++++++++++++++++ src/index.ts | 122 +++++++++++++++--------------- test/test-kms.ts | 157 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 535 insertions(+), 64 deletions(-) create mode 100644 src/api/endpoints/kms.ts create mode 100644 src/api/types/kms.ts create mode 100644 src/custom/kms.ts create mode 100644 test/test-kms.ts diff --git a/src/api/endpoints/kms.ts b/src/api/endpoints/kms.ts new file mode 100644 index 0000000..9b635f4 --- /dev/null +++ b/src/api/endpoints/kms.ts @@ -0,0 +1,61 @@ +import { ApiClient } from "../base"; +import { + CreateKmsKeyOptions, + CreateKmsKeyResponse, + DeleteKmsKeyOptions, + DeleteKmsKeyResponse, + GetKmsKeyByNameOptions, + GetKmsKeyByNameResponse, + KmsDecryptDataOptions, + KmsDecryptDataResponse, + KmsEncryptDataOptions, + KmsEncryptDataResponse, + KmsGetPublicKeyOptions, + KmsGetPublicKeyResponse, + KmsListSigningAlgorithmsOptions, + KmsListSigningAlgorithmsResponse, + KmsSignDataOptions, + KmsSignDataResponse, + KmsVerifyDataOptions, + KmsVerifyDataResponse +} from "../types/kms"; + +export class KmsApi { + constructor(private apiClient: ApiClient) {} + + async createKmsKey(data: CreateKmsKeyOptions): Promise { + return this.apiClient.post("/api/v1/kms/keys", data); + } + + async deleteKmsKey(data: DeleteKmsKeyOptions): Promise { + return this.apiClient.delete(`/api/v1/kms/keys/${data.keyId}`); + } + + async getKmsKeyByName(data: GetKmsKeyByNameOptions): Promise { + return this.apiClient.get(`/api/v1/kms/keys/key-name/${encodeURIComponent(data.name)}?projectId=${data.projectId}`); + } + + async encryptData(data: KmsEncryptDataOptions): Promise { + return this.apiClient.post(`/api/v1/kms/keys/${data.keyId}/encrypt`, data); + } + + async decryptData(data: KmsDecryptDataOptions): Promise { + return this.apiClient.post(`/api/v1/kms/keys/${data.keyId}/decrypt`, data); + } + + async signData(data: KmsSignDataOptions): Promise { + return this.apiClient.post(`/api/v1/kms/keys/${data.keyId}/sign`, data); + } + + async verifyData(data: KmsVerifyDataOptions): Promise { + return this.apiClient.post(`/api/v1/kms/keys/${data.keyId}/verify`, data); + } + + async listSigningAlgorithms(data: KmsListSigningAlgorithmsOptions): Promise { + return this.apiClient.get(`/api/v1/kms/keys/${data.keyId}/signing-algorithms`); + } + + async getSigningPublicKey(data: KmsGetPublicKeyOptions): Promise { + return this.apiClient.get(`/api/v1/kms/keys/${data.keyId}/public-key`); + } +} diff --git a/src/api/types/kms.ts b/src/api/types/kms.ts new file mode 100644 index 0000000..e590e61 --- /dev/null +++ b/src/api/types/kms.ts @@ -0,0 +1,135 @@ +export enum EncryptionAlgorithm { + RSA_4096 = "RSA_4096", + ECC_NIST_P256 = "ECC_NIST_P256", + AES_256_GCM = "aes-256-gcm", + AES_128_GCM = "aes-128-gcm" +} + +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" +} + +export enum KeyUsage { + ENCRYPTION = "encrypt-decrypt", + SIGNING = "sign-verify" +} + +export interface KmsKey { + id: string; + description: string; + isDisabled: boolean; + orgId: string; + name: string; + projectId: string; + keyUsage: KeyUsage; + version: number; + encryptionAlgorithm: EncryptionAlgorithm; +} + +export interface CreateKmsKeyOptions { + projectId: string; + name: string; + description?: string; + + keyUsage: KeyUsage; + encryptionAlgorithm: EncryptionAlgorithm; +} + +export interface CreateKmsKeyResponse { + key: KmsKey; +} + +export interface DeleteKmsKeyOptions { + keyId: string; +} + +export interface DeleteKmsKeyResponse { + key: KmsKey; +} + +export interface GetKmsKeyByNameOptions { + projectId: string; + name: string; +} + +export interface GetKmsKeyByNameResponse { + key: KmsKey; +} + +export interface KmsEncryptDataOptions { + keyId: string; + plaintext: string; +} + +export interface KmsEncryptDataResponse { + ciphertext: string; +} + +export interface KmsDecryptDataOptions { + keyId: string; + ciphertext: string; +} + +export interface KmsDecryptDataResponse { + plaintext: string; +} + +export interface KmsSignDataOptions { + keyId: string; + data: string; + signingAlgorithm: SigningAlgorithm; + isDigest?: boolean; +} + +export interface KmsSignDataResponse { + signature: string; + keyId: string; + signingAlgorithm: SigningAlgorithm; +} + +export interface KmsVerifyDataOptions { + keyId: string; + data: string; // must be base64 encoded + signature: string; + signingAlgorithm: SigningAlgorithm; + isDigest?: boolean; +} + +export interface KmsVerifyDataResponse { + signatureValid: boolean; + keyId: string; + signingAlgorithm: SigningAlgorithm; +} + +export interface KmsListSigningAlgorithmsOptions { + keyId: string; +} + +export interface KmsListSigningAlgorithmsResponse { + signingAlgorithms: SigningAlgorithm[]; +} + +export interface KmsGetPublicKeyOptions { + keyId: string; +} + +export interface KmsGetPublicKeyResponse { + publicKey: string; +} diff --git a/src/custom/kms.ts b/src/custom/kms.ts new file mode 100644 index 0000000..08dfaa2 --- /dev/null +++ b/src/custom/kms.ts @@ -0,0 +1,124 @@ +import { newInfisicalError } from "./errors"; +import { + CreateKmsKeyOptions, + DeleteKmsKeyOptions, + GetKmsKeyByNameOptions, + KmsDecryptDataOptions, + KmsEncryptDataOptions, + KmsGetPublicKeyOptions, + KmsListSigningAlgorithmsOptions, + KmsSignDataOptions, + KmsVerifyDataOptions +} from "../api/types/kms"; +import { KmsApi } from "../api/endpoints/kms"; + +export default class KmsClient { + constructor(private apiClient: KmsApi) {} + + keys = () => { + const create = async (options: CreateKmsKeyOptions) => { + try { + const res = await this.apiClient.createKmsKey(options); + return res.key; + } catch (err) { + throw newInfisicalError(err); + } + }; + + const deleteKmsKey = async (options: DeleteKmsKeyOptions) => { + try { + const res = await this.apiClient.deleteKmsKey(options); + return res.key; + } catch (err) { + throw newInfisicalError(err); + } + }; + + const getByName = async (options: GetKmsKeyByNameOptions) => { + try { + const res = await this.apiClient.getKmsKeyByName(options); + + console.log(res); + return res.key; + } catch (err) { + throw newInfisicalError(err); + } + }; + + return { + create, + delete: deleteKmsKey, + getByName + }; + }; + + encryption = () => { + const encrypt = async (options: KmsEncryptDataOptions) => { + try { + const res = await this.apiClient.encryptData(options); + return res.ciphertext; + } catch (err) { + throw newInfisicalError(err); + } + }; + + const decrypt = async (options: KmsDecryptDataOptions) => { + try { + const res = await this.apiClient.decryptData(options); + return res.plaintext; + } catch (err) { + throw newInfisicalError(err); + } + }; + + return { + encrypt, + decrypt + }; + }; + + signing = () => { + const sign = async (options: KmsSignDataOptions) => { + try { + const res = await this.apiClient.signData(options); + return res; + } catch (err) { + throw newInfisicalError(err); + } + }; + + const verify = async (options: KmsVerifyDataOptions) => { + try { + const res = await this.apiClient.verifyData(options); + return res; + } catch (err) { + throw newInfisicalError(err); + } + }; + + const listSigningAlgorithms = async (options: KmsListSigningAlgorithmsOptions) => { + try { + const res = await this.apiClient.listSigningAlgorithms(options); + return res.signingAlgorithms; + } catch (err) { + throw newInfisicalError(err); + } + }; + + const getPublicKey = async (options: KmsGetPublicKeyOptions) => { + try { + const res = await this.apiClient.getSigningPublicKey(options); + return res.publicKey; + } catch (err) { + throw newInfisicalError(err); + } + }; + + return { + sign, + verify, + listSigningAlgorithms, + getPublicKey + }; + }; +} diff --git a/src/index.ts b/src/index.ts index cde9a27..7d0a7a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,89 +12,83 @@ import DynamicSecretsClient from "./custom/dynamic-secrets"; import EnvironmentsClient from "./custom/environments"; import ProjectsClient from "./custom/projects"; import FoldersClient from "./custom/folders"; +import { KmsApi } from "./api/endpoints/kms"; +import KmsClient from "./custom/kms"; type InfisicalSDKOptions = { - siteUrl?: string; + siteUrl?: string; }; class InfisicalSDK { - private apiClient: ApiClient; + private apiClient: ApiClient; - // API instances - private authApi: AuthApi; - private secretsApi: SecretsApi; - private dynamicSecretsApi: DynamicSecretsApi; - private environmentsApi: EnvironmentsApi; - private projectsApi: ProjectsApi; - private foldersApi: FoldersApi; + // API instances + private authApi: AuthApi; + private secretsApi: SecretsApi; + private dynamicSecretsApi: DynamicSecretsApi; + private environmentsApi: EnvironmentsApi; + private projectsApi: ProjectsApi; + private foldersApi: FoldersApi; + private kmsApi: KmsApi; - // Domain clients - private authClient: AuthClient; - private secretsClient: SecretsClient; - private dynamicSecretsClient: DynamicSecretsClient; - private environmentsClient: EnvironmentsClient; - private projectsClient: ProjectsClient; - private foldersClient: FoldersClient; + // Domain clients + private authClient: AuthClient; + private secretsClient: SecretsClient; + private dynamicSecretsClient: DynamicSecretsClient; + private environmentsClient: EnvironmentsClient; + private projectsClient: ProjectsClient; + private foldersClient: FoldersClient; + private kmsClient: KmsClient; - constructor(options?: InfisicalSDKOptions) { - const baseURL = options?.siteUrl || "https://app.infisical.com"; + constructor(options?: InfisicalSDKOptions) { + const baseURL = options?.siteUrl || "https://app.infisical.com"; - // Initialize the base API client - this.apiClient = new ApiClient({ baseURL }); + // Initialize the base API client + this.apiClient = new ApiClient({ baseURL }); - // Initialize API service instances - this.authApi = new AuthApi(this.apiClient); - this.secretsApi = new SecretsApi(this.apiClient); - this.dynamicSecretsApi = new DynamicSecretsApi(this.apiClient); - this.environmentsApi = new EnvironmentsApi(this.apiClient); - this.projectsApi = new ProjectsApi(this.apiClient); - this.foldersApi = new FoldersApi(this.apiClient); + // Initialize API service instances + this.authApi = new AuthApi(this.apiClient); + this.secretsApi = new SecretsApi(this.apiClient); + this.dynamicSecretsApi = new DynamicSecretsApi(this.apiClient); + this.environmentsApi = new EnvironmentsApi(this.apiClient); + this.projectsApi = new ProjectsApi(this.apiClient); + this.foldersApi = new FoldersApi(this.apiClient); + this.kmsApi = new KmsApi(this.apiClient); - // Initialize domain clients - this.authClient = new AuthClient( - this.authenticate.bind(this), - this.authApi - ); - this.secretsClient = new SecretsClient(this.secretsApi); - this.dynamicSecretsClient = new DynamicSecretsClient( - this.dynamicSecretsApi - ); - this.environmentsClient = new EnvironmentsClient(this.environmentsApi); - this.projectsClient = new ProjectsClient(this.projectsApi); - this.foldersClient = new FoldersClient(this.foldersApi); - } + // Initialize domain clients + this.authClient = new AuthClient(this.authenticate.bind(this), this.authApi); + this.secretsClient = new SecretsClient(this.secretsApi); + this.dynamicSecretsClient = new DynamicSecretsClient(this.dynamicSecretsApi); + this.environmentsClient = new EnvironmentsClient(this.environmentsApi); + this.projectsClient = new ProjectsClient(this.projectsApi); + this.foldersClient = new FoldersClient(this.foldersApi); + this.kmsClient = new KmsClient(this.kmsApi); + } - private authenticate(accessToken: string) { - // Set the token on the API client - this.apiClient.setAccessToken(accessToken); + private authenticate(accessToken: string) { + // Set the token on the API client + this.apiClient.setAccessToken(accessToken); - // Reinitialize the auth client with the token - this.authClient = new AuthClient( - this.authenticate.bind(this), - this.authApi, - accessToken - ); + // Reinitialize the auth client with the token + this.authClient = new AuthClient(this.authenticate.bind(this), this.authApi, accessToken); - return this; - } + return this; + } - // Public methods to access domain clients - secrets = () => this.secretsClient; - environments = () => this.environmentsClient; - projects = () => this.projectsClient; - folders = () => this.foldersClient; - dynamicSecrets = () => this.dynamicSecretsClient; - auth = () => this.authClient; + // Public methods to access domain clients + secrets = () => this.secretsClient; + environments = () => this.environmentsClient; + projects = () => this.projectsClient; + folders = () => this.foldersClient; + dynamicSecrets = () => this.dynamicSecretsClient; + auth = () => this.authClient; + kms = () => this.kmsClient; } // Export main SDK class export { InfisicalSDK }; -export * from './api/types' +export * from "./api/types"; // Export types and enums from schemas -export { - TDynamicSecretProvider, - DynamicSecretProviders, - SqlProviders, -} from "./custom/schemas"; +export { TDynamicSecretProvider, DynamicSecretProviders, SqlProviders } from "./custom/schemas"; diff --git a/test/test-kms.ts b/test/test-kms.ts new file mode 100644 index 0000000..90a8057 --- /dev/null +++ b/test/test-kms.ts @@ -0,0 +1,157 @@ +import { InfisicalSDK } from "../src"; +import { EncryptionAlgorithm, KeyUsage, KmsKey } from "../src/api/types/kms"; + +(async () => { + const client = new InfisicalSDK({ + siteUrl: "https://app.infisical.com" // Optional, defaults to https://app.infisical.com + }); + + const universalAuthClientId = process.env.UNIVERSAL_AUTH_CLIENT_ID; + const universalAuthClientSecret = process.env.UNIVERSAL_AUTH_CLIENT_SECRET; + const projectId = process.env.PROJECT_ID; + + if (!universalAuthClientId || !universalAuthClientSecret) { + throw new Error("UNIVERSAL_AUTH_CLIENT_ID and UNIVERSAL_AUTH_CLIENT_SECRET must be set"); + } + + if (!projectId) { + throw new Error("PROJECT_ID must be set"); + } + + console.log("Logging in"); + + await client.auth().universalAuth.login({ + clientId: universalAuthClientId, + clientSecret: universalAuthClientSecret + }); + console.log("Logged in"); + + console.log("Creating keys"); + + const keysToCreate = [ + { + name: "test-aes-256-gcm", + keyUsage: KeyUsage.ENCRYPTION, + encryptionAlgorithm: EncryptionAlgorithm.AES_256_GCM + }, + { + name: "test-aes-128-gcm", + keyUsage: KeyUsage.ENCRYPTION, + encryptionAlgorithm: EncryptionAlgorithm.AES_128_GCM + }, + { + name: "test-ecc-nist-p256", + keyUsage: KeyUsage.SIGNING, + encryptionAlgorithm: EncryptionAlgorithm.ECC_NIST_P256 + }, + { + name: "test-rsa-4096", + keyUsage: KeyUsage.SIGNING, + encryptionAlgorithm: EncryptionAlgorithm.RSA_4096 + } + ] as const; + + console.log("Creating keys", keysToCreate); + + const createdKeys: KmsKey[] = []; + + // Create all the keys + for (const key of keysToCreate) { + const createdKey = await client.kms().keys().create({ + projectId, + description: key.name, + encryptionAlgorithm: key.encryptionAlgorithm, + keyUsage: key.keyUsage, + name: key.name + }); + console.log("Created key", createdKey.name); + createdKeys.push(createdKey); + } + + // Get all the keys by name + for (const createdKey of createdKeys) { + const key = await client.kms().keys().getByName({ + projectId: createdKey.projectId, + name: createdKey.name + }); + + console.log(key); + console.log("Got key by name", key.name); + } + + // Encrypt / decrypt data with encryption keys + + for (const createdKey of createdKeys) { + if (createdKey.keyUsage !== KeyUsage.ENCRYPTION) { + console.log("Skipping key for encryption mode:", createdKey.name); + continue; + } + + const encryptedData = await client + .kms() + .encryption() + .encrypt({ + keyId: createdKey.id, + plaintext: Buffer.from("test data").toString("base64") + }); + + const decryptedData = await client.kms().encryption().decrypt({ + keyId: createdKey.id, + ciphertext: encryptedData + }); + + console.log("Encrypted data:", { + raw: encryptedData + }); + console.log("Decrypted data:", { + raw: decryptedData, + decoded: Buffer.from(decryptedData, "base64").toString("utf-8") + }); + } + + // Sign / verify data with signing keys + for (const createdKey of createdKeys) { + if (createdKey.keyUsage !== KeyUsage.SIGNING) { + console.log("Skipping key for signing mode:", createdKey.name); + continue; + } + + const testData = Buffer.from("some test data to sign").toString("base64"); + + const publicKey = await client.kms().signing().getPublicKey({ + keyId: createdKey.id + }); + console.log(`Public key for key ${createdKey.name}:`, publicKey); + + const signingAlgorithms = await client.kms().signing().listSigningAlgorithms({ + keyId: createdKey.id + }); + + console.log(`Signing algorithms for key ${createdKey.name}:`, signingAlgorithms); + + for (const signingAlgorithm of signingAlgorithms) { + const signedData = await client.kms().signing().sign({ + keyId: createdKey.id, + data: testData, + signingAlgorithm: signingAlgorithm + }); + + console.log("Signed data:", signedData); + const verifiedData = await client.kms().signing().verify({ + keyId: createdKey.id, + data: testData, + signature: signedData.signature, + signingAlgorithm: signingAlgorithm + }); + console.log("Verified data:", verifiedData); + } + } + + // Delete all the keys + for (const createdKey of createdKeys) { + await client.kms().keys().delete({ + keyId: createdKey.id + }); + console.log("Deleted key", createdKey.name); + } +})(); From 13c99207f9b18596493bc8bd349b392dbaa781e2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 16 Sep 2025 16:15:42 +0400 Subject: [PATCH 2/3] removed all logs --- src/custom/kms.ts | 1 - src/custom/util.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/custom/kms.ts b/src/custom/kms.ts index 08dfaa2..fe9522a 100644 --- a/src/custom/kms.ts +++ b/src/custom/kms.ts @@ -38,7 +38,6 @@ export default class KmsClient { try { const res = await this.apiClient.getKmsKeyByName(options); - console.log(res); return res.key; } catch (err) { throw newInfisicalError(err); diff --git a/src/custom/util.ts b/src/custom/util.ts index 5420006..dced753 100644 --- a/src/custom/util.ts +++ b/src/custom/util.ts @@ -43,7 +43,6 @@ export const getAwsRegion = async () => { return identityResponse.data.region; } catch (e) { - console.error("Failed to retrieve AWS region"); throw e; } }; From 7286398eeac03fdec040c63310759c02c834f6a7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 16 Sep 2025 18:21:13 +0400 Subject: [PATCH 3/3] Update index.ts --- src/api/types/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/types/index.ts b/src/api/types/index.ts index c8ddc8e..7a03cd9 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -6,6 +6,7 @@ export * from "./dynamic-secrets"; export * from "./environments"; export * from "./projects"; export * from "./folders"; +export * from "./kms"; export interface ApiResponse { statusCode: number;