diff --git a/.vscode/settings.json b/.vscode/settings.json index c2307ab..112c398 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,17 @@ "./crates/infisical-c/Cargo.toml", "./crates/infisical-py/Cargo.toml" ], - "cSpell.words": ["dotenv", "infisical", "libinfisical", "openapi", "openapitools", "quicktype", "reqwest"], + "cSpell.words": [ + "ciphertext", + "dotenv", + "indicies", + "infisical", + "libinfisical", + "openapi", + "openapitools", + "quicktype", + "reqwest", + "rngs" + ], "java.configuration.updateBuildConfiguration": "interactive" } diff --git a/crates/infisical-json/src/client.rs b/crates/infisical-json/src/client.rs index b3d7504..06f0b33 100644 --- a/crates/infisical-json/src/client.rs +++ b/crates/infisical-json/src/client.rs @@ -29,11 +29,23 @@ impl Client { }; match cmd { + // Infisical secrets Command::GetSecret(req) => self.0.secrets().get(&req).await.into_string(), Command::ListSecrets(req) => self.0.secrets().list(&req).await.into_string(), Command::CreateSecret(req) => self.0.secrets().create(&req).await.into_string(), Command::UpdateSecret(req) => self.0.secrets().update(&req).await.into_string(), Command::DeleteSecret(req) => self.0.secrets().delete(&req).await.into_string(), + + // Symmetric cryptography + Command::DecryptSymmetric(req) => { + self.0.cryptography().decrypt_symmetric(&req).into_string() + } + Command::EncryptSymmetric(req) => { + self.0.cryptography().encrypt_symmetric(&req).into_string() + } + Command::CreateSymmetricKey(_) => { + self.0.cryptography().create_symmetric_key().into_string() + } } } diff --git a/crates/infisical-json/src/command.rs b/crates/infisical-json/src/command.rs index 800183b..e87e991 100644 --- a/crates/infisical-json/src/command.rs +++ b/crates/infisical-json/src/command.rs @@ -1,18 +1,31 @@ -use infisical::manager::{ - client_secrets::{CreateSecretOptions, GetSecretOptions, UpdateSecretOptions}, - secrets::{DeleteSecretOptions, ListSecretsOptions}, +use infisical::manager::secrets::{ + CreateSecretOptions, DeleteSecretOptions, GetSecretOptions, ListSecretsOptions, + UpdateSecretOptions, }; + +use infisical::manager::cryptography::{DecryptSymmetricOptions, EncryptSymmetricOptions}; + use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, JsonSchema, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] +// QuickType (our type generator), won't recognize the CreateSymmetricKey command unless it has an input. Super annoying, and this is quite a hacky workaround. +// This should be revised in the future. +pub struct ArbitraryOptions { + pub data: String, +} -// We expand this later +#[derive(Serialize, Deserialize, JsonSchema, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] pub enum Command { GetSecret(GetSecretOptions), ListSecrets(ListSecretsOptions), CreateSecret(CreateSecretOptions), UpdateSecret(UpdateSecretOptions), DeleteSecret(DeleteSecretOptions), + + CreateSymmetricKey(ArbitraryOptions), + EncryptSymmetric(EncryptSymmetricOptions), + DecryptSymmetric(DecryptSymmetricOptions), } diff --git a/crates/infisical-py/infisical_client/__init__.py b/crates/infisical-py/infisical_client/__init__.py index 33f8316..826710e 100644 --- a/crates/infisical-py/infisical_client/__init__.py +++ b/crates/infisical-py/infisical_client/__init__.py @@ -7,4 +7,10 @@ from .schemas import CreateSecretOptions as CreateSecretOptions from .schemas import ListSecretsOptions as ListSecretsOptions from .schemas import ClientSettings as ClientSettings -from .schemas import SecretElement as SecretElement \ No newline at end of file +from .schemas import SecretElement as SecretElement + +from .schemas import EncryptSymmetricOptions as EncryptSymmetricOptions +from .schemas import EncryptSymmetricResponse as EncryptSymmetricResponse + +from .schemas import DecryptSymmetricOptions as DecryptSymmetricOptions +from .schemas import DecryptSymmetricResponse as DecryptSymmetricResponse \ No newline at end of file diff --git a/crates/infisical-py/infisical_client/infisical_client.py b/crates/infisical-py/infisical_client/infisical_client.py index 1250a3f..94ba2c7 100644 --- a/crates/infisical-py/infisical_client/infisical_client.py +++ b/crates/infisical-py/infisical_client/infisical_client.py @@ -6,6 +6,12 @@ from .schemas import UpdateSecretOptions, ResponseForUpdateSecretResponse from .schemas import DeleteSecretOptions, ResponseForDeleteSecretResponse from .schemas import CreateSecretOptions, ResponseForCreateSecretResponse + +from .schemas import EncryptSymmetricOptions, EncryptSymmetricResponse, ResponseForEncryptSymmetricResponse +from .schemas import DecryptSymmetricOptions, DecryptSymmetricResponse, ResponseForDecryptSymmetricResponse + +from .schemas import ArbitraryOptions, ResponseForCreateSymmetricKeyResponse + import infisical_py import os @@ -58,4 +64,22 @@ def deleteSecret(self, options: DeleteSecretOptions) -> SecretElement: def createSecret(self, options: CreateSecretOptions) -> SecretElement: result = self._run_command(Command(create_secret=options)) - return ResponseForCreateSecretResponse.from_dict(result).data.secret \ No newline at end of file + return ResponseForCreateSecretResponse.from_dict(result).data.secret + + def createSymmetricKey(self) -> str: + + arbitraryOptions = ArbitraryOptions(data="") + + result = self._run_command(Command(create_symmetric_key=arbitraryOptions)) + + return ResponseForCreateSymmetricKeyResponse.from_dict(result).data.key + + def encryptSymmetric(self, options: EncryptSymmetricOptions) -> EncryptSymmetricResponse: + result = self._run_command(Command(encrypt_symmetric=options)) + + return ResponseForEncryptSymmetricResponse.from_dict(result).data + + def decryptSymmetric(self, options: DecryptSymmetricOptions) -> str: + result = self._run_command(Command(decrypt_symmetric=options)) + + return ResponseForDecryptSymmetricResponse.from_dict(result).data.decrypted \ No newline at end of file diff --git a/crates/infisical/Cargo.toml b/crates/infisical/Cargo.toml index 2ecf545..94251c5 100644 --- a/crates/infisical/Cargo.toml +++ b/crates/infisical/Cargo.toml @@ -46,3 +46,4 @@ env_logger = "0.10.1" seeded-random = "0.6.0" serial_test = "2.0.0" dotenv = "0.15.0" +aes-gcm = "0.10.3" diff --git a/crates/infisical/src/api/secrets/update_secret.rs b/crates/infisical/src/api/secrets/update_secret.rs index beda74a..3f8dc57 100644 --- a/crates/infisical/src/api/secrets/update_secret.rs +++ b/crates/infisical/src/api/secrets/update_secret.rs @@ -3,7 +3,6 @@ use crate::error::api_error_handler; use crate::helper::build_base_request; use crate::manager::secrets::{UpdateSecretOptions, UpdateSecretResponse}; use crate::{error::Result, Client}; -use log::debug; use reqwest::StatusCode; pub async fn update_secret_request( @@ -34,15 +33,6 @@ pub async fn update_secret_request( Err(e) => return Err(e), }; - let token = match client.auth.access_token { - Some(ref token) => format!("Bearer {}", token), - None => "".to_string(), - }; - - debug!("Creating secret with token: {}", token); - debug!("Creating secret with JSON body: {:?}", json); - debug!("Creating secret with url: {}", base_url); - let response = request.json(json).send().await?; let status = response.status(); diff --git a/crates/infisical/src/error.rs b/crates/infisical/src/error.rs index 7781baf..ca74716 100644 --- a/crates/infisical/src/error.rs +++ b/crates/infisical/src/error.rs @@ -18,6 +18,15 @@ pub enum Error { #[error("Something unexpected went wrong.")] UnknownError, + #[error("Failed to create symmetric key: {}", .message)] + CreateSymmetricKeyError { message: String }, + + #[error("Failed to encrypt symmetric key: {}", .message)] + EncryptSymmetricKeyError { message: String }, + + #[error("Failed to decrypt symmetric key: {}", .message)] + DecryptSymmetricKeyError { message: String }, + #[error("Missing access token.")] MissingAccessToken, diff --git a/crates/infisical/src/manager/client_cryptography.rs b/crates/infisical/src/manager/client_cryptography.rs new file mode 100644 index 0000000..d8b95c2 --- /dev/null +++ b/crates/infisical/src/manager/client_cryptography.rs @@ -0,0 +1,40 @@ +use crate::{error::Result, Client}; + +// DELETE SECRET +use super::cryptography::{ + decrypt_symmetric::{decrypt_symmetric, DecryptSymmetricOptions}, + encrypt_symmetric::{encrypt_symmetric, EncryptSymmetricOptions, EncryptSymmetricResponse}, + CreateSymmetricKeyResponse, DecryptSymmetricResponse, +}; +pub use crate::manager::cryptography::create_symmetric_key::create_symmetric_key; + +#[allow(dead_code)] +pub struct ClientCryptography<'a> { + pub(crate) client: &'a mut crate::Client, +} + +impl<'a> ClientCryptography<'a> { + pub fn create_symmetric_key(&'a mut self) -> Result { + create_symmetric_key() + } + + pub fn encrypt_symmetric( + &'a mut self, + input: &EncryptSymmetricOptions, + ) -> Result { + encrypt_symmetric(input) + } + + pub fn decrypt_symmetric( + &'a mut self, + input: &DecryptSymmetricOptions, + ) -> Result { + decrypt_symmetric(input) + } +} + +impl<'a> Client { + pub fn cryptography(&'a mut self) -> ClientCryptography<'a> { + ClientCryptography { client: self } + } +} diff --git a/crates/infisical/src/manager/cryptography/create_symmetric_key.rs b/crates/infisical/src/manager/cryptography/create_symmetric_key.rs new file mode 100644 index 0000000..536cb0a --- /dev/null +++ b/crates/infisical/src/manager/cryptography/create_symmetric_key.rs @@ -0,0 +1,26 @@ +use crate::error::Result; +use base64::engine::Engine; +use rand::Rng; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +macro_rules! b64_encode { + ($key:expr) => { + base64::engine::general_purpose::STANDARD.encode($key) + }; +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateSymmetricKeyResponse { + pub key: String, +} + +pub fn create_symmetric_key() -> Result { + // Generate a 256-bit key (32 bytes * 8 bits/byte) + let key: Vec = rand::thread_rng().gen::<[u8; 32]>().to_vec(); + + let encoded_key = b64_encode!(key); + + return Ok(CreateSymmetricKeyResponse { key: encoded_key }); +} diff --git a/crates/infisical/src/manager/cryptography/decrypt_symmetric.rs b/crates/infisical/src/manager/cryptography/decrypt_symmetric.rs new file mode 100644 index 0000000..2373f40 --- /dev/null +++ b/crates/infisical/src/manager/cryptography/decrypt_symmetric.rs @@ -0,0 +1,71 @@ +use crate::error::{Error, Result}; +use aes::cipher::generic_array::GenericArray; +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, +}; +use base64::engine::Engine; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; // Assuming you are using the 'aes-gcm' crate + +macro_rules! b64_decode { + ($key:expr) => { + base64::engine::general_purpose::STANDARD.decode($key) + }; +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct DecryptSymmetricOptions { + pub key: String, + pub ciphertext: String, + pub iv: String, + pub tag: String, +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct DecryptSymmetricResponse { + pub decrypted: String, +} + +pub fn decrypt_symmetric(input: &DecryptSymmetricOptions) -> Result { + let decoded_tag = b64_decode!(&input.tag).map_err(|e| Error::DecryptSymmetricKeyError { + message: e.to_string(), + })?; + let decoded_key = b64_decode!(&input.key).map_err(|e| Error::DecryptSymmetricKeyError { + message: e.to_string(), + })?; + let iv = b64_decode!(&input.iv).map_err(|e| Error::DecryptSymmetricKeyError { + message: e.to_string(), + })?; + let mut decoded_ciphertext = + b64_decode!(&input.ciphertext).map_err(|e| Error::DecryptSymmetricKeyError { + message: e.to_string(), + })?; + + // We modify the ciphertext a little bit here to remove the pre-existing tag, and append the tag that was provided as a parameter. + //decoded_ciphertext.truncate(decoded_ciphertext.len() - 16); + decoded_ciphertext.extend_from_slice(&decoded_tag); + + let nonce = GenericArray::from_slice(&iv); + + let cipher = + Aes256Gcm::new_from_slice(&decoded_key).map_err(|e| Error::DecryptSymmetricKeyError { + message: e.to_string(), + })?; + + let plaintext_bytes = cipher + .decrypt(nonce, decoded_ciphertext.as_ref()) + .map_err(|e| Error::DecryptSymmetricKeyError { + message: e.to_string(), + })?; + + return Ok(DecryptSymmetricResponse { + decrypted: String::from_utf8(plaintext_bytes) + .map_err(|e| Error::DecryptSymmetricKeyError { + message: e.to_string(), + }) + .expect("Failed to convert bytes to string."), + }); +} diff --git a/crates/infisical/src/manager/cryptography/encrypt_symmetric.rs b/crates/infisical/src/manager/cryptography/encrypt_symmetric.rs new file mode 100644 index 0000000..d898752 --- /dev/null +++ b/crates/infisical/src/manager/cryptography/encrypt_symmetric.rs @@ -0,0 +1,79 @@ +use crate::error::{Error, Result}; +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, +}; +use base64::engine::Engine; +use rand::{rngs::OsRng, RngCore}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +macro_rules! b64_encode { + ($key:expr) => { + base64::engine::general_purpose::STANDARD.encode($key) + }; +} + +macro_rules! b64_decode { + ($key:expr) => { + base64::engine::general_purpose::STANDARD.decode($key) + }; +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EncryptSymmetricOptions { + pub key: String, + pub plaintext: String, +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EncryptSymmetricResponse { + pub ciphertext: String, + pub iv: String, + pub tag: String, +} + +pub fn encrypt_symmetric(input: &EncryptSymmetricOptions) -> Result { + let key = &input.key; + let plaintext = &input.plaintext; + + // Generate a random IV + let mut iv = [0u8; 12]; + OsRng.fill_bytes(&mut iv); + + let decoded_key = b64_decode!(key).map_err(|e| Error::EncryptSymmetricKeyError { + message: e.to_string(), + })?; + + // Create a new AES256-GCM instance + let cipher = Aes256Gcm::new_from_slice(decoded_key.as_slice()).map_err(|e| { + Error::EncryptSymmetricKeyError { + message: e.to_string(), + } + })?; + + let ciphertext_r = &cipher + .encrypt(&iv.into(), plaintext.as_bytes()) + .map_err(|e| Error::EncryptSymmetricKeyError { + message: e.to_string(), + }); + + let mut ciphertext = ciphertext_r.as_ref().unwrap().clone(); + + // we need to take the last 16 bytes of the ciphertext and remove it from the ciphertext, and append it to the tag. + + let encryption_tag = &ciphertext.clone()[&ciphertext.clone().len() - 16..]; // This line is a bit confusing, but it basically takes the last 16 bytes of the ciphertext and stores it in a variable. + ciphertext.truncate(ciphertext.len() - 16); // This line removes the last 16 bytes of the ciphertext. + + let encoded_ciphertext = b64_encode!(&ciphertext.clone()); + let encoded_iv = b64_encode!(iv); + let encoded_tag = b64_encode!(encryption_tag); + + return Ok(EncryptSymmetricResponse { + ciphertext: encoded_ciphertext, + iv: encoded_iv, + tag: encoded_tag, + }); +} diff --git a/crates/infisical/src/manager/cryptography/mod.rs b/crates/infisical/src/manager/cryptography/mod.rs new file mode 100644 index 0000000..0125996 --- /dev/null +++ b/crates/infisical/src/manager/cryptography/mod.rs @@ -0,0 +1,8 @@ +pub mod create_symmetric_key; +pub mod decrypt_symmetric; +pub mod encrypt_symmetric; +pub use base64::engine::Engine; + +pub use create_symmetric_key::CreateSymmetricKeyResponse; +pub use decrypt_symmetric::{DecryptSymmetricOptions, DecryptSymmetricResponse}; +pub use encrypt_symmetric::{EncryptSymmetricOptions, EncryptSymmetricResponse}; diff --git a/crates/infisical/src/manager/mod.rs b/crates/infisical/src/manager/mod.rs index 2ca0342..232eb78 100644 --- a/crates/infisical/src/manager/mod.rs +++ b/crates/infisical/src/manager/mod.rs @@ -1,5 +1,5 @@ -pub mod secrets; - +pub mod client_cryptography; pub mod client_secrets; -pub use client_secrets::ClientSecrets; +pub mod cryptography; +pub mod secrets; diff --git a/crates/infisical/tests/cryptography.rs b/crates/infisical/tests/cryptography.rs new file mode 100644 index 0000000..f7ebe90 --- /dev/null +++ b/crates/infisical/tests/cryptography.rs @@ -0,0 +1,120 @@ +use dotenv::dotenv; +use infisical::{client::client_settings::ClientSettings, Client}; + +struct Environment { + client_id: String, + client_secret: String, + site_url: String, +} + +fn get_environment_variables() -> Environment { + dotenv().ok(); + + let client_id = std::env::var(&"INFISICAL_UNIVERSAL_CLIENT_ID") + .expect("INFISICAL_UNIVERSAL_CLIENT_ID not found in environment variables."); + let client_secret = std::env::var(&"INFISICAL_UNIVERSAL_CLIENT_SECRET") + .expect("INFISICAL_UNIVERSAL_CLIENT_SECRET not found in environment variables."); + let site_url = std::env::var(&"INFISICAL_SITE_URL") + .expect("INFISICAL_SITE_URL not found in environment variables."); + + let environment = Environment { + client_id, + client_secret, + site_url, + }; + + return environment; +} + +fn create_client() -> Client { + let environment = get_environment_variables(); + + let settings = ClientSettings { + client_id: Some(environment.client_id), + client_secret: Some(environment.client_secret), + access_token: None, + site_url: Some(environment.site_url), + cache_ttl: None, + }; + + let client = Client::new(Some(settings)); + + return client; +} + +#[cfg(test)] +mod tests { + + use infisical::manager::cryptography::{ + decrypt_symmetric::DecryptSymmetricOptions, encrypt_symmetric::EncryptSymmetricOptions, + }; + + use crate::create_client; + + #[tokio::test] + async fn test_create_key() { + let mut client = create_client(); + + let key = client + .cryptography() + .create_symmetric_key() + .expect("Failed to create key."); + + println!("Key: {}", key.key); + + assert_eq!(key.key.len(), 44); // It should be 44 because its base64 encoded, and 32 bytes long. + } + + #[tokio::test] + async fn test_encrypt_symmetric() { + let mut client = create_client(); + + let test_key = &client.cryptography().create_symmetric_key().unwrap(); // We define a static string so the output is predictable and measurable. + + let encrypt_options = EncryptSymmetricOptions { + key: test_key.key.clone(), + plaintext: "Infisical".to_string(), + }; + + let encrypted = client + .cryptography() + .encrypt_symmetric(&encrypt_options) + .expect("Failed to encrypt data."); + + assert!(encrypted.ciphertext.len() > 0); + assert_eq!(encrypted.tag.len(), 24); // It should be 24 because its base64 encoded, and 16 bytes long. + assert_eq!(encrypted.iv.len(), 16); // It should be 16 because its base64 encoded, and 12 bytes long. + } + + #[tokio::test] + async fn test_decrypt_symmetric() { + let mut client = create_client(); + let plaintext = &"Infisical rocks!".to_string(); + + let test_key = &client.cryptography().create_symmetric_key().unwrap(); // We define a static string so the output is predictable and measurable. + + let encrypt_options = EncryptSymmetricOptions { + key: test_key.key.clone(), + plaintext: plaintext.clone(), + }; + + let encrypted = client + .cryptography() + .encrypt_symmetric(&encrypt_options) + .expect("Failed to encrypt data."); + + let decrypt_options = DecryptSymmetricOptions { + key: test_key.key.clone(), + ciphertext: encrypted.ciphertext, + iv: encrypted.iv, + tag: encrypted.tag, + }; + + let decrypted = &client + .cryptography() + .decrypt_symmetric(&decrypt_options) + .expect("Failed to decrypt data."); + + assert_eq!(&decrypted.decrypted, plaintext); + } +} diff --git a/crates/sdk-schemas/src/main.rs b/crates/sdk-schemas/src/main.rs index d0cfd3f..f12d8e6 100644 --- a/crates/sdk-schemas/src/main.rs +++ b/crates/sdk-schemas/src/main.rs @@ -63,7 +63,11 @@ fn main() -> Result<()> { infisical::manager::secrets::UpdateSecretResponse, infisical::manager::secrets::DeleteSecretResponse, infisical::manager::secrets::CreateSecretResponse, - infisical::auth::AccessTokenSuccessResponse + infisical::auth::AccessTokenSuccessResponse, + + infisical::manager::cryptography::EncryptSymmetricResponse, + infisical::manager::cryptography::DecryptSymmetricResponse, + infisical::manager::cryptography::CreateSymmetricKeyResponse, }; Ok(()) diff --git a/languages/java/src/main/java/com/infisical/sdk/InfisicalClient.java b/languages/java/src/main/java/com/infisical/sdk/InfisicalClient.java index 1a938f7..ba908b7 100644 --- a/languages/java/src/main/java/com/infisical/sdk/InfisicalClient.java +++ b/languages/java/src/main/java/com/infisical/sdk/InfisicalClient.java @@ -85,6 +85,45 @@ public DeleteSecretResponseSecret deleteSecret(DeleteSecretOptions options) { return response.getData().getSecret(); } + public String createSymmetricKey() { + ArbitraryOptions options = new ArbitraryOptions(); + options.setData(""); + + Command command = new Command(); + command.setCreateSymmetricKey(options); + + ResponseForCreateSymmetricKeyResponse response = commandRunner.runCommand(command, + InfisicalClient + .throwingFunctionWrapper(Converter::ResponseForCreateSymmetricKeyResponseFromJsonString)); + + errorCheck(response.getSuccess(), response.getErrorMessage()); + return response.getData().getKey(); + } + + public EncryptSymmetricResponse encryptSymmetric(EncryptSymmetricOptions options) { + Command command = new Command(); + command.setEncryptSymmetric(options); + + ResponseForEncryptSymmetricResponse response = commandRunner.runCommand(command, + InfisicalClient + .throwingFunctionWrapper(Converter::ResponseForEncryptSymmetricResponseFromJsonString)); + + errorCheck(response.getSuccess(), response.getErrorMessage()); + return response.getData(); + } + + public DecryptSymmetricResponse decryptSymmetric(DecryptSymmetricOptions options) { + Command command = new Command(); + command.setDecryptSymmetric(options); + + ResponseForDecryptSymmetricResponse response = commandRunner.runCommand(command, + InfisicalClient + .throwingFunctionWrapper(Converter::ResponseForDecryptSymmetricResponseFromJsonString)); + + errorCheck(response.getSuccess(), response.getErrorMessage()); + return response.getData(); + } + private void errorCheck(boolean success, String errorMessage) { if (!success) { if (errorMessage.length() > 0) { diff --git a/languages/node/example.ts b/languages/node/example.ts deleted file mode 100644 index 9357dea..0000000 --- a/languages/node/example.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { GetSecretOptions, InfisicalClient, LogLevel } from "./src"; - -// Just need a random string for testing, nothing crazy. -const randomStr = () => Date.now().toString(36); -const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - -const uncached_client = new InfisicalClient({ - clientId: "CLIENT_ID", - clientSecret: "CLIENT_SECRET", - siteUrl: "http://localhost:8080", - cacheTtl: 0 -}); - -const cached_client = new InfisicalClient({ - clientId: "CLIENT_ID", - clientSecret: "CLIENT_SECRET", - siteUrl: "http://localhost:8080", - cacheTtl: 30 -}); - -(async () => { - /*const projectId = "6587ff06fe3abf0cb8bf1742"; - const environment = "dev"; - - await uncached_client.getSecret({ projectId, environment, secretName: "TEST" }); - await cached_client.getSecret({ projectId, environment, secretName: "TEST" }); - - console.time("uncached"); - for (let i = 0; i < 10; i++) { - await uncached_client.getSecret({ projectId, environment, secretName: "TEST" }).then(console.log); - } - console.timeEnd("uncached"); - - console.time("cached"); - for (let i = 0; i < 10; i++) { - await cached_client.getSecret({ projectId, environment, secretName: "TEST" }); - } - console.timeEnd("cached"); - - process.exit(0);*/ - - const getOptions = { - projectId: "PROJECT_ID", - environment: "dev", - secretName: "TEST" - } as const; - - const startSecret = await cached_client.getSecret(getOptions); - - console.time("fetch secret cached"); - await cached_client.getSecret(getOptions); - console.timeEnd("fetch secret cached"); - - // update the secret to remove from cache - /*await cached_client.updateSecret({ - ...getOptions, - secretValue: randomStr() - });*/ - - console.time("fetch secret uncached"); - await cached_client.getSecret(getOptions); - console.timeEnd("fetch secret uncached"); - - process.exit(0); -})(); diff --git a/languages/node/examples/cryptography.ts b/languages/node/examples/cryptography.ts new file mode 100644 index 0000000..054e173 --- /dev/null +++ b/languages/node/examples/cryptography.ts @@ -0,0 +1,37 @@ +import { InfisicalClient } from "../src"; + +const client = new InfisicalClient({ + clientId: "CLIENT_ID", + clientSecret: "CLIENT_SECRET", + siteUrl: "http://localhost:8080", + cacheTtl: 30 +}); + +(async () => { + // This key will be used for encryption and decryption. It will be different every time you execute the function. + const key = await client.createSymmetricKey(); + + console.log(`\n\nSymmetric key: ${key}\n\n`); + + const PLAIN_STR = "Infisical is awesome!"; + + console.log(`Plain string: ${PLAIN_STR}\n\n`); + + const encrypted = await client.encryptSymmetric({ + plaintext: PLAIN_STR, + key: key + }); + + console.log(`Encrypted string (b64): ${encrypted.ciphertext}`); + console.log(`IV (b64): ${encrypted.iv}`); + console.log(`Tag (b64): ${encrypted.tag}\n\n`); + + const decrypted = await client.decryptSymmetric({ + ciphertext: encrypted.ciphertext, + key: key, + iv: encrypted.iv, + tag: encrypted.tag + }); + + console.log(`Decrypted string: ${decrypted}\n\n`); +})(); diff --git a/languages/node/examples/secrets.ts b/languages/node/examples/secrets.ts new file mode 100644 index 0000000..fff975e --- /dev/null +++ b/languages/node/examples/secrets.ts @@ -0,0 +1,68 @@ +import { InfisicalClient, LogLevel } from "../src"; + +// Just need a random string for testing, nothing crazy. +const randomStr = () => Date.now().toString(36); +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +const uncachedClient = new InfisicalClient({ + clientId: "CLIENT_ID", + clientSecret: "CLIENT_SECRET", + siteUrl: "http://localhost:8080", // This is optional. You can remove it and it will default to https://app.infisical.com. + cacheTtl: 0, // Default is 300 seconds (5 minutes). + logLevel: LogLevel.Error // Optional, default is LogLevel.Error. +}); + +const cachedClient = new InfisicalClient({ + clientId: "CLIENT_ID", + clientSecret: "CLIENT_SECRET", + siteUrl: "http://localhost:8080", // This is optional. You can remove it and it will default to https://app.infisical.com. + cacheTtl: 300, // Default is 300 seconds (5 minutes). + logLevel: LogLevel.Error // Optional, default is LogLevel.Error. +}); + +// Make sure to change these values. +const projectId = "YOUR_PROJECT_ID"; +const environment = "dev"; + +async function main() { + await testCacheSpeeds(); + + // Get a secret, and update it afterwards, as an example. + const secretOptions = { + projectId: projectId, + environment: environment, + secretName: "TEST" + } as const; + + const secret = await cachedClient.getSecret(secretOptions); + console.log("Fetched secret", secret); + + const updatedSecret = await cachedClient.updateSecret({ + ...secretOptions, + secretValue: "NEW VALUE" + }); + console.log("Updated secret", updatedSecret); +} + +async function testCacheSpeeds() { + console.log("Testing cache speeds..."); + + await uncachedClient.getSecret({ projectId, environment, secretName: "TEST" }); + await cachedClient.getSecret({ projectId, environment, secretName: "TEST" }); + + const startUncached = Date.now(); + for (let i = 0; i < 10; i++) await uncachedClient.getSecret({ projectId, environment, secretName: "TEST" }).then(console.log); + const endUncached = Date.now(); + + const startCached = Date.now(); + for (let i = 0; i < 10; i++) await cachedClient.getSecret({ projectId, environment, secretName: "TEST" }); + const endCached = Date.now(); + + console.log(`Uncached: ${endUncached - startUncached}ms`); + console.log(`Cached: ${endCached - startCached}ms\n`); + + const percentage = (endUncached - startUncached) / (endCached - startCached); + console.log(`Cached fetched the same secret 10 times, ${percentage.toFixed(2)}x faster than uncached`); +} + +main(); diff --git a/languages/node/src/infisical_client/index.ts b/languages/node/src/infisical_client/index.ts index fac5b53..6fcd100 100644 --- a/languages/node/src/infisical_client/index.ts +++ b/languages/node/src/infisical_client/index.ts @@ -8,6 +8,9 @@ import type { UpdateSecretOptions, UpdateSecretResponse } from "./schemas"; import type { CreateSecretOptions, CreateSecretResponse } from "./schemas"; import type { DeleteSecretOptions, DeleteSecretResponse } from "./schemas"; +import type { DecryptSymmetricOptions } from "./schemas"; +import type { EncryptSymmetricOptions, EncryptSymmetricResponse } from "./schemas"; + export class InfisicalClient { #client: rust.InfisicalClient; @@ -90,6 +93,54 @@ export class InfisicalClient { return response.data.secret; } + + // Has to be a promise because our client is async + async createSymmetricKey(): Promise { + const command = await this.#client.runCommand( + Convert.commandToJson({ + createSymmetricKey: { + data: "" + } + }) + ); + const response = Convert.toResponseForCreateSymmetricKeyResponse(command); + + if (!response.success || response.data == null) { + throw new Error(response.errorMessage ?? "Something went wrong"); + } + + return response.data.key; + } + + async encryptSymmetric(options: EncryptSymmetricOptions): Promise { + const command = await this.#client.runCommand( + Convert.commandToJson({ + encryptSymmetric: options + }) + ); + const response = Convert.toResponseForEncryptSymmetricResponse(command); + + if (!response.success || response.data == null) { + throw new Error(response.errorMessage ?? "Something went wrong"); + } + + return response.data; + } + + async decryptSymmetric(options: DecryptSymmetricOptions): Promise { + const command = await this.#client.runCommand( + Convert.commandToJson({ + decryptSymmetric: options + }) + ); + const response = Convert.toResponseForDecryptSymmetricResponse(command); + + if (!response.success || response.data == null) { + throw new Error(response.errorMessage ?? "Something went wrong"); + } + + return response.data.decrypted; + } } export { LogLevel }; diff --git a/languages/node/tsconfig.json b/languages/node/tsconfig.json index bc2531f..dfee97b 100644 --- a/languages/node/tsconfig.json +++ b/languages/node/tsconfig.json @@ -10,5 +10,5 @@ "esModuleInterop": true }, "include": [".", "/infisical_client"], - "exclude": ["example.ts", "node_modules", "lib"] + "exclude": ["examples", "node_modules", "lib"] }