From 286310eb05f3279f88b6b4764f7faaa331e03314 Mon Sep 17 00:00:00 2001 From: holly sparkles Date: Mon, 18 Dec 2023 21:46:02 +0100 Subject: [PATCH] chore(formatting): rustfmt --- src/bwutil.rs | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------- src/main.rs | 251 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------ src/util.rs | 45 ++++++++++++++++++++++++++++----------------- 3 files changed, 228 insertions(+), 204 deletions(-) diff --git a/src/bwutil.rs b/src/bwutil.rs index 468cddd..e2295d2 100644 --- a/src/bwutil.rs +++ b/src/bwutil.rs @@ -1,118 +1,132 @@ -use std::process::{Command, Output, Stdio}; +use anyhow::{anyhow, Context, Result}; use colored::*; use serde::Deserialize; -use anyhow::{anyhow, Result, Context}; +use std::process::{Command, Output, Stdio}; /// Represents a Bitwarden folder. #[derive(Default, Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BitwardenFolder { - pub id: String, - pub name: String + pub id: String, + pub name: String, } /// Represents an item in Bitwarden containing an SSH key. #[derive(Default, Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BitwardenItem { - pub id: String, - pub name: String, - pub attachments: Option>, - pub fields: Option> + pub id: String, + pub name: String, + pub attachments: Option>, + pub fields: Option>, } /// Represents a custom field in Bitwarden. #[derive(Default, Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BitwardenFieldItem { - pub name: String, - pub value: Option + pub name: String, + pub value: Option, } /// Represents a file attachment in Bitwarden. #[derive(Default, Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BitwardenAttachment { - pub id: String, - pub file_name: String + pub id: String, + pub file_name: String, } /// Gets the user's logged-in status from Bitwarden. pub fn is_logged_in() -> Result { - let logged_in = Command::new("bw") - .arg("login") - .arg("--check") - .output()?; + let logged_in = Command::new("bw").arg("login").arg("--check").output()?; - Ok(logged_in.stdout.len() > 0) + Ok(logged_in.stdout.len() > 0) } /// Gets a session token from Bitwarden. pub fn get_session_token() -> Result { - let mut token = String::new(); - let mut operation = String::new(); - - // No session token found. check to see if Bitwarden needs a login or is locked - if is_logged_in()? { - println!("Vault is locked."); - operation.push_str("unlock") + let mut token = String::new(); + let mut operation = String::new(); - } - else { - println!("You are not logged in."); - operation.push_str("login") - } + // No session token found. check to see if Bitwarden needs a login or is locked + if is_logged_in()? { + println!("Vault is locked."); + operation.push_str("unlock") + } else { + println!("You are not logged in."); + operation.push_str("login") + } - let success: Output = exec_piped_command("bw", - ["--raw", &operation].to_vec()); - if success.status.success() { - token.push_str(&String::from_utf8(success.stdout)?); - } + let success: Output = exec_piped_command("bw", ["--raw", &operation].to_vec()); + if success.status.success() { + token.push_str(&String::from_utf8(success.stdout)?); + } - Ok(token) + Ok(token) } /// Search Bitwarden for folders matching `folder_name` and return the results. pub fn exec_folder_search(session: &str, folder_name: &str) -> Result> { - let folders: Output = exec_piped_command("bw", - ["list", "folders", "--search", &folder_name, "--session", &session].to_vec()); - let result: String = String::from_utf8_lossy(&folders.stdout).to_string(); - if result.is_empty() { - Err(anyhow!("Could not authenticate. This could mean your session has expired.".red())) - } - else { - Ok(serde_json::from_str(&result).with_context(|| "Could not deserialize folder search results.")?) - } + let folders: Output = exec_piped_command( + "bw", + [ + "list", + "folders", + "--search", + &folder_name, + "--session", + &session, + ] + .to_vec(), + ); + let result: String = String::from_utf8_lossy(&folders.stdout).to_string(); + if result.is_empty() { + Err(anyhow!( + "Could not authenticate. This could mean your session has expired.".red() + )) + } else { + Ok(serde_json::from_str(&result) + .with_context(|| "Could not deserialize folder search results.")?) + } } // List items in a folder matching `folder_id` and return the results pub fn exec_list_folder_items(session: &str, folder_id: &str) -> Result> { - let items = exec_piped_command("bw", - ["list", "items", "--folderid", &folder_id, "--session", &session].to_vec()); - let result = String::from_utf8_lossy(&items.stdout).to_string(); - if result.is_empty() { - Err(anyhow!("Could not authenticate. This could mean your session has expired.".red())) - } - else { - Ok(serde_json::from_str(&result).with_context(|| "Could not deserialize item search results.")?) - } + let items = exec_piped_command( + "bw", + [ + "list", + "items", + "--folderid", + &folder_id, + "--session", + &session, + ] + .to_vec(), + ); + let result = String::from_utf8_lossy(&items.stdout).to_string(); + if result.is_empty() { + Err(anyhow!( + "Could not authenticate. This could mean your session has expired.".red() + )) + } else { + Ok(serde_json::from_str(&result) + .with_context(|| "Could not deserialize item search results.")?) + } } /// Execute an interactive command. -/// +/// /// The resulting output will be returned as `Output`. This is a modified version of: -/// +/// /// https://users.rust-lang.org/t/command-if-a-child-process-is-asking-for-input-how-to-forward-the-question-to-the-user/37490/3 fn exec_piped_command(cmd: &str, args: Vec<&str>) -> Output { - let cli_command = match Command::new(cmd) - .args(&args) - .stdout(Stdio::piped()) - .spawn() - { + let cli_command = match Command::new(cmd).args(&args).stdout(Stdio::piped()).spawn() { Err(err) => panic!("Error spawning: {}", err), Ok(process) => process, }; - let output = cli_command.wait_with_output().unwrap(); + let output = cli_command.wait_with_output().unwrap(); return output; } diff --git a/src/main.rs b/src/main.rs index 90e2946..41fa601 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,95 +17,95 @@ const BW_FIELD_KEY_PASSPHRASE: &str = "BW_KEY_PASSPHRASE"; /// A macro to print when the debug CLI arg is enabled. macro_rules! debug_println { ($($arg:tt)*) => { - if let Ok(enabled) = env::var(BW_SSH_DEBUG_ENV_KEY) { - if bool::from_str(&enabled).unwrap_or(false) { - println!("{}",format!($($arg)*).yellow()); - } - } + if let Ok(enabled) = env::var(BW_SSH_DEBUG_ENV_KEY) { + if bool::from_str(&enabled).unwrap_or(false) { + println!("{}",format!($($arg)*).yellow()); + } + } } } macro_rules! warn_println { ($($arg:tt)*) => { - println!("{}",format!($($arg)*).yellow()); + println!("{}",format!($($arg)*).yellow()); } } macro_rules! info_println { ($($arg:tt)*) => { - println!("{}",format!($($arg)*).green()); + println!("{}",format!($($arg)*).green()); } } fn main() -> Result<()> { - // This environment variable only set when calling ssh-agent, so return the passphrase to authenticate and then quit. - let unlock_passphrase = env::var(BW_FIELD_KEY_PASSPHRASE); - if unlock_passphrase.is_ok() { - println!("{}", unlock_passphrase.unwrap()); - return Ok(()) - } - - // Parse args and set the debug environment variable + // This environment variable only set when calling ssh-agent, so return the passphrase to authenticate and then quit. + let unlock_passphrase = env::var(BW_FIELD_KEY_PASSPHRASE); + if unlock_passphrase.is_ok() { + println!("{}", unlock_passphrase.unwrap()); + return Ok(()) + } + + // Parse args and set the debug environment variable let args: util::Cli = util::Cli::parse_args_default_or_exit(); - env::set_var(BW_SSH_DEBUG_ENV_KEY, OsString::from(args.debug.to_string())); - - // Print the version information and exit if the --version flag was passed. - if args.version { - println!("{}", &util::get_version_string()?); - return Ok(()); - } - - let session_token: String = check_session_token(&args)?; - if !session_token.is_empty() { - let folders = bwutil::exec_folder_search(&session_token, &args.folder)?; - debug_println!("Found {} folder(s) named `{}`", folders.len().to_string().cyan(), args.folder.cyan()); - - // Retrieve items from each folder since there may be multiple folders with the same name. - // Pre-filter the items by checking for the presence of custom fields AND at least one attachment. - for folder in folders { - let folder_items: Vec = bwutil::exec_list_folder_items(&session_token, &folder.id)?; - let folder_items: Vec<&BitwardenItem> = folder_items - .iter() - .filter(|&item| item.fields.is_some() && item.attachments.is_some()) - .collect(); - - debug_println!("Found {} item(s) in folder `{}` id({}) with at least one custom field and attachment", folder_items.len().to_string().cyan(), folder.name.cyan(), folder.id.cyan()); - - for item in folder_items { - // In order for this to be considered a valid SSH key item, this item needs to have the following fields: - // - private - (required) this is the filename of the attachment containing the SSH key. - // The field name is not case sensitive in order to be more user-friendly. - // - passphrase - (optional) this is the passphrase (if any) for the SSH key. - // The field name is not case sensitive in order to be more user-friendly. - // - attachment (required) an attachment with the name specified in `private`. - if let Some(fields) = &item.fields { - let key_filename: String = fields - .iter() - .find(|field| field.name.to_lowercase().eq(&args.key.to_lowercase()) && field.value.is_some()) - .map(|field| field.value.as_ref().unwrap().clone()) - .unwrap_or_default(); - - if !&key_filename.is_empty() { - let key_passphrase: String = fields - .iter() - .find(|field| field.name.to_lowercase().eq(&args.passphrase.to_lowercase()) && field.value.is_some()) - .map(|field| field.value.as_ref().unwrap().clone()) - .unwrap_or_default(); - - if let Some(attachments) = &item.attachments { - attachments - .iter() - .filter(|attachment| attachment.file_name.eq(&key_filename)) - .for_each(|attachment| register_key(&item, &attachment.id, &key_passphrase, &session_token).unwrap()); - } - } - } - } - } - } - - Ok(()) + env::set_var(BW_SSH_DEBUG_ENV_KEY, OsString::from(args.debug.to_string())); + + // Print the version information and exit if the --version flag was passed. + if args.version { + println!("{}", &util::get_version_string()?); + return Ok(()); + } + + let session_token: String = check_session_token(&args)?; + if !session_token.is_empty() { + let folders = bwutil::exec_folder_search(&session_token, &args.folder)?; + debug_println!("Found {} folder(s) named `{}`", folders.len().to_string().cyan(), args.folder.cyan()); + + // Retrieve items from each folder since there may be multiple folders with the same name. + // Pre-filter the items by checking for the presence of custom fields AND at least one attachment. + for folder in folders { + let folder_items: Vec = bwutil::exec_list_folder_items(&session_token, &folder.id)?; + let folder_items: Vec<&BitwardenItem> = folder_items + .iter() + .filter(|&item| item.fields.is_some() && item.attachments.is_some()) + .collect(); + + debug_println!("Found {} item(s) in folder `{}` id({}) with at least one custom field and attachment", folder_items.len().to_string().cyan(), folder.name.cyan(), folder.id.cyan()); + + for item in folder_items { + // In order for this to be considered a valid SSH key item, this item needs to have the following fields: + // - private - (required) this is the filename of the attachment containing the SSH key. + // The field name is not case sensitive in order to be more user-friendly. + // - passphrase - (optional) this is the passphrase (if any) for the SSH key. + // The field name is not case sensitive in order to be more user-friendly. + // - attachment (required) an attachment with the name specified in `private`. + if let Some(fields) = &item.fields { + let key_filename: String = fields + .iter() + .find(|field| field.name.to_lowercase().eq(&args.key.to_lowercase()) && field.value.is_some()) + .map(|field| field.value.as_ref().unwrap().clone()) + .unwrap_or_default(); + + if !&key_filename.is_empty() { + let key_passphrase: String = fields + .iter() + .find(|field| field.name.to_lowercase().eq(&args.passphrase.to_lowercase()) && field.value.is_some()) + .map(|field| field.value.as_ref().unwrap().clone()) + .unwrap_or_default(); + + if let Some(attachments) = &item.attachments { + attachments + .iter() + .filter(|attachment| attachment.file_name.eq(&key_filename)) + .for_each(|attachment| register_key(&item, &attachment.id, &key_passphrase, &session_token).unwrap()); + } + } + } + } + } + } + + Ok(()) } /// Gets a session token from the active Bitwarden session. The checking process is as follows: @@ -115,75 +115,74 @@ fn main() -> Result<()> { /// 4. Login or unlock based on login status and use that. /// An invalid `BW_SESSION` or `--session` will prompt a login. fn check_session_token(args: &util::Cli) -> Result { - info_println!("Getting Bitwarden session..."); - let mut session_token: String = String::new(); - // Get session flag from the user - if args.session.trim().is_empty() { - // No session flag, check for an environment variable - let env_key = env::var(SESSION_ENV_KEY); - match env_key { - Ok(key) => { - info_println!("{} is set. Reusing existing session.", SESSION_ENV_KEY); - // We found it, set our session key - session_token.push_str(&key); - }, - Err(_) => { - // We don't have a token to reuse, so get it from Bitwarden - warn_println!("{} is not set. Attempting to login.", SESSION_ENV_KEY); - let token: &String = &bwutil::get_session_token()?; - - if !token.is_empty() { - info_println!("Successfully unlocked. To re-use this session, run:"); - info_println!("export {}=\"{}\"", SESSION_ENV_KEY, token); - } - session_token.push_str(token) - } - } - } - else { - // Session flag is already defined, pass it on - session_token.push_str(&args.session); - } - - Ok(session_token) + info_println!("Getting Bitwarden session..."); + let mut session_token: String = String::new(); + // Get session flag from the user + if args.session.trim().is_empty() { + // No session flag, check for an environment variable + let env_key = env::var(SESSION_ENV_KEY); + match env_key { + Ok(key) => { + info_println!("{} is set. Reusing existing session.", SESSION_ENV_KEY); + // We found it, set our session key + session_token.push_str(&key); + }, + Err(_) => { + // We don't have a token to reuse, so get it from Bitwarden + warn_println!("{} is not set. Attempting to login.", SESSION_ENV_KEY); + let token: &String = &bwutil::get_session_token()?; + + if !token.is_empty() { + info_println!("Successfully unlocked. To re-use this session, run:"); + info_println!("export {}=\"{}\"", SESSION_ENV_KEY, token); + } + session_token.push_str(token) + } + } + } + else { + // Session flag is already defined, pass it on + session_token.push_str(&args.session); + } + + Ok(session_token) } /// Registers a key with `ssh-agent`. /// First retrieves the attachment from `bitwarden-cli` and then passes it to `ssh-agent`. fn register_key(item: &BitwardenItem, attachment_id: &str, passphrase: &str, session_token: &str) -> Result<()> { - debug_println!("Item `{}` id({}) meets all requirements. Adding to `{}`", item.name.cyan(), item.id.to_string().cyan(), "ssh-agent".cyan()); + debug_println!("Item `{}` id({}) meets all requirements. Adding to `{}`", item.name.cyan(), item.id.to_string().cyan(), "ssh-agent".cyan()); let bw_command = Command::new("bw") - .arg("get") - .arg("attachment") - .arg(attachment_id) - .arg("--itemid") - .arg(&item.id) - .arg("--raw") - .arg("--session") - .arg(session_token) + .arg("get") + .arg("attachment") + .arg(attachment_id) + .arg("--itemid") + .arg(&item.id) + .arg("--raw") + .arg("--session") + .arg(session_token) .stdout(Stdio::piped()) .spawn() - .with_context(|| - format!("Error running command:\n `bw get attachment {} --itemid {} --raw --session **REDACTED**`", attachment_id, item.id) - )?; + .with_context(|| + format!("Error running command:\n `bw get attachment {} --itemid {} --raw --session **REDACTED**`", attachment_id, item.id) + )?; - let app_path = env::current_exe()?; - let app_path = app_path.to_string_lossy().to_string(); + let app_path = env::current_exe()?.to_string_lossy().to_string(); let ssh_command = Command::new("ssh-add") - .env("SSH_ASKPASS", &app_path) - .env(BW_FIELD_KEY_PASSPHRASE, passphrase) + .env("SSH_ASKPASS", &app_path) + .env(BW_FIELD_KEY_PASSPHRASE, passphrase) .arg("-") .stdin(Stdio::from(bw_command.stdout.unwrap())) // Pipe through. .spawn() - .with_context(|| - format!("Error running command:\n `SSH_ASKPASS=\"{}\" {}=\"{}\"ssh-add -`", app_path, BW_FIELD_KEY_PASSPHRASE, passphrase) - )?; + .with_context(|| + format!("Error running command:\n `SSH_ASKPASS=\"{}\" {}=\"{}\"ssh-add -`", app_path, BW_FIELD_KEY_PASSPHRASE, passphrase) + )?; let output = ssh_command.wait_with_output().unwrap(); let result = String::from_utf8(output.stdout).unwrap(); - if !result.is_empty() { - println!("{:?}", result); - } + if !result.is_empty() { + println!("{:?}", result); + } Ok(()) } \ No newline at end of file diff --git a/src/util.rs b/src/util.rs index 857abf5..05df314 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,29 +1,40 @@ -use std::env; use anyhow::Result; use gumdrop::Options; +use std::env; /// Represents command-line parameters. #[derive(Debug, Options)] pub struct Cli { - #[options(help = "print help message and exit")] + #[options(help = "print help message and exit")] pub help: bool, - #[options(help = "show debug output")] - pub debug: bool, - #[options(help = "folder name to use to search for SSH keys", default = "ssh-agent")] - pub folder: String, - #[options(help = "custom field name where the private key filename is stored", meta = "NAME", default = "private")] - pub key: String, - #[options(help = "custom field name where the key passphrase is stored", meta = "PASS", default = "passphrase")] - pub passphrase: String, - #[options(help = "session to use to log in to bitwarden-cli")] - pub session: String, - #[options(help = "print version information and exit")] + #[options(help = "show debug output")] + pub debug: bool, + #[options( + help = "folder name to use to search for SSH keys", + default = "ssh-agent" + )] + pub folder: String, + #[options( + help = "custom field name where the private key filename is stored", + meta = "NAME", + default = "private" + )] + pub key: String, + #[options( + help = "custom field name where the key passphrase is stored", + meta = "PASS", + default = "passphrase" + )] + pub passphrase: String, + #[options(help = "session to use to log in to bitwarden-cli")] + pub session: String, + #[options(help = "print version information and exit")] pub version: bool, } /// Retrieves the version information in the form of `PKG_NAME` - `PKG_VERSION`. pub fn get_version_string() -> Result { - let name = env!("CARGO_PKG_NAME"); - let version = env!("CARGO_PKG_VERSION"); - return Ok(format!("{} {}", name, version)) -} \ No newline at end of file + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + return Ok(format!("{} {}", name, version)); +} -- libgit2 1.7.2