aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2021-10-27 22:16:46 -0400
committerJesse Luehrs <doy@tozt.net>2021-10-27 22:16:46 -0400
commit5d0e308c1c57bea30d2ad9008b6dbc7936d58645 (patch)
tree3c3798d5fb826aeaca62e11d9e56e1e3b610ca2b
parent7b95f7b9d84c44ca273c1a1ce7ab62925d4a5def (diff)
downloadrbw-5d0e308c1c57bea30d2ad9008b6dbc7936d58645.tar.gz
rbw-5d0e308c1c57bea30d2ad9008b6dbc7936d58645.zip
add a separate "register" command to log in with api keys
-rw-r--r--src/actions.rs19
-rw-r--r--src/api.rs91
-rw-r--r--src/bin/rbw-agent/actions.rs354
-rw-r--r--src/bin/rbw-agent/agent.rs21
-rw-r--r--src/bin/rbw/actions.rs12
-rw-r--r--src/bin/rbw/commands.rs15
-rw-r--r--src/bin/rbw/main.rs17
-rw-r--r--src/locked.rs44
-rw-r--r--src/protocol.rs2
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<crate::api::TwoFactorProviderType>,
) -> 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<TwoFactorProviderType>,
) -> 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<tokio::sync::RwLock<crate::agent::State>>,
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<rbw::locked::Password>,
+ 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<HashedLoginCredentials> {
- 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,