use anyhow::{anyhow, Context, Result}; use colored::*; use serde::Deserialize; 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, } /// 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>, } /// 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, } /// 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, } /// 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()?; 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") } 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)?); } 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.")?) } } // 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.")?) } } /// 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() { Err(err) => panic!("Error spawning: {}", err), Ok(process) => process, }; let output = cli_command.wait_with_output().unwrap(); return output; }