use crate::sh; use regex::Regex; use serde::Deserialize; use std::io::{Error, ErrorKind}; const EXECUTABLE: &str = "pass-cli"; const EMAIL_REGEX: &str = r"^[\w\-\.]+@([\w-]+\.)+[\w-]{2,}$"; #[derive(Deserialize, Debug)] pub struct Vaults { pub vaults: Vec, } #[derive(Deserialize, Debug)] pub struct Vault { pub name: String, } #[derive(Deserialize, Debug)] pub struct Items { pub items: Vec, } #[derive(Deserialize, Debug)] pub struct Item { pub id: String, pub content: ItemContent, pub state: String, } #[derive(Deserialize, Debug, Clone)] pub struct ItemContent { pub title: String, pub note: String, pub content: ItemContentContent, } #[derive(Deserialize, Debug, Clone)] pub struct ItemContentContent { #[serde(rename = "Login")] pub login: Option, } // pass-cli provided structure, this is what it's called inside the JSON data #[derive(Deserialize, Debug, Clone)] pub struct ItemLogin { pub email: String, pub username: String, pub password: String, pub urls: Vec, pub totp_uri: Option, } // our own custom structure, a counterpart to rbw::LoginItem #[derive(Debug, Clone)] pub struct LoginItem { pub id: String, pub title: String, pub username: String, pub email: String, pub password: String, pub urls: Option>, pub notes: String, pub totp: Option, } pub fn check_pass() -> Result<(), Error> { let which = sh::sh(format!("which {}", EXECUTABLE)); if which.is_empty() { return Err(Error::new( ErrorKind::Other, format!("{} is not installed", EXECUTABLE), )); } let test = sh::sh(format!("{} test", EXECUTABLE)); if test != "Connection successful\n" { return Err(Error::new( ErrorKind::Other, format!("{} test failed", EXECUTABLE), )); } Ok(()) } pub fn get_vaults() -> Result { let vaults_raw = sh::sh(format!("{} vault list --output json", EXECUTABLE)); match serde_json::from_str(vaults_raw.as_str()) { Ok(val) => Ok(val), Err(e) => Err(Error::new( ErrorKind::Other, format!("Couldn't get vaults: {e}"), )), } } pub fn get_items(vault: &String) -> Result { let items_raw = sh::sh(format!( "{} item list '{}' --output json --show-secrets", EXECUTABLE, vault )); match serde_json::from_str(items_raw.as_str()) { Ok(val) => Ok(val), Err(e) => Err(Error::new( ErrorKind::Other, format!("Couldn't get items: {e}"), )), } } pub fn get_logins(items: Items) -> Vec { items .items .iter() .filter(|item| { if item.state.to_lowercase() != "active" { false } else if let None = item.content.content.login { false } else { true } }) .map(|item| { let login = item.content.content.login.clone().unwrap(); LoginItem { id: item.id.clone(), title: item.content.title.clone(), username: login.username, email: login.email, password: login.password, notes: item.content.note.clone(), urls: Some(login.urls), totp: login.totp_uri, } }) .collect() } pub fn update(vault: &String, item: LoginItem, user_is_actually_email: bool) { let user_field_update = if user_is_actually_email { format!("email={}", item.email) } else { format!("usename={}", item.username) }; let maybe_urls = if let Some(urls) = item.urls { let mut it = "".to_string(); for url in urls { it = format!("{} --field 'urls={}'", it, url); } it } else { "".to_string() }; let cmd = format!( "{} item update --vault-name '{}' --item-id '{}' --field 'password={}' --field '{}' --field 'note={}' {}", EXECUTABLE, vault, item.id, item.password, user_field_update, item.notes, maybe_urls ); let output = sh::sh(cmd.clone()); println!("> {}", output); } pub fn create(vault: &String, title: String, user: String, password: String) { let re = Regex::new(EMAIL_REGEX).expect("Couldn't parse regex"); let is_email = re.is_match(&user); let user_arg = if is_email { format!("--email '{}'", user) } else { format!("--username '{}'", user) }; let output = sh::sh(format!( "{} item create login --vault-name '{}' --title '{}' {} --password '{}'", EXECUTABLE, vault, title, user_arg, password )); println!("> {}", output); } pub fn trash(vault: &String, id: String) { let output = sh::sh(format!( "{} item trash --vault-name '{}' --item-id '{}'", EXECUTABLE, vault, id )); println!("> {}", output); }