From 5d0e308c1c57bea30d2ad9008b6dbc7936d58645 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Wed, 27 Oct 2021 22:16:46 -0400 Subject: add a separate "register" command to log in with api keys --- src/actions.rs | 19 ++- src/api.rs | 91 ++++++----- src/bin/rbw-agent/actions.rs | 354 +++++++++++++++++++------------------------ src/bin/rbw-agent/agent.rs | 21 +-- src/bin/rbw/actions.rs | 12 +- src/bin/rbw/commands.rs | 15 +- src/bin/rbw/main.rs | 17 +-- src/locked.rs | 44 ------ src/protocol.rs | 2 +- 9 files changed, 250 insertions(+), 325 deletions(-) diff --git a/src/actions.rs b/src/actions.rs index b65226c..02ec854 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,8 +1,21 @@ 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, - creds: crate::locked::LoginCredentials, + password: crate::locked::Password, two_factor_token: Option<&str>, two_factor_provider: Option, ) -> Result<(String, String, u32, String)> { @@ -11,11 +24,13 @@ pub async fn login( 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)?; let (access_token, refresh_token, protected_key) = client .login( email, &config.device_id, - &creds.to_hashed(email, iterations)?, + &identity.master_password_hash, two_factor_token, two_factor_provider, ) diff --git a/src/api.rs b/src/api.rs index f4198b5..d8e35fe 100644 --- a/src/api.rs +++ b/src/api.rs @@ -576,42 +576,60 @@ 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, device_id: &str, - creds: &crate::locked::HashedLoginCredentials, + password_hash: &crate::locked::PasswordHash, two_factor_token: Option<&str>, two_factor_provider: Option, ) -> Result<(String, String, String)> { - let (grant_type, scope, password, client_id, client_secret) = - match creds { - crate::locked::HashedLoginCredentials::Password { - password_hash, - } => ( - "password", - "api offline_access", - Some(base64::encode(password_hash.hash())), - &b"desktop"[..], - None, - ), - crate::locked::HashedLoginCredentials::ApiKey { apikey } => ( - "client_credentials", - "api", - None, - apikey.client_id(), - Some(apikey.client_secret()), - ), - }; let connect_req = ConnectPasswordReq { - grant_type: grant_type.to_string(), + grant_type: "password".to_string(), username: email.to_string(), - password, - scope: scope.to_string(), - // XXX unwraps here are not necessarily safe - client_id: String::from_utf8(client_id.to_vec()).unwrap(), - client_secret: client_secret - .map(|secret| String::from_utf8(secret.to_vec()).unwrap()), + 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: device_id.to_string(), device_name: "rbw".to_string(), @@ -621,24 +639,13 @@ impl Client { two_factor_provider: two_factor_provider.map(|ty| ty as u32), }; let client = reqwest::Client::new(); - let mut headers = reqwest::header::HeaderMap::new(); - if connect_req.client_secret.is_none() - && connect_req.password.is_some() - { - headers.insert( - "auth-email", - // unwrap is safe because url safe base64 strings are always - // valid header values - reqwest::header::HeaderValue::from_str( - &base64::encode_config(email, base64::URL_SAFE_NO_PAD), - ) - .unwrap(), - ); - } let res = client .post(&self.identity_url("/connect/token")) .form(&connect_req) - .headers(headers) + .header( + "auth-email", + base64::encode_config(email, base64::URL_SAFE_NO_PAD), + ) .send() .await .map_err(|source| Error::Reqwest { source })?; diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs index d29321c..5918e2c 100644 --- a/src/bin/rbw-agent/actions.rs +++ b/src/bin/rbw-agent/actions.rs @@ -1,10 +1,86 @@ 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, + ) + .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, + ) + .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>, tty: Option<&str>, - apikey: bool, ) -> anyhow::Result<()> { let db = load_db().await.unwrap_or_else(|_| rbw::db::Db::new()); @@ -24,66 +100,51 @@ pub async fn login( let email = config_email().await?; let mut err_msg = None; - if apikey { - 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, - ) - .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, - ) + 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 password = rbw::pinentry::getpin( + &config_pinentry().await?, + "Master Password", + &format!("Log in to {}", host), + err.as_deref(), + tty, + ) + .await + .context("failed to read password from pinentry")?; + match rbw::actions::login(&email, password.clone(), None, None) .await - .context("failed to read client_secret from pinentry")?; - let creds = rbw::locked::LoginCredentials::from_apikey( - rbw::locked::ApiKey::new(client_id, client_secret), - ); - match rbw::actions::login(&email, creds.clone(), None, None) - .await - { - Ok(( + { + Ok(( + access_token, + refresh_token, + iterations, + protected_key, + )) => { + login_success( + sock, + state, access_token, refresh_token, iterations, protected_key, - )) => { - login_success( - sock, - state, - access_token, - refresh_token, - iterations, - protected_key, - None, - db, - email, - ) - .await?; - break; - } - Err(rbw::error::Error::TwoFactorRequired { - providers, - }) => { - if providers.contains( - &rbw::api::TwoFactorProviderType::Authenticator, - ) { - let ( + password, + db, + email, + ) + .await?; + break; + } + Err(rbw::error::Error::TwoFactorRequired { providers }) => { + if providers.contains( + &rbw::api::TwoFactorProviderType::Authenticator, + ) { + let ( access_token, refresh_token, iterations, @@ -91,79 +152,10 @@ pub async fn login( ) = two_factor( tty, &email, - creds, + password.clone(), rbw::api::TwoFactorProviderType::Authenticator, ) .await?; - login_success( - sock, - state, - access_token, - refresh_token, - iterations, - protected_key, - None, - db, - email, - ) - .await?; - break; - } else { - return Err(anyhow::anyhow!("TODO")); - } - } - 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", - ) - } - } - } - } else { - 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 password = rbw::pinentry::getpin( - &config_pinentry().await?, - "Master Password", - &format!("Log in to {}", host), - err.as_deref(), - tty, - ) - .await - .context("failed to read password from pinentry")?; - let creds = rbw::locked::LoginCredentials::from_password( - password.clone(), - ); - match rbw::actions::login(&email, creds.clone(), None, None) - .await - { - Ok(( - access_token, - refresh_token, - iterations, - protected_key, - )) => { login_success( sock, state, @@ -171,69 +163,31 @@ pub async fn login( refresh_token, iterations, protected_key, - Some(password), + password, db, email, ) .await?; break; + } else { + return Err(anyhow::anyhow!("TODO")); } - Err(rbw::error::Error::TwoFactorRequired { - providers, - }) => { - if providers.contains( - &rbw::api::TwoFactorProviderType::Authenticator, - ) { - let ( - access_token, - refresh_token, - iterations, - protected_key, - ) = two_factor( - tty, - &email, - creds.clone(), - rbw::api::TwoFactorProviderType::Authenticator, - ) - .await?; - login_success( - sock, - state, - access_token, - refresh_token, - iterations, - protected_key, - Some(password), - db, - email, - ) - .await?; - break; - } else { - return Err(anyhow::anyhow!("TODO")); - } - } - 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", - ) + } + 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") + } } } } @@ -246,7 +200,7 @@ pub async fn login( async fn two_factor( tty: Option<&str>, email: &str, - creds: rbw::locked::LoginCredentials, + password: rbw::locked::Password, provider: rbw::api::TwoFactorProviderType, ) -> anyhow::Result<(String, String, u32, String)> { let mut err_msg = None; @@ -271,7 +225,7 @@ async fn two_factor( .context("code was not valid utf8")?; match rbw::actions::login( email, - creds.clone(), + password.clone(), Some(code), Some(provider), ) @@ -326,7 +280,7 @@ async fn login_success( refresh_token: String, iterations: u32, protected_key: String, - password: Option, + password: rbw::locked::Password, mut db: rbw::db::Db, email: String, ) -> anyhow::Result<()> { @@ -348,25 +302,23 @@ async fn login_success( )); }; - if let Some(password) = password { - let res = rbw::actions::unlock( - &email, - &password, - iterations, - &protected_key, - &protected_private_key, - &db.protected_org_keys, - ) - .await; + let res = rbw::actions::unlock( + &email, + &password, + iterations, + &protected_key, + &protected_private_key, + &db.protected_org_keys, + ) + .await; - match res { - Ok((keys, org_keys)) => { - let mut state = state.write().await; - state.priv_key = Some(keys); - state.org_keys = Some(org_keys); - } - Err(e) => return Err(e).context("failed to unlock database"), + match res { + Ok((keys, org_keys)) => { + let mut state = state.write().await; + state.priv_key = Some(keys); + state.org_keys = Some(org_keys); } + Err(e) => return Err(e).context("failed to unlock database"), } Ok(()) diff --git a/src/bin/rbw-agent/agent.rs b/src/bin/rbw-agent/agent.rs index 7ca519b..fae8c7b 100644 --- a/src/bin/rbw-agent/agent.rs +++ b/src/bin/rbw-agent/agent.rs @@ -133,24 +133,13 @@ async fn handle_request( } }; let set_timeout = match &req.action { - rbw::protocol::Action::Login => { - crate::actions::login( - sock, - state.clone(), - req.tty.as_deref(), - false, - ) - .await?; + rbw::protocol::Action::Register => { + crate::actions::register(sock, req.tty.as_deref()).await?; true } - rbw::protocol::Action::LoginApiKey => { - crate::actions::login( - sock, - state.clone(), - req.tty.as_deref(), - true, - ) - .await?; + rbw::protocol::Action::Login => { + crate::actions::login(sock, state.clone(), req.tty.as_deref()) + .await?; true } rbw::protocol::Action::Unlock => { diff --git a/src/bin/rbw/actions.rs b/src/bin/rbw/actions.rs index e0e1e55..39fde15 100644 --- a/src/bin/rbw/actions.rs +++ b/src/bin/rbw/actions.rs @@ -1,12 +1,12 @@ use anyhow::Context as _; use std::io::Read as _; -pub fn login(apikey: bool) -> anyhow::Result<()> { - if apikey { - simple_action(rbw::protocol::Action::LoginApiKey) - } else { - simple_action(rbw::protocol::Action::Login) - } +pub fn register() -> anyhow::Result<()> { + simple_action(rbw::protocol::Action::Register) +} + +pub fn login() -> anyhow::Result<()> { + simple_action(rbw::protocol::Action::Login) } pub fn unlock() -> anyhow::Result<()> { diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index dddd501..9efd966 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -465,16 +465,23 @@ pub fn config_unset(key: &str) -> anyhow::Result<()> { Ok(()) } -pub fn login(apikey: bool) -> anyhow::Result<()> { +pub fn register() -> anyhow::Result<()> { ensure_agent()?; - crate::actions::login(apikey)?; + crate::actions::register()?; + + Ok(()) +} + +pub fn login() -> anyhow::Result<()> { + ensure_agent()?; + crate::actions::login()?; Ok(()) } pub fn unlock() -> anyhow::Result<()> { ensure_agent()?; - crate::actions::login(false)?; + crate::actions::login()?; crate::actions::unlock()?; Ok(()) @@ -489,7 +496,7 @@ pub fn unlocked() -> anyhow::Result<()> { pub fn sync() -> anyhow::Result<()> { ensure_agent()?; - crate::actions::login(false)?; + crate::actions::login()?; crate::actions::sync()?; Ok(()) diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index 2612398..0f2d13e 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -17,14 +17,11 @@ enum Opt { config: Config, }, + #[structopt(about = "Register this device with the Bitwarden server")] + Register, + #[structopt(about = "Log in to the Bitwarden server")] - Login { - #[structopt( - long, - help = "Log in to the Bitwarden server using your user API key (see https://bitwarden.com/help/article/personal-api-key/)" - )] - apikey: bool, - }, + Login, #[structopt(about = "Unlock the local Bitwarden database")] Unlock, @@ -220,7 +217,8 @@ impl Opt { Self::Config { config } => { format!("config {}", config.subcommand_name()) } - Self::Login { .. } => "login".to_string(), + Self::Register => "register".to_string(), + Self::Login => "login".to_string(), Self::Unlock => "unlock".to_string(), Self::Unlocked => "unlocked".to_string(), Self::Sync => "sync".to_string(), @@ -290,7 +288,8 @@ fn main(opt: Opt) { Config::Set { key, value } => commands::config_set(key, value), Config::Unset { key } => commands::config_unset(key), }, - Opt::Login { apikey } => commands::login(*apikey), + Opt::Register => commands::register(), + Opt::Login => commands::login(), Opt::Unlock => commands::unlock(), Opt::Unlocked => commands::unlocked(), Opt::Sync => commands::sync(), diff --git a/src/locked.rs b/src/locked.rs index 4008912..4ddf021 100644 --- a/src/locked.rs +++ b/src/locked.rs @@ -1,5 +1,3 @@ -use crate::prelude::*; - use zeroize::Zeroize; const LEN: usize = 4096; @@ -147,45 +145,3 @@ impl ApiKey { self.client_secret.password() } } - -#[derive(Clone)] -pub enum LoginCredentials { - Password { password: Password }, - ApiKey { apikey: ApiKey }, -} - -impl LoginCredentials { - pub fn from_password(password: Password) -> Self { - Self::Password { password } - } - - pub fn from_apikey(apikey: ApiKey) -> Self { - Self::ApiKey { apikey } - } - - pub fn to_hashed( - self, - email: &str, - iterations: u32, - ) -> Result { - match self { - Self::Password { password } => { - let identity = crate::identity::Identity::new( - email, &password, iterations, - )?; - Ok(HashedLoginCredentials::Password { - password_hash: identity.master_password_hash, - }) - } - Self::ApiKey { apikey } => { - Ok(HashedLoginCredentials::ApiKey { apikey }) - } - } - } -} - -#[derive(Clone)] -pub enum HashedLoginCredentials { - Password { password_hash: PasswordHash }, - ApiKey { apikey: ApiKey }, -} diff --git a/src/protocol.rs b/src/protocol.rs index d36f909..14fa7f9 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -23,7 +23,7 @@ pub struct Request { #[serde(tag = "type")] pub enum Action { Login, - LoginApiKey, + Register, Unlock, CheckLock, Lock, -- cgit v1.2.3-54-g00ecf