From ef2bc1f9c08ed4b5f4d4d41533358cdde4959da9 Mon Sep 17 00:00:00 2001 From: Brad Heller Date: Fri, 8 Nov 2024 12:53:56 +0000 Subject: [PATCH] feat: Add support for exporting secrets --- Cargo.lock | 2 ++ crates/crypto/Cargo.toml | 1 + crates/crypto/src/lib.rs | 34 +++++++++++++++---- crates/tower-api/Cargo.toml | 1 + crates/tower-api/src/lib.rs | 35 +++++++++++++++++++ crates/tower-api/src/types.rs | 16 ++++++++- crates/tower-cmd/src/lib.rs | 2 +- crates/tower-cmd/src/secrets.rs | 60 ++++++++++++++++++++++----------- 8 files changed, 122 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f7a296..dc03851 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,7 @@ name = "crypto" version = "0.1.2" dependencies = [ "base64", + "pem", "rand", "rsa", "sha2", @@ -2072,6 +2073,7 @@ version = "0.1.2" dependencies = [ "chrono", "config", + "crypto", "log", "pem", "reqwest", diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index e3561af..9c12a22 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] base64 = { workspace = true } +pem = { workspace = true } rand = { workspace = true } rsa = { workspace = true } sha2 = { workspace = true } diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index 5d0550c..d276d76 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -4,6 +4,7 @@ use base64::prelude::*; use rsa::{ RsaPrivateKey, RsaPublicKey, Oaep, traits::PublicKeyParts, + pkcs1::EncodeRsaPublicKey, }; /// encrypt manages the process of encrypting long messages using the RSA algorithm and OAEP @@ -43,17 +44,29 @@ pub fn decrypt(key: RsaPrivateKey, ciphertext: String) -> String { String::from_utf8(res).unwrap() } +/// generate_key_pair creates a new 2048-bit public and private key for use in +/// encrypting/decrypting payloads destined for the Tower service. +pub fn generate_key_pair() -> (RsaPrivateKey, RsaPublicKey) { + let bits = 2048; + let private_key = RsaPrivateKey::new(&mut OsRng, bits).unwrap(); + let public_key = RsaPublicKey::from(&private_key); + (private_key, public_key) +} + +/// serialize_public_key takes an RSA public key and serializes it into a PEM-encoded string. +pub fn serialize_public_key(key: RsaPublicKey) -> String { + key.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF).unwrap() +} + #[cfg(test)] mod test { use super::*; - use rsa::{RsaPrivateKey, RsaPublicKey}; use rand::{distributions::Alphanumeric, Rng}; + use rsa::pkcs1::DecodeRsaPublicKey; #[test] fn test_encrypt_decrypt() { - let bits = 2048; - let private_key = RsaPrivateKey::new(&mut OsRng, bits).unwrap(); - let public_key = RsaPublicKey::from(&private_key); + let (private_key, public_key) = generate_key_pair(); let plaintext = "Hello, World!".to_string(); let ciphertext = encrypt(public_key, plaintext.clone()); @@ -64,9 +77,7 @@ mod test { #[test] fn test_encrypt_decrypt_long_messages() { - let bits = 2048; - let private_key = RsaPrivateKey::new(&mut OsRng, bits).unwrap(); - let public_key = RsaPublicKey::from(&private_key); + let (private_key, public_key) = generate_key_pair(); let plaintext: String = rand::thread_rng() .sample_iter(&Alphanumeric) @@ -79,4 +90,13 @@ mod test { assert_eq!(plaintext, decrypted); } + + #[test] + fn test_serialize_public_key() { + let (_private_key, public_key) = generate_key_pair(); + let serialized = serialize_public_key(public_key.clone()); + let deserialized = RsaPublicKey::from_pkcs1_pem(&serialized).unwrap(); + + assert_eq!(public_key, deserialized); + } } diff --git a/crates/tower-api/Cargo.toml b/crates/tower-api/Cargo.toml index 86d2355..fbf835b 100644 --- a/crates/tower-api/Cargo.toml +++ b/crates/tower-api/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] chrono = { workspace = true } config = { workspace = true } +crypto = { workspace = true } log = { workspace = true } pem = { workspace = true } reqwest = { workspace = true } diff --git a/crates/tower-api/src/lib.rs b/crates/tower-api/src/lib.rs index 208c8ff..4da6017 100644 --- a/crates/tower-api/src/lib.rs +++ b/crates/tower-api/src/lib.rs @@ -83,6 +83,17 @@ struct CreateSecretResponse { secret: Secret, } +#[derive(Serialize, Deserialize)] +struct ExportSecretsRequest { + public_key: String, +} + +#[derive(Serialize, Deserialize)] +struct ExportSecretsResponse { + #[serde(deserialize_with="parse_nullable_sequence")] + secrets: Vec, +} + pub type Result = std::result::Result; pub struct Client { @@ -188,6 +199,30 @@ impl Client { Ok(res.secret) } + pub async fn export_secrets(&self) -> Result> { + let (private_key, public_key) = crypto::generate_key_pair(); + + let data = ExportSecretsRequest { + public_key: crypto::serialize_public_key(public_key), + }; + + let body = serde_json::to_value(data).unwrap(); + let res = self.request::(Method::POST, "/api/secrets/export", Some(body)).await?; + + let secrets = res.secrets.iter().map(|secret| { + let encrypted_value = secret.encrypted_value.clone(); + let decrypted = crypto::decrypt(private_key.clone(), encrypted_value); + + ExportedSecret { + name: secret.name.clone(), + value: decrypted, + created_at: secret.created_at, + } + }).collect(); + + Ok(secrets) + } + async fn request(&self, method: Method, path: &str, body: Option) -> Result where T: for<'de> Deserialize<'de>, diff --git a/crates/tower-api/src/types.rs b/crates/tower-api/src/types.rs index 09e7b01..b14fed0 100644 --- a/crates/tower-api/src/types.rs +++ b/crates/tower-api/src/types.rs @@ -40,10 +40,24 @@ pub struct Secret { pub created_at: DateTime, } +#[derive(Serialize, Deserialize)] +pub struct EncryptedSecret { + pub name: String, + pub encrypted_value: String, + pub created_at: DateTime, +} + +#[derive(Serialize, Deserialize)] +pub struct ExportedSecret { + pub name: String, + pub value: String, + pub created_at: DateTime, +} + /// parse_nullable_sequence is a helper function that deserializes a sequence of items that may be /// null in the underlying data. This is useful for parsing content coming from the API that may or /// may not be null if the resultant data is empty. -fn parse_nullable_sequence<'de, D, T>(deserializer: D) -> Result, D::Error> +pub fn parse_nullable_sequence<'de, D, T>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, T: Deserialize<'de>, diff --git a/crates/tower-cmd/src/lib.rs b/crates/tower-cmd/src/lib.rs index a3a83d4..c778761 100644 --- a/crates/tower-cmd/src/lib.rs +++ b/crates/tower-cmd/src/lib.rs @@ -51,7 +51,7 @@ impl App { let apps_command = sub_matches.subcommand(); match apps_command { - Some(("list", _)) => secrets::do_list_secrets(config, client).await, + Some(("list", args)) => secrets::do_list_secrets(config, client, args).await, Some(("create", args)) => secrets::do_create_secret(config, client, args).await, Some(("delete", args)) => secrets::do_delete_secret(config, client, args.subcommand()).await, _ => unreachable!() diff --git a/crates/tower-cmd/src/secrets.rs b/crates/tower-cmd/src/secrets.rs index c2ca1c2..834a675 100644 --- a/crates/tower-cmd/src/secrets.rs +++ b/crates/tower-cmd/src/secrets.rs @@ -12,6 +12,11 @@ pub fn secrets_cmd() -> Command { .arg_required_else_help(true) .subcommand( Command::new("list") + .arg( + Arg::new("show") + .long("show") + .action(clap::ArgAction::SetTrue) + ) .about("List all of your secrets") ) .subcommand( @@ -37,29 +42,44 @@ pub fn secrets_cmd() -> Command { ) } -pub async fn do_list_secrets(_config: Config, client: Client) { - let res = client.list_secrets().await; - - match res { - Ok(secrets) => { - let headers = vec![ - "Secret".bold().yellow().to_string(), - "Preview".bold().yellow().to_string(), - ]; +pub async fn do_list_secrets(_config: Config, client: Client, args: &ArgMatches) { + let show = args.get_one::("show").unwrap_or(&false); - let data = secrets.iter().map(|sum| { + let (headers, data) = if *show { + match client.export_secrets().await { + Ok(secrets) => ( vec![ - sum.name.clone(), - sum.preview.dimmed().to_string(), - ] - }).collect(); - - output::table(headers, data); - }, - Err(err) => { - output::tower_error(err); + "Secret".bold().yellow().to_string(), + "Value".bold().yellow().to_string(), + ], + secrets.iter().map(|sum| { + vec![ + sum.name.clone(), + sum.value.dimmed().to_string(), + ] + }).collect(), + ), + Err(err) => return output::tower_error(err), } - } + } else { + match client.list_secrets().await { + Ok(secrets) => ( + vec![ + "Secret".bold().yellow().to_string(), + "Preview".bold().yellow().to_string(), + ], + secrets.iter().map(|sum| { + vec![ + sum.name.clone(), + sum.preview.dimmed().to_string(), + ] + }).collect(), + ), + Err(err) => return output::tower_error(err), + } + }; + + output::table(headers, data); } pub async fn do_create_secret(_config: Config, client: Client, args: &ArgMatches) {