Merge branch 'feature/bw-integration' into develop
Diff
Cargo.toml | 4 +-
src/bwutil.rs | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
src/main.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++++----------
src/util.rs | 29 +++++++++++++++-
4 files changed, 244 insertions(+), 20 deletions(-)
@@ -7,4 +7,6 @@ edition = "2021"
[dependencies]
gumdrop = "0.8.1"
anyhow = "1.0.72"
\ No newline at end of file
anyhow = "1.0.72"
serde = { version = "1.0.177", features = ["derive"] }
serde_json = "1.0.104"
\ No newline at end of file
@@ -0,0 +1,117 @@
use std::process::{Command, Output, Stdio};
use serde::Deserialize;
use anyhow::{anyhow, Result, Context};
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BitwardenFolder {
pub id: String,
pub name: String
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BitwardenItem {
pub id: String,
pub name: String,
pub attachments: Option<Vec<BitwardenAttachment>>,
pub fields: Option<Vec<BitwardenFieldItem>>
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BitwardenFieldItem {
pub name: String,
pub value: String
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BitwardenAttachment {
pub id: String,
pub file_name: String
}
pub fn is_logged_in() -> Result<bool> {
let logged_in = Command::new("bw")
.arg("login")
.arg("--check")
.output()?;
Ok(logged_in.stdout.len() > 0)
}
pub fn get_session_token() -> Result<String> {
let mut token = String::new();
let mut operation = String::new();
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)
}
pub fn exec_folder_search(session: &str, folder_name: &str) -> Result<Vec<BitwardenFolder>> {
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."))
}
else {
Ok(serde_json::from_str(&result).with_context(|| "Could not deserialize folder search results.")?)
}
}
pub fn exec_list_folder_items(session: &str, folder_id: &str) -> Result<Vec<BitwardenItem>> {
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."))
}
else {
Ok(serde_json::from_str(&result).with_context(|| "Could not deserialize item search results.")?)
}
}
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;
}
@@ -1,24 +1,100 @@
use std::env;
use gumdrop::Options;
use anyhow::Result;
#[derive(Debug, Options)]
struct Cli {
#[options(help = "print help message and exit")]
help: bool,
#[options(help = "show debug output")]
debug: bool,
#[options(help = "folder name to use to search for SSH keys", default = "ssh-agent")]
folder: String,
#[options(help = "custom field name where the private key is stored", meta = "PRIVATE_KEY", default = "private-key")]
key: String,
#[options(help = "custom field name where the key passphrase is stored", meta = "PASS", default = "passphrase")]
passphrase: String,
#[options(help = "session to use to log in to bitwarden-cli")]
session: Option<String>,
#[options(help = "print version and exit")]
version: bool,
mod util;
mod bwutil;
const SESSION_ENV_KEY: &str = "BW_SESSION";
const BW_FIELD_KEY_FILENAME: &str = "private";
const BW_FIELD_KEY_PASSPHRASE: &str = "passphrase";
fn main() -> Result<()> {
let args: util::Cli = util::Cli::parse_args_default_or_exit();
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)?;
for folder in folders {
let folder_items = bwutil::exec_list_folder_items(&session_token, &folder.id)?;
for item in folder_items {
if let Some(fields) = &item.fields {
let mut key_filename = String::new();
let mut key_passphrase = String::new();
for field in fields {
if field.name.to_lowercase().eq(&String::from(BW_FIELD_KEY_FILENAME).to_lowercase()) {
key_filename.push_str(&field.value);
}
if field.name.to_lowercase().eq(&String::from(BW_FIELD_KEY_PASSPHRASE).to_lowercase()) {
key_passphrase.push_str(&field.value);
}
}
if let Some(attachments) = &item.attachments {
for attachment in attachments {
if attachment.file_name.eq(&key_filename) {
println!("{:#?}", item);
}
}
}
}
}
}
}
Ok(())
}
fn main() {
let args: Cli = Cli::parse_args_default_or_exit();
println!("{:#?}", args);
fn check_session_token(args: &util::Cli) -> Result<String> {
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) => {
println!("{} is set. Reusing existing session.", SESSION_ENV_KEY);
session_token.push_str(&key);
},
Err(_) => {
println!("{} is not set. Attempting to login.", SESSION_ENV_KEY);
let token: &String = &bwutil::get_session_token()?;
if !token.is_empty() {
println!("Successfully unlocked. To re-use this session, run:");
println!("export {}=\"{}\"", SESSION_ENV_KEY, token);
}
session_token.push_str(token)
}
}
}
else {
session_token.push_str(&args.session);
}
Ok(session_token)
}
@@ -0,0 +1,29 @@
use std::env;
use anyhow::Result;
use gumdrop::Options;
#[derive(Debug, Options)]
pub struct Cli {
#[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 is stored", meta = "PRIVATE_KEY", default = "private-key")]
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 and exit")]
pub version: bool,
}
pub fn get_version_string() -> Result<String> {
let name = env!("CARGO_PKG_NAME");
let version = env!("CARGO_PKG_VERSION");
return Ok(format!("{} {}", name, version))
}
\ No newline at end of file