aboutsummaryrefslogtreecommitdiffstats
path: root/src/bin
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2021-10-27 05:38:03 -0400
committerJesse Luehrs <doy@tozt.net>2021-10-27 05:44:08 -0400
commit9acabc3a7497e3eb3536992212d34f77a649ae16 (patch)
treee1433fe36c6768704ea63443910731c4ad7fbcc1 /src/bin
parenteb5e4ea7bcc3008a8d06da6884ee9713f8df0034 (diff)
downloadrbw-9acabc3a7497e3eb3536992212d34f77a649ae16.tar.gz
rbw-9acabc3a7497e3eb3536992212d34f77a649ae16.zip
start working on apikey authentication
not entirely working yet, because api keys aren't allowed to use the offline_access scope which provides access to the oauth refresh token. we will probably need to move to using stable device identifiers (should be able to generate one when the config is first created) and move this logic to a separate "rbw register" command which just does an apikey login but doesn't try to store any data, and then use the normal login flow after that
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/rbw-agent/actions.rs284
-rw-r--r--src/bin/rbw-agent/agent.rs19
-rw-r--r--src/bin/rbw/actions.rs8
-rw-r--r--src/bin/rbw/commands.rs8
-rw-r--r--src/bin/rbw/main.rs12
5 files changed, 244 insertions, 87 deletions
diff --git a/src/bin/rbw-agent/actions.rs b/src/bin/rbw-agent/actions.rs
index cb30551..d29321c 100644
--- a/src/bin/rbw-agent/actions.rs
+++ b/src/bin/rbw-agent/actions.rs
@@ -4,6 +4,7 @@ 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());
@@ -23,49 +24,66 @@ pub async fn login(
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 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, None, None).await {
- Ok((
- access_token,
- refresh_token,
- iterations,
- protected_key,
- )) => {
- login_success(
- sock,
- state,
+ 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,
+ )
+ .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((
access_token,
refresh_token,
iterations,
protected_key,
- password,
- db,
- email,
- )
- .await?;
- break;
- }
- Err(rbw::error::Error::TwoFactorRequired { providers }) => {
- if providers.contains(
- &rbw::api::TwoFactorProviderType::Authenticator,
- ) {
- let (
+ )) => {
+ 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 (
access_token,
refresh_token,
iterations,
@@ -73,10 +91,79 @@ pub async fn login(
) = two_factor(
tty,
&email,
- &password,
+ creds,
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,
@@ -84,30 +171,68 @@ pub async fn login(
refresh_token,
iterations,
protected_key,
- password,
+ 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(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(e) => {
- return Err(e)
- .context("failed to log in to bitwarden instance")
}
}
}
@@ -121,7 +246,7 @@ pub async fn login(
async fn two_factor(
tty: Option<&str>,
email: &str,
- password: &rbw::locked::Password,
+ creds: rbw::locked::LoginCredentials,
provider: rbw::api::TwoFactorProviderType,
) -> anyhow::Result<(String, String, u32, String)> {
let mut err_msg = None;
@@ -144,8 +269,13 @@ async fn two_factor(
.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, Some(code), Some(provider))
- .await
+ match rbw::actions::login(
+ email,
+ creds.clone(),
+ Some(code),
+ Some(provider),
+ )
+ .await
{
Ok((access_token, refresh_token, iterations, protected_key)) => {
return Ok((
@@ -196,7 +326,7 @@ async fn login_success(
refresh_token: String,
iterations: u32,
protected_key: String,
- password: rbw::locked::Password,
+ password: Option<rbw::locked::Password>,
mut db: rbw::db::Db,
email: String,
) -> anyhow::Result<()> {
@@ -218,23 +348,25 @@ async fn login_success(
));
};
- let res = rbw::actions::unlock(
- &email,
- &password,
- iterations,
- &protected_key,
- &protected_private_key,
- &db.protected_org_keys,
- )
- .await;
+ if let Some(password) = password {
+ 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);
+ 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"),
}
- 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 8db8a59..7ca519b 100644
--- a/src/bin/rbw-agent/agent.rs
+++ b/src/bin/rbw-agent/agent.rs
@@ -134,8 +134,23 @@ async fn handle_request(
};
let set_timeout = match &req.action {
rbw::protocol::Action::Login => {
- crate::actions::login(sock, state.clone(), req.tty.as_deref())
- .await?;
+ crate::actions::login(
+ sock,
+ state.clone(),
+ req.tty.as_deref(),
+ false,
+ )
+ .await?;
+ true
+ }
+ rbw::protocol::Action::LoginApiKey => {
+ crate::actions::login(
+ sock,
+ state.clone(),
+ req.tty.as_deref(),
+ true,
+ )
+ .await?;
true
}
rbw::protocol::Action::Unlock => {
diff --git a/src/bin/rbw/actions.rs b/src/bin/rbw/actions.rs
index 75703f9..e0e1e55 100644
--- a/src/bin/rbw/actions.rs
+++ b/src/bin/rbw/actions.rs
@@ -1,8 +1,12 @@
use anyhow::Context as _;
use std::io::Read as _;
-pub fn login() -> anyhow::Result<()> {
- simple_action(rbw::protocol::Action::Login)
+pub fn login(apikey: bool) -> anyhow::Result<()> {
+ if apikey {
+ simple_action(rbw::protocol::Action::LoginApiKey)
+ } else {
+ 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 db385a8..dddd501 100644
--- a/src/bin/rbw/commands.rs
+++ b/src/bin/rbw/commands.rs
@@ -465,16 +465,16 @@ pub fn config_unset(key: &str) -> anyhow::Result<()> {
Ok(())
}
-pub fn login() -> anyhow::Result<()> {
+pub fn login(apikey: bool) -> anyhow::Result<()> {
ensure_agent()?;
- crate::actions::login()?;
+ crate::actions::login(apikey)?;
Ok(())
}
pub fn unlock() -> anyhow::Result<()> {
ensure_agent()?;
- crate::actions::login()?;
+ crate::actions::login(false)?;
crate::actions::unlock()?;
Ok(())
@@ -489,7 +489,7 @@ pub fn unlocked() -> anyhow::Result<()> {
pub fn sync() -> anyhow::Result<()> {
ensure_agent()?;
- crate::actions::login()?;
+ crate::actions::login(false)?;
crate::actions::sync()?;
Ok(())
diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs
index e6e1249..2612398 100644
--- a/src/bin/rbw/main.rs
+++ b/src/bin/rbw/main.rs
@@ -18,7 +18,13 @@ enum Opt {
},
#[structopt(about = "Log in to the Bitwarden server")]
- Login,
+ 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,
+ },
#[structopt(about = "Unlock the local Bitwarden database")]
Unlock,
@@ -214,7 +220,7 @@ impl Opt {
Self::Config { config } => {
format!("config {}", config.subcommand_name())
}
- Self::Login => "login".to_string(),
+ Self::Login { .. } => "login".to_string(),
Self::Unlock => "unlock".to_string(),
Self::Unlocked => "unlocked".to_string(),
Self::Sync => "sync".to_string(),
@@ -284,7 +290,7 @@ fn main(opt: Opt) {
Config::Set { key, value } => commands::config_set(key, value),
Config::Unset { key } => commands::config_unset(key),
},
- Opt::Login => commands::login(),
+ Opt::Login { apikey } => commands::login(*apikey),
Opt::Unlock => commands::unlock(),
Opt::Unlocked => commands::unlocked(),
Opt::Sync => commands::sync(),