Skip to content

Commit

Permalink
feat: Add support for exporting secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
bradhe committed Nov 8, 2024
1 parent 4e3686f commit ef2bc1f
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 29 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"

[dependencies]
base64 = { workspace = true }
pem = { workspace = true }
rand = { workspace = true }
rsa = { workspace = true }
sha2 = { workspace = true }
34 changes: 27 additions & 7 deletions crates/crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand All @@ -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)
Expand All @@ -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);
}
}
1 change: 1 addition & 0 deletions crates/tower-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
35 changes: 35 additions & 0 deletions crates/tower-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EncryptedSecret>,
}

pub type Result<T> = std::result::Result<T, TowerError>;

pub struct Client {
Expand Down Expand Up @@ -188,6 +199,30 @@ impl Client {
Ok(res.secret)
}

pub async fn export_secrets(&self) -> Result<Vec<ExportedSecret>> {
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::<ExportSecretsResponse>(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<T>(&self, method: Method, path: &str, body: Option<Value>) -> Result<T>
where
T: for<'de> Deserialize<'de>,
Expand Down
16 changes: 15 additions & 1 deletion crates/tower-api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,24 @@ pub struct Secret {
pub created_at: DateTime<Utc>,
}

#[derive(Serialize, Deserialize)]
pub struct EncryptedSecret {
pub name: String,
pub encrypted_value: String,
pub created_at: DateTime<Utc>,
}

#[derive(Serialize, Deserialize)]
pub struct ExportedSecret {
pub name: String,
pub value: String,
pub created_at: DateTime<Utc>,
}

/// 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<Vec<T>, D::Error>
pub fn parse_nullable_sequence<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
Expand Down
2 changes: 1 addition & 1 deletion crates/tower-cmd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!()
Expand Down
60 changes: 40 additions & 20 deletions crates/tower-cmd/src/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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::<bool>("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) {
Expand Down

0 comments on commit ef2bc1f

Please sign in to comment.