use std::{env, process::{Command, Stdio}, ffi::OsString};
use std::str::FromStr;
use gumdrop::Options;
use anyhow::{Result, Context};
use colored::*;
use crate::bwutil::BitwardenItem;
mod util;
mod bwutil;
const SESSION_ENV_KEY: &str = "BW_SESSION";
const BW_SSH_DEBUG_ENV_KEY: &str = "BW_SSH_DEBUG";
const BW_FIELD_KEY_PASSPHRASE: &str = "BW_KEY_PASSPHRASE";
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());
}
}
}
}
macro_rules! warn_println {
($($arg:tt)*) => {
println!("{}",format!($($arg)*).yellow());
}
}
macro_rules! info_println {
($($arg:tt)*) => {
println!("{}",format!($($arg)*).green());
}
}
fn main() -> Result<()> {
let unlock_passphrase = env::var(BW_FIELD_KEY_PASSPHRASE);
if unlock_passphrase.is_ok() {
println!("{}", unlock_passphrase.unwrap());
return Ok(())
}
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()));
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());
for folder in folders {
let folder_items: Vec<BitwardenItem> = 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 {
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(())
}
fn check_session_token(args: &util::Cli) -> Result<String> {
info_println!("Getting Bitwarden session...");
let mut session_token: String = String::new();
if args.session.trim().is_empty() {
let env_key = env::var(SESSION_ENV_KEY);
match env_key {
Ok(key) => {
info_println!("{} is set. Reusing existing session.", SESSION_ENV_KEY);
session_token.push_str(&key);
},
Err(_) => {
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_token.push_str(&args.session);
}
Ok(session_token)
}
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());
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)
.stdout(Stdio::piped())
.spawn()
.with_context(||
format!("Error running command:\n `bw get attachment {} --itemid {} --raw --session **REDACTED**`", attachment_id, item.id)
)?;
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)
.arg("-")
.stdin(Stdio::from(bw_command.stdout.unwrap()))
.spawn()
.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);
}
Ok(())
}