6 Commits
5 changed files with 211 additions and 128 deletions
+111
View File
@@ -0,0 +1,111 @@
use crate::sh;
use serde::Deserialize;
use std::io::{Error, ErrorKind};
const EXECUTABLE: &str = "bw";
#[derive(Deserialize, Clone)]
pub struct Item {
pub id: String,
pub name: String,
#[serde(rename = "passwordHistory")]
pub password_history: Vec<PasswordHistoryItem>,
#[serde(rename = "revisionDate")]
pub revision_date: String,
#[serde(rename = "creationDate")]
pub creation_date: String,
pub object: String,
#[serde(rename = "type")]
pub type_: u16,
pub notes: Option<String>,
pub favorite: bool,
pub fields: Vec<ItemField>,
pub login: Option<Login>,
}
#[derive(Deserialize, Clone)]
pub struct PasswordHistoryItem {
#[serde(rename = "lastUsedDate")]
pub last_used_date: String,
pub password: String,
}
#[derive(Deserialize, Clone)]
pub struct ItemField {
pub name: String,
pub value: String,
#[serde(rename = "type")]
pub type_: u16,
}
#[derive(Deserialize, Clone)]
pub struct Login {
pub uris: Vec<LoginUri>,
pub username: Option<String>,
pub password: Option<String>,
pub totp: Option<String>,
#[serde(rename = "passwordRevisionDate")]
pub password_revision_date: Option<String>,
#[serde(rename = "fido2Credentials")]
pub fido2_credentials: Vec<Fido2Credential>,
}
#[derive(Deserialize, Clone)]
pub struct LoginUri {
pub uri: String,
}
#[derive(Deserialize, Clone)]
pub struct Fido2Credential {
#[serde(rename = "credentialId")]
pub credential_id: String,
#[serde(rename = "keyType")]
pub key_type: String,
#[serde(rename = "keyAlgorithm")]
pub key_algorithm: String,
#[serde(rename = "keyCurve")]
pub key_curve: String,
#[serde(rename = "keyValue")]
pub key_value: String,
#[serde(rename = "rpId")]
pub rp_id: String,
#[serde(rename = "userHandle")]
pub user_handle: String,
#[serde(rename = "userName")]
pub user_name: String,
pub counter: String,
#[serde(rename = "rpName")]
pub rp_name: String,
#[serde(rename = "userDisplayName")]
pub user_display_name: Option<String>,
pub discoverable: String,
#[serde(rename = "creationDate")]
pub creation_date: String,
}
pub fn check_bw() -> Result<(), Error> {
let which = sh::sh(format!("which {}", EXECUTABLE));
if which.is_empty() {
return Err(Error::new(
ErrorKind::Other,
format!("{} is not installed", EXECUTABLE),
));
}
Ok(())
}
pub fn get_items() -> Result<Vec<Item>, Error> {
let items_raw = sh::sh(format!("{} list items --raw", EXECUTABLE));
match serde_json::from_str::<Vec<Item>>(items_raw.as_str()) {
Ok(val) => Ok(val
.iter()
.filter(|x| x.type_ == 1)
.cloned()
.collect::<Vec<Item>>()),
Err(e) => Err(Error::new(
ErrorKind::Other,
format!("Couldn't get items: {e}"),
)),
}
}
+69 -35
View File
@@ -1,11 +1,11 @@
mod bw;
mod pass; mod pass;
mod rbw;
mod sh; mod sh;
use clap::Parser;
use std::env; use std::env;
use std::io; use std::io;
use std::io::{Error, ErrorKind, Write}; use std::io::{Error, ErrorKind, Write};
use clap::Parser;
const ENV_VAR_DEFAULT_VAULT: &str = "PASS_VAULT"; const ENV_VAR_DEFAULT_VAULT: &str = "PASS_VAULT";
@@ -13,21 +13,27 @@ const ENV_VAR_DEFAULT_VAULT: &str = "PASS_VAULT";
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
struct Cli {} struct Cli {}
fn ask_consent(question: String, line1: String, line2: String) -> Result<bool, Error> { fn ask_consent(
question: String,
line1: String,
line2: String,
line3: String,
) -> Result<bool, Error> {
println!("{}", question); println!("{}", question);
println!("\t{}", line1); println!("\t{}", line1);
println!("\t{}", line2); println!("\t{}", line2);
println!("\t{}", line3);
print!("Proceed? [y/N] "); print!("Proceed? [y/N] ");
io::stdout().flush()?; io::stdout().flush()?;
let mut input = String::new(); let mut input = String::new();
io::stdin().read_line(&mut input)?; io::stdin().read_line(&mut input)?;
return Ok(input.to_lowercase() == "y\n"); Ok(input.to_lowercase() == "y\n")
} }
fn sync() -> Result<(), Error> { fn sync() -> Result<(), Error> {
pass::check_pass()?; pass::check_pass()?;
rbw::check_rbw()?; bw::check_bw()?;
let vaults = pass::get_vaults()?.vaults; let vaults = pass::get_vaults()?.vaults;
if vaults.len() <= 0 { if vaults.len() <= 0 {
@@ -62,21 +68,25 @@ fn sync() -> Result<(), Error> {
let pass_items = pass::get_items(&vault)?; let pass_items = pass::get_items(&vault)?;
let mut pass_logins = pass::get_logins(pass_items); let mut pass_logins = pass::get_logins(pass_items);
let rbw_items = rbw::get_items()?; let mut bw_items = bw::get_items()?;
let mut rbw_logins = rbw::get_logins(rbw_items)?;
println!(); println!();
println!( println!(
"Got {} rbw logins and {} pass logins...", "Got {} bw logins and {} pass logins...",
rbw_logins.len(), bw_items.len(),
pass_logins.len() pass_logins.len()
); );
for rbw_login in rbw_logins.clone().iter() { for bw_item in bw_items.clone().iter() {
if let None = bw_item.login {
continue;
}
let bw_login = bw_item.login.clone().unwrap();
let mut pass_login: Option<&pass::LoginItem> = None; let mut pass_login: Option<&pass::LoginItem> = None;
'inner: for pass_login_iter in pass_logins.iter() { 'inner: for pass_login_iter in pass_logins.iter() {
if rbw_login.name == pass_login_iter.title { if bw_item.name == pass_login_iter.title {
pass_login = Some(pass_login_iter); pass_login = Some(pass_login_iter);
break 'inner; break 'inner;
} }
@@ -87,39 +97,55 @@ fn sync() -> Result<(), Error> {
} }
let pass_login = pass_login.unwrap(); let pass_login = pass_login.unwrap();
let rbw_password = rbw_login.password.clone(); let bw_password = bw_login.password.clone().unwrap_or("".to_string());
let pass_password = pass_login.password.clone(); let pass_password = pass_login.password.clone();
let rbw_user = rbw_login.user.clone(); let bw_user = bw_login.username.clone().unwrap_or("".to_string());
let pass_user = match pass_login.username.is_empty() { let pass_user = match pass_login.username.is_empty() {
true => pass_login.email.clone(), true => pass_login.email.clone(),
false => pass_login.username.clone(), false => pass_login.username.clone(),
}; };
let pass_user_is_actually_email = pass_user == pass_login.email; let pass_user_is_actually_email = pass_user == pass_login.email;
if rbw_user != pass_user || rbw_password != pass_password { let bw_notes = bw_item.notes.clone().unwrap_or("".to_string());
let pass_notes = pass_login.notes.clone();
if bw_user != pass_user
|| (bw_password != pass_password && bw_password != "".to_string())
|| bw_notes != pass_notes
{
let consent = ask_consent( let consent = ask_consent(
format!("Attempting to update {}:", rbw_login.name), format!("Attempting to update {}:", bw_item.name),
format!("{} -> {}", pass_user, rbw_user.clone()), format!("{} -> {}", pass_user, bw_user.clone()),
format!("{} -> {}", pass_password, rbw_password.clone()), format!("{} -> {}", pass_password, bw_password.clone()),
format!("{} -> {}", bw_notes, pass_notes),
)?; )?;
if consent { if consent {
let updated_pass_login = pass::LoginItem { let updated_pass_login = pass::LoginItem {
id: pass_login.id.clone(), id: pass_login.id.clone(),
title: pass_login.title.clone(), title: pass_login.title.clone(),
username: rbw_user.clone(), username: bw_user.clone(),
email: rbw_user.clone(), email: bw_user.clone(),
password: rbw_password, password: bw_password,
notes: bw_notes,
urls: Some(bw_login.uris.iter().map(|x| x.uri.clone()).collect()),
totp: pass_login.totp.clone(),
}; };
pass::update(&vault, updated_pass_login, pass_user_is_actually_email); pass::update(&vault, updated_pass_login, pass_user_is_actually_email);
} }
} }
let rbw_index = rbw_logins let bw_totp = bw_login.totp.clone().unwrap_or("".to_string());
.iter() let pass_totp = pass_login.totp.clone().unwrap_or("".to_string());
.position(|x| x.id == rbw_login.id) if bw_totp != "".to_string() && pass_totp == "".to_string() {
.unwrap(); println!(
rbw_logins.remove(rbw_index); "TOTP found for {} but not in pass, you need to manually change it to {}",
bw_item.name, bw_totp
);
}
let bw_index = bw_items.iter().position(|x| x.id == bw_item.id).unwrap();
bw_items.remove(bw_index);
let pass_index = pass_logins let pass_index = pass_logins
.iter() .iter()
@@ -128,22 +154,29 @@ fn sync() -> Result<(), Error> {
pass_logins.remove(pass_index); pass_logins.remove(pass_index);
} }
// TODO: need to delete these
println!(); println!();
println!( println!(
"Remaining {} rbw logins and {} pass logins...", "Remaining {} bw logins and {} pass logins...",
rbw_logins.len(), bw_items.len(),
pass_logins.len() pass_logins.len()
); );
for rbw_login in rbw_logins { for bw_item in bw_items {
let login = bw_item.login.unwrap();
let consent = ask_consent( let consent = ask_consent(
format!("Attempting to create {}:", rbw_login.name), format!("Attempting to create {}:", bw_item.name),
format!("{}", rbw_login.user), format!("{}", login.username.clone().unwrap_or("".to_string())),
format!("{}", rbw_login.password), format!("{}", login.password.clone().unwrap_or("".to_string())),
format!("{}", bw_item.notes.clone().unwrap_or("".to_string())),
)?; )?;
if consent { if consent {
pass::create(&vault, rbw_login.name, rbw_login.user, rbw_login.password); pass::create(
&vault,
bw_item.name,
login.username.unwrap(),
login.password.unwrap_or("".to_string()),
);
} }
} }
@@ -159,17 +192,18 @@ fn sync() -> Result<(), Error> {
} }
), ),
format!("{}", pass_login.password), format!("{}", pass_login.password),
format!("{}", pass_login.notes),
)?; )?;
if consent { if consent {
pass::trash(&vault, pass_login.id); pass::trash(&vault, pass_login.id);
} }
} }
return Ok(()); Ok(())
} }
fn main() -> Result<(), Error> { fn main() -> Result<(), Error> {
Cli::parse(); Cli::parse();
sync()?; sync()?;
return Ok(()); Ok(())
} }
+30 -9
View File
@@ -31,6 +31,7 @@ pub struct Item {
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct ItemContent { pub struct ItemContent {
pub title: String, pub title: String,
pub note: String,
pub content: ItemContentContent, pub content: ItemContentContent,
} }
@@ -46,6 +47,8 @@ pub struct ItemLogin {
pub email: String, pub email: String,
pub username: String, pub username: String,
pub password: String, pub password: String,
pub urls: Vec<String>,
pub totp_uri: Option<String>,
} }
// our own custom structure, a counterpart to rbw::LoginItem // our own custom structure, a counterpart to rbw::LoginItem
@@ -56,6 +59,9 @@ pub struct LoginItem {
pub username: String, pub username: String,
pub email: String, pub email: String,
pub password: String, pub password: String,
pub urls: Option<Vec<String>>,
pub notes: String,
pub totp: Option<String>,
} }
pub fn check_pass() -> Result<(), Error> { pub fn check_pass() -> Result<(), Error> {
@@ -75,18 +81,18 @@ pub fn check_pass() -> Result<(), Error> {
)); ));
} }
return Ok(()); Ok(())
} }
pub fn get_vaults() -> Result<Vaults, Error> { pub fn get_vaults() -> Result<Vaults, Error> {
let vaults_raw = sh::sh(format!("{} vault list --output json", EXECUTABLE)); let vaults_raw = sh::sh(format!("{} vault list --output json", EXECUTABLE));
return match serde_json::from_str(vaults_raw.as_str()) { match serde_json::from_str(vaults_raw.as_str()) {
Ok(val) => Ok(val), Ok(val) => Ok(val),
Err(e) => Err(Error::new( Err(e) => Err(Error::new(
ErrorKind::Other, ErrorKind::Other,
format!("Couldn't get vaults: {e}"), format!("Couldn't get vaults: {e}"),
)), )),
}; }
} }
pub fn get_items(vault: &String) -> Result<Items, Error> { pub fn get_items(vault: &String) -> Result<Items, Error> {
@@ -94,13 +100,13 @@ pub fn get_items(vault: &String) -> Result<Items, Error> {
"{} item list '{}' --output json --show-secrets", "{} item list '{}' --output json --show-secrets",
EXECUTABLE, vault EXECUTABLE, vault
)); ));
return match serde_json::from_str(items_raw.as_str()) { match serde_json::from_str(items_raw.as_str()) {
Ok(val) => Ok(val), Ok(val) => Ok(val),
Err(e) => Err(Error::new( Err(e) => Err(Error::new(
ErrorKind::Other, ErrorKind::Other,
format!("Couldn't get items: {e}"), format!("Couldn't get items: {e}"),
)), )),
}; }
} }
pub fn get_logins(items: Items) -> Vec<LoginItem> { pub fn get_logins(items: Items) -> Vec<LoginItem> {
@@ -124,6 +130,9 @@ pub fn get_logins(items: Items) -> Vec<LoginItem> {
username: login.username, username: login.username,
email: login.email, email: login.email,
password: login.password, password: login.password,
notes: item.content.note.clone(),
urls: Some(login.urls),
totp: login.totp_uri,
} }
}) })
.collect() .collect()
@@ -135,10 +144,22 @@ pub fn update(vault: &String, item: LoginItem, user_is_actually_email: bool) {
} else { } else {
format!("usename={}", item.username) format!("usename={}", item.username)
}; };
let output = sh::sh(format!(
"{} item update --vault-name '{}' --item-id '{}' --field 'password={}' --field '{}'", let maybe_urls = if let Some(urls) = item.urls {
EXECUTABLE, vault, item.id, item.password, user_field_update 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); println!("> {}", output);
} }
-83
View File
@@ -1,83 +0,0 @@
use crate::sh;
use serde::Deserialize;
use std::io::{Error, ErrorKind};
const EXECUTABLE: &str = "rbw";
#[derive(Deserialize, Debug)]
pub struct Item {
pub id: String,
pub name: String,
pub user: Option<String>,
#[serde(rename = "type")]
pub type_: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct LoginItem {
pub id: String,
pub name: String,
pub user: String,
pub password: String,
}
pub fn check_rbw() -> 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 unlocked = sh::sh(format!("{} unlocked", EXECUTABLE));
if !unlocked.is_empty() {
return Err(Error::new(
ErrorKind::Other,
format!("{} database not unlocked", EXECUTABLE),
));
}
return Ok(());
}
pub fn get_items() -> Result<Vec<Item>, Error> {
let items_raw = sh::sh(format!("{} list --raw", EXECUTABLE));
return 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: Vec<Item>) -> Result<Vec<LoginItem>, Error> {
let items = items
.iter()
.filter(|item| item.type_.to_lowercase() == "login")
.collect::<Vec<&Item>>();
let mut login_items = Vec::<LoginItem>::new();
for item in items {
let password = sh::sh(format!("{} get '{}'", EXECUTABLE, item.id));
if let None = item.user {
println!("WARNING: user not found for {}", item.name);
}
login_items.push(LoginItem {
id: item.id.clone(),
name: item.name.clone(),
user: match item.user.clone() {
Some(val) => val,
None => "".to_string(),
},
password: match password.strip_suffix("\n") {
Some(s) => s.to_string(),
None => password,
},
});
}
return Ok(login_items);
}
+1 -1
View File
@@ -9,5 +9,5 @@ pub fn sh(command: impl Into<String>) -> String {
.output() .output()
.expect("Failed to execute command"); .expect("Failed to execute command");
return String::from_utf8(output.stdout).expect("Invalid UTF-8 sequence"); String::from_utf8(output.stdout).expect("Invalid UTF-8 sequence")
} }