diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/actions.rs | 33 | ||||
-rw-r--r-- | src/api.rs | 76 | ||||
-rw-r--r-- | src/bin/rbw-agent/actions.rs | 109 | ||||
-rw-r--r-- | src/bin/rbw-agent/agent.rs | 8 | ||||
-rw-r--r-- | src/bin/rbw/actions.rs | 4 | ||||
-rw-r--r-- | src/bin/rbw/commands.rs | 47 | ||||
-rw-r--r-- | src/bin/rbw/main.rs | 33 | ||||
-rw-r--r-- | src/cipherstring.rs | 116 | ||||
-rw-r--r-- | src/config.rs | 17 | ||||
-rw-r--r-- | src/error.rs | 9 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/locked.rs | 37 | ||||
-rw-r--r-- | src/pinentry.rs | 10 | ||||
-rw-r--r-- | src/protocol.rs | 1 |
14 files changed, 364 insertions, 137 deletions
diff --git a/src/actions.rs b/src/actions.rs index 0d8c88d..02ec854 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,37 +1,42 @@ use crate::prelude::*; +pub async fn register( + email: &str, + apikey: crate::locked::ApiKey, +) -> Result<()> { + let config = crate::config::Config::load_async().await?; + let client = + crate::api::Client::new(&config.base_url(), &config.identity_url()); + + client.register(email, &config.device_id, &apikey).await?; + + Ok(()) +} + pub async fn login( email: &str, - password: &crate::locked::Password, + password: crate::locked::Password, two_factor_token: Option<&str>, two_factor_provider: Option<crate::api::TwoFactorProviderType>, -) -> Result<(String, String, u32, String, crate::locked::Keys)> { +) -> Result<(String, String, u32, String)> { let config = crate::config::Config::load_async().await?; let client = crate::api::Client::new(&config.base_url(), &config.identity_url()); let iterations = client.prelogin(email).await?; let identity = - crate::identity::Identity::new(email, password, iterations)?; - + crate::identity::Identity::new(email, &password, iterations)?; let (access_token, refresh_token, protected_key) = client .login( - &identity.email, + email, + &config.device_id, &identity.master_password_hash, two_factor_token, two_factor_provider, ) .await?; - let master_keys = crate::cipherstring::CipherString::new(&protected_key)? - .decrypt_locked_symmetric(&identity.keys)?; - Ok(( - access_token, - refresh_token, - iterations, - protected_key, - crate::locked::Keys::new(master_keys), - )) + Ok((access_token, refresh_token, iterations, protected_key)) } pub async fn unlock( @@ -148,9 +148,10 @@ struct PreloginRes { struct ConnectPasswordReq { grant_type: String, username: String, - password: String, + password: Option<String>, scope: String, client_id: String, + client_secret: Option<String>, #[serde(rename = "deviceType")] device_type: u32, #[serde(rename = "deviceIdentifier")] @@ -178,7 +179,7 @@ struct ConnectPasswordRes { #[derive(serde::Deserialize, Debug)] struct ConnectErrorRes { error: String, - error_description: String, + error_description: Option<String>, #[serde(rename = "ErrorModel")] error_model: Option<ConnectErrorResErrorModel>, #[serde(rename = "TwoFactorProviders")] @@ -575,23 +576,62 @@ impl Client { Ok(prelogin_res.kdf_iterations) } + pub async fn register( + &self, + email: &str, + device_id: &str, + apikey: &crate::locked::ApiKey, + ) -> Result<()> { + let connect_req = ConnectPasswordReq { + grant_type: "client_credentials".to_string(), + username: email.to_string(), + password: None, + scope: "api".to_string(), + // XXX unwraps here are not necessarily safe + client_id: String::from_utf8(apikey.client_id().to_vec()) + .unwrap(), + client_secret: Some( + String::from_utf8(apikey.client_secret().to_vec()).unwrap(), + ), + device_type: 8, + device_identifier: device_id.to_string(), + device_name: "rbw".to_string(), + device_push_token: "".to_string(), + two_factor_token: None, + two_factor_provider: None, + }; + let client = reqwest::Client::new(); + let res = client + .post(&self.identity_url("/connect/token")) + .form(&connect_req) + .send() + .await + .map_err(|source| Error::Reqwest { source })?; + if let reqwest::StatusCode::OK = res.status() { + Ok(()) + } else { + let code = res.status().as_u16(); + Err(classify_login_error(&res.json_with_path().await?, code)) + } + } + pub async fn login( &self, email: &str, - master_password_hash: &crate::locked::PasswordHash, + device_id: &str, + password_hash: &crate::locked::PasswordHash, two_factor_token: Option<&str>, two_factor_provider: Option<TwoFactorProviderType>, ) -> Result<(String, String, String)> { let connect_req = ConnectPasswordReq { grant_type: "password".to_string(), username: email.to_string(), - password: base64::encode(master_password_hash.hash()), + password: Some(base64::encode(password_hash.hash())), scope: "api offline_access".to_string(), client_id: "desktop".to_string(), + client_secret: None, device_type: 8, - device_identifier: uuid::Uuid::new_v4() - .to_hyphenated() - .to_string(), + device_identifier: device_id.to_string(), device_name: "rbw".to_string(), device_push_token: "".to_string(), two_factor_token: two_factor_token @@ -602,6 +642,10 @@ impl Client { let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) + .header( + "auth-email", + base64::encode_config(email, base64::URL_SAFE_NO_PAD), + ) .send() .await .map_err(|source| Error::Reqwest { source })?; @@ -708,7 +752,7 @@ impl Client { password: password.clone(), totp: totp.clone(), uris, - }) + }); } crate::db::EntryData::Card { cardholder_name, @@ -1046,15 +1090,17 @@ impl Client { } fn classify_login_error(error_res: &ConnectErrorRes, code: u16) -> Error { + let error_desc = error_res.error_description.clone(); + let error_desc = error_desc.as_deref(); match error_res.error.as_str() { - "invalid_grant" => match error_res.error_description.as_str() { - "invalid_username_or_password" => { + "invalid_grant" => match error_desc { + Some("invalid_username_or_password") => { if let Some(error_model) = error_res.error_model.as_ref() { let message = error_model.message.as_str().to_string(); return Error::IncorrectPassword { message }; } } - "Two factor required." => { + Some("Two factor required.") => { if let Some(providers) = error_res.two_factor_providers.as_ref() { @@ -1063,12 +1109,18 @@ fn classify_login_error(error_res: &ConnectErrorRes, code: u16) -> Error { }; } } + Some("Captcha required.") => { + return Error::RegistrationRequired; + } _ => {} }, + "invalid_client" => { + return Error::IncorrectApiKey; + } "" => { // bitwarden_rs returns an empty error and error_description for // this case, for some reason - if error_res.error_description.is_empty() { + if error_desc.is_none() || error_desc == Some("") { if let Some(error_model) = error_res.error_model.as_ref() { let message = error_model.message.as_str().to_string(); match message.as_str() { diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs index 4f4096e..1cc71c3 100644 --- a/src/bin/rbw-agent/actions.rs +++ b/src/bin/rbw-agent/actions.rs @@ -1,5 +1,84 @@ use anyhow::Context as _; +pub async fn register( + sock: &mut crate::sock::Sock, + tty: Option<&str>, +) -> anyhow::Result<()> { + let db = load_db().await.unwrap_or_else(|_| rbw::db::Db::new()); + + if db.needs_login() { + let url_str = config_base_url().await?; + let url = reqwest::Url::parse(&url_str) + .context("failed to parse base url")?; + let host = if let Some(host) = url.host_str() { + host + } else { + return Err(anyhow::anyhow!( + "couldn't find host in rbw base url {}", + url_str + )); + }; + + let email = config_email().await?; + + let mut err_msg = None; + for i in 1_u8..=3 { + let err = if i > 1 { + // this unwrap is safe because we only ever continue the loop + // if we have set err_msg + Some(format!("{} (attempt {}/3)", err_msg.unwrap(), i)) + } else { + None + }; + let client_id = rbw::pinentry::getpin( + &config_pinentry().await?, + "API key client__id", + &format!("Log in to {}", host), + err.as_deref(), + tty, + false, + ) + .await + .context("failed to read client_id from pinentry")?; + let client_secret = rbw::pinentry::getpin( + &config_pinentry().await?, + "API key client__secret", + &format!("Log in to {}", host), + err.as_deref(), + tty, + false, + ) + .await + .context("failed to read client_secret from pinentry")?; + let apikey = rbw::locked::ApiKey::new(client_id, client_secret); + match rbw::actions::register(&email, apikey.clone()).await { + Ok(()) => { + break; + } + Err(rbw::error::Error::IncorrectPassword { message }) => { + if i == 3 { + return Err(rbw::error::Error::IncorrectPassword { + message, + }) + .context("failed to log in to bitwarden instance"); + } else { + err_msg = Some(message); + continue; + } + } + Err(e) => { + return Err(e) + .context("failed to log in to bitwarden instance") + } + } + } + } + + respond_ack(sock).await?; + + Ok(()) +} + pub async fn login( sock: &mut crate::sock::Sock, state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>, @@ -37,16 +116,18 @@ pub async fn login( &format!("Log in to {}", host), err.as_deref(), tty, + true, ) .await .context("failed to read password from pinentry")?; - match rbw::actions::login(&email, &password, None, None).await { + match rbw::actions::login(&email, password.clone(), None, None) + .await + { Ok(( access_token, refresh_token, iterations, protected_key, - _, )) => { login_success( sock, @@ -74,7 +155,7 @@ pub async fn login( ) = two_factor( tty, &email, - &password, + password.clone(), rbw::api::TwoFactorProviderType::Authenticator, ) .await?; @@ -122,7 +203,7 @@ pub async fn login( async fn two_factor( tty: Option<&str>, email: &str, - password: &rbw::locked::Password, + password: rbw::locked::Password, provider: rbw::api::TwoFactorProviderType, ) -> anyhow::Result<(String, String, u32, String)> { let mut err_msg = None; @@ -140,26 +221,21 @@ async fn two_factor( "Enter the 6 digit verification code from your authenticator app.", err.as_deref(), tty, + true, ) .await .context("failed to read code from pinentry")?; let code = std::str::from_utf8(code.password()) .context("code was not valid utf8")?; match rbw::actions::login( - &email, - &password, + email, + password.clone(), Some(code), Some(provider), ) .await { - Ok(( - access_token, - refresh_token, - iterations, - protected_key, - _, - )) => { + Ok((access_token, refresh_token, iterations, protected_key)) => { return Ok(( access_token, refresh_token, @@ -300,6 +376,7 @@ pub async fn unlock( "Unlock the local database", err.as_deref(), tty, + true, ) .await .context("failed to read password from pinentry")?; @@ -430,7 +507,7 @@ pub async fn decrypt( .context("failed to parse encrypted secret")?; let plaintext = String::from_utf8( cipherstring - .decrypt_symmetric(&keys) + .decrypt_symmetric(keys) .context("failed to decrypt encrypted secret")?, ) .context("failed to parse decrypted secret")?; @@ -512,7 +589,7 @@ 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) + rbw::db::Db::load_async(&config.server_name(), email) .await .map_err(anyhow::Error::new) } else { @@ -523,7 +600,7 @@ async fn load_db() -> anyhow::Result<rbw::db::Db> { 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) + db.save_async(&config.server_name(), email) .await .map_err(anyhow::Error::new) } else { diff --git a/src/bin/rbw-agent/agent.rs b/src/bin/rbw-agent/agent.rs index 760a1fc..fae8c7b 100644 --- a/src/bin/rbw-agent/agent.rs +++ b/src/bin/rbw-agent/agent.rs @@ -133,6 +133,10 @@ async fn handle_request( } }; let set_timeout = match &req.action { + rbw::protocol::Action::Register => { + crate::actions::register(sock, req.tty.as_deref()).await?; + true + } rbw::protocol::Action::Login => { crate::actions::login(sock, state.clone(), req.tty.as_deref()) .await?; @@ -167,7 +171,7 @@ async fn handle_request( crate::actions::decrypt( sock, state.clone(), - &cipherstring, + cipherstring, org_id.as_deref(), ) .await?; @@ -177,7 +181,7 @@ async fn handle_request( crate::actions::encrypt( sock, state.clone(), - &plaintext, + plaintext, org_id.as_deref(), ) .await?; diff --git a/src/bin/rbw/actions.rs b/src/bin/rbw/actions.rs index 75703f9..39fde15 100644 --- a/src/bin/rbw/actions.rs +++ b/src/bin/rbw/actions.rs @@ -1,6 +1,10 @@ use anyhow::Context as _; use std::io::Read as _; +pub fn register() -> anyhow::Result<()> { + simple_action(rbw::protocol::Action::Register) +} + pub fn login() -> anyhow::Result<()> { simple_action(rbw::protocol::Action::Login) } diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index c1f9291..9efd966 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -465,6 +465,13 @@ pub fn config_unset(key: &str) -> anyhow::Result<()> { Ok(()) } +pub fn register() -> anyhow::Result<()> { + ensure_agent()?; + crate::actions::register()?; + + Ok(()) +} + pub fn login() -> anyhow::Result<()> { ensure_agent()?; crate::actions::login()?; @@ -642,7 +649,7 @@ pub fn add( let mut folder_id = None; if let Some(folder_name) = folder { let (new_access_token, folders) = - rbw::actions::list_folders(&access_token, &refresh_token)?; + rbw::actions::list_folders(&access_token, refresh_token)?; if let Some(new_access_token) = new_access_token { access_token = new_access_token.clone(); db.access_token = Some(new_access_token); @@ -663,7 +670,7 @@ pub fn add( if folder_id.is_none() { let (new_access_token, id) = rbw::actions::create_folder( &access_token, - &refresh_token, + refresh_token, &crate::actions::encrypt(folder_name, None)?, )?; if let Some(new_access_token) = new_access_token { @@ -677,7 +684,7 @@ pub fn add( if let (Some(access_token), ()) = rbw::actions::add( &access_token, - &refresh_token, + refresh_token, &name, &rbw::db::EntryData::Login { username, @@ -735,7 +742,7 @@ pub fn generate( let mut folder_id = None; if let Some(folder_name) = folder { let (new_access_token, folders) = - rbw::actions::list_folders(&access_token, &refresh_token)?; + rbw::actions::list_folders(&access_token, refresh_token)?; if let Some(new_access_token) = new_access_token { access_token = new_access_token.clone(); db.access_token = Some(new_access_token); @@ -758,7 +765,7 @@ pub fn generate( if folder_id.is_none() { let (new_access_token, id) = rbw::actions::create_folder( &access_token, - &refresh_token, + refresh_token, &crate::actions::encrypt(folder_name, None)?, )?; if let Some(new_access_token) = new_access_token { @@ -772,7 +779,7 @@ pub fn generate( if let (Some(access_token), ()) = rbw::actions::add( &access_token, - &refresh_token, + refresh_token, &name, &rbw::db::EntryData::Login { username, @@ -880,8 +887,8 @@ pub fn edit( }; if let (Some(access_token), ()) = rbw::actions::edit( - &access_token, - &refresh_token, + access_token, + refresh_token, &entry.id, entry.org_id.as_deref(), &entry.name, @@ -921,7 +928,7 @@ pub fn remove( .with_context(|| format!("couldn't find entry for '{}'", desc))?; if let (Some(access_token), ()) = - rbw::actions::remove(&access_token, &refresh_token, &entry.id)? + rbw::actions::remove(access_token, refresh_token, &entry.id)? { db.access_token = Some(access_token); save_db(&db)?; @@ -1053,7 +1060,7 @@ fn find_entry( Ok(_) => { for cipher in &db.entries { if name == cipher.id { - return Ok((cipher.clone(), decrypt_cipher(&cipher)?)); + return Ok((cipher.clone(), decrypt_cipher(cipher)?)); } } Err(anyhow::anyhow!("no entry found")) @@ -1183,10 +1190,7 @@ fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result<DecryptedCipher> { .name .as_ref() .map(|name| { - crate::actions::decrypt( - &name, - entry.org_id.as_deref(), - ) + crate::actions::decrypt(name, entry.org_id.as_deref()) }) .transpose()?, value: field @@ -1194,7 +1198,7 @@ fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result<DecryptedCipher> { .as_ref() .map(|value| { crate::actions::decrypt( - &value, + value, entry.org_id.as_deref(), ) }) @@ -1444,7 +1448,7 @@ fn parse_editor(contents: &str) -> (Option<String>, Option<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) + rbw::db::Db::load(&config.server_name(), email) .map_err(anyhow::Error::new) } else { Err(anyhow::anyhow!("failed to find email address in config")) @@ -1454,7 +1458,7 @@ fn load_db() -> anyhow::Result<rbw::db::Db> { 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) + db.save(&config.server_name(), email) .map_err(anyhow::Error::new) } else { Err(anyhow::anyhow!("failed to find email address in config")) @@ -1464,7 +1468,7 @@ fn save_db(db: &rbw::db::Db) -> anyhow::Result<()> { fn remove_db() -> anyhow::Result<()> { let config = rbw::config::Config::load()?; if let Some(email) = &config.email { - rbw::db::Db::remove(&config.server_name(), &email) + rbw::db::Db::remove(&config.server_name(), email) .map_err(anyhow::Error::new) } else { Err(anyhow::anyhow!("failed to find email address in config")) @@ -1494,8 +1498,11 @@ fn parse_totp_secret(secret: &str) -> anyhow::Result<Vec<u8>> { } else { secret.to_string() }; - base32::decode(base32::Alphabet::RFC4648 { padding: false }, &secret_str.replace(" ", "")) - .ok_or_else(|| anyhow::anyhow!("totp secret was not valid base32")) + base32::decode( + base32::Alphabet::RFC4648 { padding: false }, + &secret_str.replace(" ", ""), + ) + .ok_or_else(|| anyhow::anyhow!("totp secret was not valid base32")) } fn generate_totp(secret: &str) -> anyhow::Result<String> { diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index 6c6c33e..85631c5 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -17,6 +17,17 @@ enum Opt { config: Config, }, + #[structopt( + about = "Register this device with the Bitwarden server", + long_about = "Register this device with the Bitwarden server\n\n\ + The official Bitwarden server includes bot detection to prevent \ + brute force attacks. In order to avoid being detected as bot \ + traffic, you will need to use this command to log in with your \ + personal API key (instead of your password) first before regular \ + logins will work." + )] + Register, + #[structopt(about = "Log in to the Bitwarden server")] Login, @@ -214,6 +225,7 @@ impl Opt { Self::Config { config } => { format!("config {}", config.subcommand_name()) } + Self::Register => "register".to_string(), Self::Login => "login".to_string(), Self::Unlock => "unlock".to_string(), Self::Unlocked => "unlocked".to_string(), @@ -281,22 +293,23 @@ fn main(opt: Opt) { let res = match &opt { Opt::Config { config } => match config { Config::Show => commands::config_show(), - Config::Set { key, value } => commands::config_set(&key, &value), - Config::Unset { key } => commands::config_unset(&key), + Config::Set { key, value } => commands::config_set(key, value), + Config::Unset { key } => commands::config_unset(key), }, + Opt::Register => commands::register(), Opt::Login => commands::login(), Opt::Unlock => commands::unlock(), Opt::Unlocked => commands::unlocked(), Opt::Sync => commands::sync(), - Opt::List { fields } => commands::list(&fields), + Opt::List { fields } => commands::list(fields), Opt::Get { name, user, folder, full, - } => commands::get(&name, user.as_deref(), folder.as_deref(), *full), + } => commands::get(name, user.as_deref(), folder.as_deref(), *full), Opt::Code { name, user, folder } => { - commands::code(&name, user.as_deref(), folder.as_deref()) + commands::code(name, user.as_deref(), folder.as_deref()) } Opt::Add { name, @@ -304,7 +317,7 @@ fn main(opt: Opt) { uri, folder, } => commands::add( - &name, + name, user.as_deref(), uri.iter() // XXX not sure what the ui for specifying the match type @@ -349,18 +362,18 @@ fn main(opt: Opt) { ) } Opt::Edit { name, user, folder } => { - commands::edit(&name, user.as_deref(), folder.as_deref()) + commands::edit(name, user.as_deref(), folder.as_deref()) } Opt::Remove { name, user, folder } => { - commands::remove(&name, user.as_deref(), folder.as_deref()) + commands::remove(name, user.as_deref(), folder.as_deref()) } Opt::History { name, user, folder } => { - commands::history(&name, user.as_deref(), folder.as_deref()) + commands::history(name, user.as_deref(), folder.as_deref()) } Opt::Lock => commands::lock(), Opt::Purge => commands::purge(), Opt::StopAgent => commands::stop_agent(), - Opt::GenCompletions { shell } => gen_completions(&shell), + Opt::GenCompletions { shell } => gen_completions(shell), } .context(format!("rbw {}", opt.subcommand_name())); diff --git a/src/cipherstring.rs b/src/cipherstring.rs index 73eeeb6..39254c7 100644 --- a/src/cipherstring.rs +++ b/src/cipherstring.rs @@ -4,6 +4,7 @@ use block_modes::BlockMode as _; use block_padding::Padding as _; use hmac::{Mac as _, NewMac as _}; use rand::RngCore as _; +use rsa::pkcs8::FromPrivateKey as _; use zeroize::Zeroize as _; pub enum CipherString { @@ -122,27 +123,27 @@ impl CipherString { &self, keys: &crate::locked::Keys, ) -> Result<Vec<u8>> { - match self { - Self::Symmetric { + if let Self::Symmetric { + iv, + ciphertext, + mac, + } = self + { + let cipher = decrypt_common_symmetric( + keys, iv, ciphertext, - mac, - } => { - let cipher = decrypt_common_symmetric( - keys, - iv, - ciphertext, - mac.as_deref(), - )?; - cipher - .decrypt_vec(ciphertext) - .map_err(|source| Error::Decrypt { source }) - } - _ => Err(Error::InvalidCipherString { + mac.as_deref(), + )?; + cipher + .decrypt_vec(ciphertext) + .map_err(|source| Error::Decrypt { source }) + } else { + Err(Error::InvalidCipherString { reason: "found an asymmetric cipherstring, expecting symmetric" .to_string(), - }), + }) } } @@ -150,30 +151,30 @@ impl CipherString { &self, keys: &crate::locked::Keys, ) -> Result<crate::locked::Vec> { - match self { - Self::Symmetric { + if let Self::Symmetric { + iv, + ciphertext, + mac, + } = self + { + let mut res = crate::locked::Vec::new(); + res.extend(ciphertext.iter().copied()); + let cipher = decrypt_common_symmetric( + keys, iv, ciphertext, - mac, - } => { - let mut res = crate::locked::Vec::new(); - res.extend(ciphertext.iter().copied()); - let cipher = decrypt_common_symmetric( - keys, - iv, - ciphertext, - mac.as_deref(), - )?; - cipher - .decrypt(res.data_mut()) - .map_err(|source| Error::Decrypt { source })?; - Ok(res) - } - _ => Err(Error::InvalidCipherString { + mac.as_deref(), + )?; + cipher + .decrypt(res.data_mut()) + .map_err(|source| Error::Decrypt { source })?; + Ok(res) + } else { + Err(Error::InvalidCipherString { reason: "found an asymmetric cipherstring, expecting symmetric" .to_string(), - }), + }) } } @@ -181,34 +182,33 @@ impl CipherString { &self, private_key: &crate::locked::PrivateKey, ) -> Result<crate::locked::Vec> { - match self { - Self::Asymmetric { ciphertext } => { - let privkey_data = private_key.private_key(); - let privkey_data = block_padding::Pkcs7::unpad(privkey_data) - .map_err(|_| Error::Padding)?; - let pkey = rsa::RSAPrivateKey::from_pkcs8(privkey_data) - .map_err(|source| Error::Rsa { source })?; - let mut bytes = pkey - .decrypt( - rsa::padding::PaddingScheme::new_oaep::<sha1::Sha1>(), - ciphertext, - ) - .map_err(|source| Error::Rsa { source })?; + if let Self::Asymmetric { ciphertext } = self { + let privkey_data = private_key.private_key(); + let privkey_data = block_padding::Pkcs7::unpad(privkey_data) + .map_err(|_| Error::Padding)?; + let pkey = rsa::RsaPrivateKey::from_pkcs8_der(privkey_data) + .map_err(|source| Error::RsaPkcs8 { source })?; + let mut bytes = pkey + .decrypt( + rsa::padding::PaddingScheme::new_oaep::<sha1::Sha1>(), + ciphertext, + ) + .map_err(|source| Error::Rsa { source })?; - // XXX it'd be great if the rsa crate would let us decrypt - // into a preallocated buffer directly to avoid the - // intermediate vec that needs to be manually zeroized, etc - let mut res = crate::locked::Vec::new(); - res.extend(bytes.iter().copied()); - bytes.zeroize(); + // XXX it'd be great if the rsa crate would let us decrypt + // into a preallocated buffer directly to avoid the + // intermediate vec that needs to be manually zeroized, etc + let mut res = crate::locked::Vec::new(); + res.extend(bytes.iter().copied()); + bytes.zeroize(); - Ok(res) - } - _ => Err(Error::InvalidCipherString { + Ok(res) + } else { + Err(Error::InvalidCipherString { reason: "found a symmetric cipherstring, expecting asymmetric" .to_string(), - }), + }) } } } diff --git a/src/config.rs b/src/config.rs index c6e0787..bbc39f7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,8 @@ pub struct Config { pub lock_timeout: u64, #[serde(default = "default_pinentry")] pub pinentry: String, + #[serde(default = "stub_device_id")] + pub device_id: String, } impl Default for Config { @@ -22,6 +24,7 @@ impl Default for Config { identity_url: Default::default(), lock_timeout: default_lock_timeout(), pinentry: default_pinentry(), + device_id: default_device_id(), } } } @@ -34,6 +37,14 @@ pub fn default_pinentry() -> String { "pinentry".to_string() } +fn default_device_id() -> String { + uuid::Uuid::new_v4().to_hyphenated().to_string() +} + +fn stub_device_id() -> String { + String::from("fix") +} + impl Config { pub fn new() -> Self { Self::default() @@ -116,10 +127,14 @@ impl Config { } pub fn validate() -> Result<()> { - let config = Self::load()?; + let mut config = Self::load()?; if config.email.is_none() { return Err(Error::ConfigMissingEmail); } + if config.device_id == stub_device_id() { + config.device_id = default_device_id(); + config.save()?; + } Ok(()) } diff --git a/src/error.rs b/src/error.rs index bc97087..d584e53 100644 --- a/src/error.rs +++ b/src/error.rs @@ -46,6 +46,9 @@ pub enum Error { #[error("failed to expand with hkdf")] HkdfExpand, + #[error("incorrect api key")] + IncorrectApiKey, + #[error("{message}")] IncorrectPassword { message: String }, @@ -132,6 +135,9 @@ pub enum Error { #[error("error waiting for pinentry to exit")] PinentryWait { source: tokio::io::Error }, + #[error("This device has not yet been registered with the Bitwarden server. Run `rbw register` first, and then try again.")] + RegistrationRequired, + #[error("failed to remove db at {}", .file.display())] RemoveDb { source: std::io::Error, @@ -150,6 +156,9 @@ pub enum Error { #[error("failed to decrypt")] Rsa { source: rsa::errors::Error }, + #[error("failed to decrypt")] + RsaPkcs8 { source: rsa::pkcs8::Error }, + #[error("failed to save config to {}", .file.display())] SaveConfig { source: std::io::Error, @@ -12,6 +12,7 @@ #![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_lines)] #![allow(clippy::type_complexity)] +#![allow(clippy::unused_async)] pub mod actions; pub mod api; diff --git a/src/locked.rs b/src/locked.rs index 611e57e..4ddf021 100644 --- a/src/locked.rs +++ b/src/locked.rs @@ -32,7 +32,7 @@ impl Vec { pub fn zero(&mut self) { self.truncate(0); - self.data.extend(std::iter::repeat(0).take(LEN)) + self.data.extend(std::iter::repeat(0).take(LEN)); } pub fn extend(&mut self, it: impl Iterator<Item = u8>) { @@ -51,6 +51,15 @@ impl Drop for Vec { } } +impl Clone for Vec { + fn clone(&self) -> Self { + let mut new_vec = Self::new(); + new_vec.extend(self.data().iter().copied()); + new_vec + } +} + +#[derive(Clone)] pub struct Password { password: Vec, } @@ -65,6 +74,7 @@ impl Password { } } +#[derive(Clone)] pub struct Keys { keys: Vec, } @@ -83,6 +93,7 @@ impl Keys { } } +#[derive(Clone)] pub struct PasswordHash { hash: Vec, } @@ -97,6 +108,7 @@ impl PasswordHash { } } +#[derive(Clone)] pub struct PrivateKey { private_key: Vec, } @@ -110,3 +122,26 @@ impl PrivateKey { self.private_key.data() } } + +#[derive(Clone)] +pub struct ApiKey { + client_id: Password, + client_secret: Password, +} + +impl ApiKey { + pub fn new(client_id: Password, client_secret: Password) -> Self { + Self { + client_id, + client_secret, + } + } + + pub fn client_id(&self) -> &[u8] { + self.client_id.password() + } + + pub fn client_secret(&self) -> &[u8] { + self.client_secret.password() + } +} diff --git a/src/pinentry.rs b/src/pinentry.rs index d62d4b2..b4d2bb0 100644 --- a/src/pinentry.rs +++ b/src/pinentry.rs @@ -8,15 +8,19 @@ pub async fn getpin( desc: &str, err: Option<&str>, tty: Option<&str>, + grab: bool, ) -> Result<crate::locked::Password> { let mut opts = tokio::process::Command::new(pinentry); opts.stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()); + let mut args = vec!["-o", "0"]; if let Some(tty) = tty { - opts.args(&["-T", tty, "-o", "0"]); - } else { - opts.args(&["-o", "0"]); + args.extend(&["-T", tty]); } + if !grab { + args.push("-g"); + } + opts.args(args); let mut child = opts.spawn().map_err(|source| Error::Spawn { source })?; // unwrap is safe because we specified stdin as piped in the command opts // above diff --git a/src/protocol.rs b/src/protocol.rs index a4b9722..14fa7f9 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -23,6 +23,7 @@ pub struct Request { #[serde(tag = "type")] pub enum Action { Login, + Register, Unlock, CheckLock, Lock, |