feat: kms support

This commit is contained in:
Daniel Hougaard
2025-09-16 15:22:33 +04:00
parent d20d88a4bf
commit b4646476a8
5 changed files with 535 additions and 64 deletions

61
src/api/endpoints/kms.ts Normal file
View File

@@ -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<CreateKmsKeyResponse> {
return this.apiClient.post<CreateKmsKeyResponse>("/api/v1/kms/keys", data);
}
async deleteKmsKey(data: DeleteKmsKeyOptions): Promise<DeleteKmsKeyResponse> {
return this.apiClient.delete<DeleteKmsKeyResponse>(`/api/v1/kms/keys/${data.keyId}`);
}
async getKmsKeyByName(data: GetKmsKeyByNameOptions): Promise<GetKmsKeyByNameResponse> {
return this.apiClient.get<GetKmsKeyByNameResponse>(`/api/v1/kms/keys/key-name/${encodeURIComponent(data.name)}?projectId=${data.projectId}`);
}
async encryptData(data: KmsEncryptDataOptions): Promise<KmsEncryptDataResponse> {
return this.apiClient.post<KmsEncryptDataResponse>(`/api/v1/kms/keys/${data.keyId}/encrypt`, data);
}
async decryptData(data: KmsDecryptDataOptions): Promise<KmsDecryptDataResponse> {
return this.apiClient.post<KmsDecryptDataResponse>(`/api/v1/kms/keys/${data.keyId}/decrypt`, data);
}
async signData(data: KmsSignDataOptions): Promise<KmsSignDataResponse> {
return this.apiClient.post<KmsSignDataResponse>(`/api/v1/kms/keys/${data.keyId}/sign`, data);
}
async verifyData(data: KmsVerifyDataOptions): Promise<KmsVerifyDataResponse> {
return this.apiClient.post<KmsVerifyDataResponse>(`/api/v1/kms/keys/${data.keyId}/verify`, data);
}
async listSigningAlgorithms(data: KmsListSigningAlgorithmsOptions): Promise<KmsListSigningAlgorithmsResponse> {
return this.apiClient.get<KmsListSigningAlgorithmsResponse>(`/api/v1/kms/keys/${data.keyId}/signing-algorithms`);
}
async getSigningPublicKey(data: KmsGetPublicKeyOptions): Promise<KmsGetPublicKeyResponse> {
return this.apiClient.get<KmsGetPublicKeyResponse>(`/api/v1/kms/keys/${data.keyId}/public-key`);
}
}

135
src/api/types/kms.ts Normal file
View File

@@ -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;
}

124
src/custom/kms.ts Normal file
View File

@@ -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
};
};
}

View File

@@ -12,89 +12,83 @@ import DynamicSecretsClient from "./custom/dynamic-secrets";
import EnvironmentsClient from "./custom/environments"; import EnvironmentsClient from "./custom/environments";
import ProjectsClient from "./custom/projects"; import ProjectsClient from "./custom/projects";
import FoldersClient from "./custom/folders"; import FoldersClient from "./custom/folders";
import { KmsApi } from "./api/endpoints/kms";
import KmsClient from "./custom/kms";
type InfisicalSDKOptions = { type InfisicalSDKOptions = {
siteUrl?: string; siteUrl?: string;
}; };
class InfisicalSDK { class InfisicalSDK {
private apiClient: ApiClient; private apiClient: ApiClient;
// API instances // API instances
private authApi: AuthApi; private authApi: AuthApi;
private secretsApi: SecretsApi; private secretsApi: SecretsApi;
private dynamicSecretsApi: DynamicSecretsApi; private dynamicSecretsApi: DynamicSecretsApi;
private environmentsApi: EnvironmentsApi; private environmentsApi: EnvironmentsApi;
private projectsApi: ProjectsApi; private projectsApi: ProjectsApi;
private foldersApi: FoldersApi; private foldersApi: FoldersApi;
private kmsApi: KmsApi;
// Domain clients // Domain clients
private authClient: AuthClient; private authClient: AuthClient;
private secretsClient: SecretsClient; private secretsClient: SecretsClient;
private dynamicSecretsClient: DynamicSecretsClient; private dynamicSecretsClient: DynamicSecretsClient;
private environmentsClient: EnvironmentsClient; private environmentsClient: EnvironmentsClient;
private projectsClient: ProjectsClient; private projectsClient: ProjectsClient;
private foldersClient: FoldersClient; private foldersClient: FoldersClient;
private kmsClient: KmsClient;
constructor(options?: InfisicalSDKOptions) { constructor(options?: InfisicalSDKOptions) {
const baseURL = options?.siteUrl || "https://app.infisical.com"; const baseURL = options?.siteUrl || "https://app.infisical.com";
// Initialize the base API client // Initialize the base API client
this.apiClient = new ApiClient({ baseURL }); this.apiClient = new ApiClient({ baseURL });
// Initialize API service instances // Initialize API service instances
this.authApi = new AuthApi(this.apiClient); this.authApi = new AuthApi(this.apiClient);
this.secretsApi = new SecretsApi(this.apiClient); this.secretsApi = new SecretsApi(this.apiClient);
this.dynamicSecretsApi = new DynamicSecretsApi(this.apiClient); this.dynamicSecretsApi = new DynamicSecretsApi(this.apiClient);
this.environmentsApi = new EnvironmentsApi(this.apiClient); this.environmentsApi = new EnvironmentsApi(this.apiClient);
this.projectsApi = new ProjectsApi(this.apiClient); this.projectsApi = new ProjectsApi(this.apiClient);
this.foldersApi = new FoldersApi(this.apiClient); this.foldersApi = new FoldersApi(this.apiClient);
this.kmsApi = new KmsApi(this.apiClient);
// Initialize domain clients // Initialize domain clients
this.authClient = new AuthClient( this.authClient = new AuthClient(this.authenticate.bind(this), this.authApi);
this.authenticate.bind(this), this.secretsClient = new SecretsClient(this.secretsApi);
this.authApi this.dynamicSecretsClient = new DynamicSecretsClient(this.dynamicSecretsApi);
); this.environmentsClient = new EnvironmentsClient(this.environmentsApi);
this.secretsClient = new SecretsClient(this.secretsApi); this.projectsClient = new ProjectsClient(this.projectsApi);
this.dynamicSecretsClient = new DynamicSecretsClient( this.foldersClient = new FoldersClient(this.foldersApi);
this.dynamicSecretsApi this.kmsClient = new KmsClient(this.kmsApi);
); }
this.environmentsClient = new EnvironmentsClient(this.environmentsApi);
this.projectsClient = new ProjectsClient(this.projectsApi);
this.foldersClient = new FoldersClient(this.foldersApi);
}
private authenticate(accessToken: string) { private authenticate(accessToken: string) {
// Set the token on the API client // Set the token on the API client
this.apiClient.setAccessToken(accessToken); this.apiClient.setAccessToken(accessToken);
// Reinitialize the auth client with the token // Reinitialize the auth client with the token
this.authClient = new AuthClient( this.authClient = new AuthClient(this.authenticate.bind(this), this.authApi, accessToken);
this.authenticate.bind(this),
this.authApi,
accessToken
);
return this; return this;
} }
// Public methods to access domain clients // Public methods to access domain clients
secrets = () => this.secretsClient; secrets = () => this.secretsClient;
environments = () => this.environmentsClient; environments = () => this.environmentsClient;
projects = () => this.projectsClient; projects = () => this.projectsClient;
folders = () => this.foldersClient; folders = () => this.foldersClient;
dynamicSecrets = () => this.dynamicSecretsClient; dynamicSecrets = () => this.dynamicSecretsClient;
auth = () => this.authClient; auth = () => this.authClient;
kms = () => this.kmsClient;
} }
// Export main SDK class // Export main SDK class
export { InfisicalSDK }; export { InfisicalSDK };
export * from './api/types' export * from "./api/types";
// Export types and enums from schemas // Export types and enums from schemas
export { export { TDynamicSecretProvider, DynamicSecretProviders, SqlProviders } from "./custom/schemas";
TDynamicSecretProvider,
DynamicSecretProviders,
SqlProviders,
} from "./custom/schemas";

157
test/test-kms.ts Normal file
View File

@@ -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);
}
})();