aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2020-05-03 04:07:03 -0400
committerJesse Luehrs <doy@tozt.net>2020-05-03 04:09:34 -0400
commitdbdd8a78951f8a44630abcceb7b13d0d823ee074 (patch)
tree64c69f16279876c71ba22a55ef6dd255289aaaef
parent6ebf7d55e4c553870306a70092cfb677c17429b9 (diff)
downloadrbw-dbdd8a78951f8a44630abcceb7b13d0d823ee074.tar.gz
rbw-dbdd8a78951f8a44630abcceb7b13d0d823ee074.zip
allow storing password databases from different servers
this should allow easy switching of servers by just `rbw config set base_url`
-rw-r--r--CHANGELOG.md6
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--src/bin/rbw-agent/actions.rs56
-rw-r--r--src/bin/rbw/commands.rs73
-rw-r--r--src/config.rs6
-rw-r--r--src/db.rs25
-rw-r--r--src/dirs.rs9
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<tokio::sync::RwLock<crate::agent::State>>,
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<String> {
}
}
+async fn load_db() -> anyhow::Result<rbw::db::Db> {
+ 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<String> {
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<DecryptedCipher> = 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<String>, Option<String>) {
(password, notes)
}
-fn config_email() -> anyhow::Result<String> {
+fn load_db() -> anyhow::Result<rbw::db::Db> {
+ 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<Self> {
- let mut fh = std::fs::File::open(crate::dirs::db_file(email))
+ pub fn load(server: &str, email: &str) -> Result<Self> {
+ 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<Self> {
- 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<Self> {
+ 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 {