From dbdd8a78951f8a44630abcceb7b13d0d823ee074 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 3 May 2020 04:07:03 -0400 Subject: allow storing password databases from different servers this should allow easy switching of servers by just `rbw config set base_url` --- CHANGELOG.md | 6 ++++ Cargo.lock | 1 + Cargo.toml | 1 + src/bin/rbw-agent/actions.rs | 56 ++++++++++++++++++--------------- src/bin/rbw/commands.rs | 73 ++++++++++++++++++++++++-------------------- src/config.rs | 6 ++++ src/db.rs | 25 +++++++-------- src/dirs.rs | 9 ++++-- 8 files changed, 105 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d41cbab..d560034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Added + +* Multi-server support - you can now switch between multiple different + bitwarden servers with `rbw config set base_url` without needing to + redownload the password database each time. + ### Fixed * `rbw` is now able to decrypt secrets from organizations you are a member of. diff --git a/Cargo.lock b/Cargo.lock index 574145b..afecb28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1031,6 +1031,7 @@ dependencies = [ "log", "nix", "openssl", + "percent-encoding", "rand", "region", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index b734472..0539a7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ humantime = "1.3" log = "0.4" nix = "0.17" openssl = "0.10" +percent-encoding = "2.0" rand = "0.7" region = "2.1" reqwest = { version = "0.10", features = ["blocking", "json"] } diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs index 3ca6d51..4e76d4b 100644 --- a/src/bin/rbw-agent/actions.rs +++ b/src/bin/rbw-agent/actions.rs @@ -5,12 +5,7 @@ pub async fn login( state: std::sync::Arc>, tty: Option<&str>, ) -> anyhow::Result<()> { - let email = config_email() - .await - .context("failed to read email from config")?; - let mut db = rbw::db::Db::load_async(&email) - .await - .unwrap_or_else(|_| rbw::db::Db::new()); + let mut db = load_db().await.unwrap_or_else(|_| rbw::db::Db::new()); if db.needs_login() { let url_str = config_base_url() @@ -27,6 +22,8 @@ pub async fn login( )); }; + let email = config_email().await?; + for i in 1_u8..=3 { let err = if i > 1 { Some(format!("Incorrect password (attempt {}/3)", i)) @@ -56,9 +53,7 @@ pub async fn login( db.refresh_token = Some(refresh_token); db.iterations = Some(iterations); db.protected_key = Some(protected_key); - db.save_async(&email) - .await - .context("failed to save local database")?; + save_db(&db).await?; break; } @@ -93,13 +88,7 @@ pub async fn unlock( tty: Option<&str>, ) -> anyhow::Result<()> { if state.read().await.needs_unlock() { - let email = config_email() - .await - .context("failed to read email from config")?; - - let db = rbw::db::Db::load_async(&email) - .await - .context("failed to load local database")?; + let db = load_db().await?; let iterations = if let Some(iterations) = db.iterations { iterations @@ -124,6 +113,8 @@ pub async fn unlock( )); }; + let email = config_email().await?; + for i in 1u8..=3 { let err = if i > 1 { Some(format!("Incorrect password (attempt {}/3)", i)) @@ -184,12 +175,7 @@ pub async fn lock( } pub async fn sync(sock: &mut crate::sock::Sock) -> anyhow::Result<()> { - let email = config_email() - .await - .context("failed to read email from config")?; - let mut db = rbw::db::Db::load_async(&email) - .await - .context("failed to load local database")?; + let mut db = load_db().await?; let access_token = if let Some(access_token) = &db.access_token { access_token.clone() @@ -214,9 +200,7 @@ pub async fn sync(sock: &mut crate::sock::Sock) -> anyhow::Result<()> { db.protected_private_key = Some(protected_private_key); db.protected_org_keys = protected_org_keys; db.entries = entries; - db.save_async(&email) - .await - .context("failed to save database")?; + save_db(&db).await?; respond_ack(sock).await?; @@ -327,6 +311,28 @@ async fn config_email() -> anyhow::Result { } } +async fn load_db() -> anyhow::Result { + let config = rbw::config::Config::load_async().await?; + if let Some(email) = &config.email { + rbw::db::Db::load_async(&config.server_name(), &email) + .await + .context("failed to load password database") + } else { + Err(anyhow::anyhow!("failed to find email address in config")) + } +} + +async fn save_db(db: &rbw::db::Db) -> anyhow::Result<()> { + let config = rbw::config::Config::load_async().await?; + if let Some(email) = &config.email { + db.save_async(&config.server_name(), &email) + .await + .context("failed to save password database") + } else { + Err(anyhow::anyhow!("failed to find email address in config")) + } +} + async fn config_base_url() -> anyhow::Result { let config = rbw::config::Config::load_async() .await diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 51d0453..a6e98c7 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -107,10 +107,7 @@ pub fn list(fields: &[&str]) -> anyhow::Result<()> { unlock()?; - let email = config_email()?; - let db = rbw::db::Db::load(&email) - .context("failed to load password database")?; - + let db = load_db()?; let mut ciphers: Vec = db .entries .iter() @@ -146,9 +143,7 @@ pub fn list(fields: &[&str]) -> anyhow::Result<()> { pub fn get(name: &str, user: Option<&str>, full: bool) -> anyhow::Result<()> { unlock()?; - let email = config_email()?; - let db = rbw::db::Db::load(&email) - .context("failed to load password database")?; + let db = load_db()?; let desc = format!( "{}{}", @@ -182,8 +177,7 @@ pub fn add( ) -> anyhow::Result<()> { unlock()?; - let email = config_email()?; - let mut db = rbw::db::Db::load(&email)?; + let mut db = load_db()?; // unwrap is safe here because the call to unlock above is guaranteed to // populate these or error let mut access_token = db.access_token.as_ref().unwrap().clone(); @@ -216,7 +210,7 @@ pub fn add( if let Some(new_access_token) = new_access_token { access_token = new_access_token.clone(); db.access_token = Some(new_access_token); - db.save(&email).context("failed to save database")?; + save_db(&db)?; } let folders: Vec<(String, String)> = folders @@ -239,7 +233,7 @@ pub fn add( if let Some(new_access_token) = new_access_token { access_token = new_access_token.clone(); db.access_token = Some(new_access_token); - db.save(&email).context("failed to save database")?; + save_db(&db)?; } folder_id = Some(id); } @@ -256,7 +250,7 @@ pub fn add( folder_id.as_deref(), )? { db.access_token = Some(access_token); - db.save(&email).context("failed to save database")?; + save_db(&db)?; } crate::actions::sync()?; @@ -278,8 +272,7 @@ pub fn generate( if let Some(name) = name { unlock()?; - let email = config_email()?; - let mut db = rbw::db::Db::load(&email)?; + let mut db = load_db()?; // unwrap is safe here because the call to unlock above is guaranteed // to populate these or error let mut access_token = db.access_token.as_ref().unwrap().clone(); @@ -302,7 +295,7 @@ pub fn generate( if let Some(new_access_token) = new_access_token { access_token = new_access_token.clone(); db.access_token = Some(new_access_token); - db.save(&email).context("failed to save database")?; + save_db(&db)?; } let folders: Vec<(String, String)> = folders @@ -327,7 +320,7 @@ pub fn generate( if let Some(new_access_token) = new_access_token { access_token = new_access_token.clone(); db.access_token = Some(new_access_token); - db.save(&email).context("failed to save database")?; + save_db(&db)?; } folder_id = Some(id); } @@ -344,7 +337,7 @@ pub fn generate( folder_id.as_deref(), )? { db.access_token = Some(access_token); - db.save(&email).context("failed to save database")?; + save_db(&db)?; } crate::actions::sync()?; @@ -356,9 +349,7 @@ pub fn generate( pub fn edit(name: &str, username: Option<&str>) -> anyhow::Result<()> { unlock()?; - let email = config_email()?; - let mut db = rbw::db::Db::load(&email) - .context("failed to load password database")?; + let mut db = load_db()?; let access_token = db.access_token.as_ref().unwrap(); let refresh_token = db.refresh_token.as_ref().unwrap(); @@ -409,7 +400,7 @@ pub fn edit(name: &str, username: Option<&str>) -> anyhow::Result<()> { &history, )? { db.access_token = Some(access_token); - db.save(&email).context("failed to save database")?; + save_db(&db)?; } crate::actions::sync()?; @@ -419,9 +410,7 @@ pub fn edit(name: &str, username: Option<&str>) -> anyhow::Result<()> { pub fn remove(name: &str, username: Option<&str>) -> anyhow::Result<()> { unlock()?; - let email = config_email()?; - let mut db = rbw::db::Db::load(&email) - .context("failed to load password database")?; + let mut db = load_db()?; let access_token = db.access_token.as_ref().unwrap(); let refresh_token = db.refresh_token.as_ref().unwrap(); @@ -440,7 +429,7 @@ pub fn remove(name: &str, username: Option<&str>) -> anyhow::Result<()> { rbw::actions::remove(&access_token, &refresh_token, &entry.id)? { db.access_token = Some(access_token); - db.save(&email).context("failed to save database")?; + save_db(&db)?; } crate::actions::sync()?; @@ -451,9 +440,7 @@ pub fn remove(name: &str, username: Option<&str>) -> anyhow::Result<()> { pub fn history(name: &str, username: Option<&str>) -> anyhow::Result<()> { unlock()?; - let email = config_email()?; - let db = rbw::db::Db::load(&email) - .context("failed to load password database")?; + let db = load_db()?; let desc = format!( "{}{}", @@ -482,8 +469,7 @@ pub fn lock() -> anyhow::Result<()> { pub fn purge() -> anyhow::Result<()> { stop_agent()?; - let email = config_email()?; - rbw::db::Db::remove(&email).context("failed to remove database")?; + remove_db()?; Ok(()) } @@ -755,10 +741,31 @@ fn parse_editor(contents: &str) -> (Option, Option) { (password, notes) } -fn config_email() -> anyhow::Result { +fn load_db() -> anyhow::Result { + let config = rbw::config::Config::load()?; + if let Some(email) = &config.email { + rbw::db::Db::load(&config.server_name(), &email) + .context("failed to load password database") + } else { + Err(anyhow::anyhow!("failed to find email address in config")) + } +} + +fn save_db(db: &rbw::db::Db) -> anyhow::Result<()> { + let config = rbw::config::Config::load()?; + if let Some(email) = &config.email { + db.save(&config.server_name(), &email) + .context("failed to save password database") + } else { + Err(anyhow::anyhow!("failed to find email address in config")) + } +} + +fn remove_db() -> anyhow::Result<()> { let config = rbw::config::Config::load()?; - if let Some(email) = config.email { - Ok(email) + if let Some(email) = &config.email { + rbw::db::Db::remove(&config.server_name(), &email) + .context("failed to remove password database") } else { Err(anyhow::anyhow!("failed to find email address in config")) } diff --git a/src/config.rs b/src/config.rs index 5b3cfd3..7bbb928 100644 --- a/src/config.rs +++ b/src/config.rs @@ -77,4 +77,10 @@ impl Config { ) }) } + + pub fn server_name(&self) -> String { + self.base_url + .clone() + .unwrap_or_else(|| "default".to_string()) + } } diff --git a/src/db.rs b/src/db.rs index b61f1bd..e5c7257 100644 --- a/src/db.rs +++ b/src/db.rs @@ -43,8 +43,8 @@ impl Db { Self::default() } - pub fn load(email: &str) -> Result { - let mut fh = std::fs::File::open(crate::dirs::db_file(email)) + pub fn load(server: &str, email: &str) -> Result { + let mut fh = std::fs::File::open(crate::dirs::db_file(server, email)) .context(crate::error::LoadDb)?; let mut json = String::new(); fh.read_to_string(&mut json).context(crate::error::LoadDb)?; @@ -53,10 +53,11 @@ impl Db { Ok(slf) } - pub async fn load_async(email: &str) -> Result { - let mut fh = tokio::fs::File::open(crate::dirs::db_file(email)) - .await - .context(crate::error::LoadDbAsync)?; + pub async fn load_async(server: &str, email: &str) -> Result { + let mut fh = + tokio::fs::File::open(crate::dirs::db_file(server, email)) + .await + .context(crate::error::LoadDbAsync)?; let mut json = String::new(); fh.read_to_string(&mut json) .await @@ -67,8 +68,8 @@ impl Db { } // XXX need to make this atomic - pub fn save(&self, email: &str) -> Result<()> { - let filename = crate::dirs::db_file(email); + pub fn save(&self, server: &str, email: &str) -> Result<()> { + let filename = crate::dirs::db_file(server, email); // unwrap is safe here because Self::filename is explicitly // constructed as a filename in a directory std::fs::create_dir_all(filename.parent().unwrap()) @@ -85,8 +86,8 @@ impl Db { } // XXX need to make this atomic - pub async fn save_async(&self, email: &str) -> Result<()> { - let filename = crate::dirs::db_file(email); + pub async fn save_async(&self, server: &str, email: &str) -> Result<()> { + let filename = crate::dirs::db_file(server, email); // unwrap is safe here because Self::filename is explicitly // constructed as a filename in a directory tokio::fs::create_dir_all(filename.parent().unwrap()) @@ -105,8 +106,8 @@ impl Db { Ok(()) } - pub fn remove(email: &str) -> Result<()> { - let filename = crate::dirs::db_file(email); + pub fn remove(server: &str, email: &str) -> Result<()> { + let filename = crate::dirs::db_file(server, email); let res = std::fs::remove_file(filename); if let Err(e) = &res { if e.kind() == std::io::ErrorKind::NotFound { diff --git a/src/dirs.rs b/src/dirs.rs index 5dd2c35..53a433e 100644 --- a/src/dirs.rs +++ b/src/dirs.rs @@ -17,8 +17,13 @@ pub fn config_file() -> std::path::PathBuf { config_dir().join("config.json") } -pub fn db_file(email: &str) -> std::path::PathBuf { - cache_dir().join(format!("{}.json", email)) +const INVALID_PATH: &percent_encoding::AsciiSet = + &percent_encoding::CONTROLS.add(b'/').add(b'%').add(b':'); +pub fn db_file(server: &str, email: &str) -> std::path::PathBuf { + let server = + percent_encoding::percent_encode(server.as_bytes(), INVALID_PATH) + .to_string(); + cache_dir().join(format!("{}:{}.json", server, email)) } pub fn pid_file() -> std::path::PathBuf { -- cgit v1.2.3-54-g00ecf