index : bitwarden-ssh-agent.git

ascending towards madness

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<Vec<BitwardenAttachment>>,
    pub fields: Option<Vec<BitwardenFieldItem>>,
}

/// 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<String>,
}

/// 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<bool> {
    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<String> {
    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<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. 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<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. 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;
}