Skip to content

Commit

Permalink
Add YubiKey Auth Support
Browse files Browse the repository at this point in the history
This commit adds support for YubiKey auth.
A pinentry prompt asks the user to touch the Yubikey, which causes it to
produce input (like a keyboard). That can then be sent to the Bitwarden
API where it can ID the YubiKey and validate the request.

Fixes: doy#7

Signed-off-by: Dave Tucker <[email protected]>
  • Loading branch information
dave-tucker committed Nov 24, 2021
1 parent cc20037 commit 290b0a9
Showing 1 changed file with 93 additions and 2 deletions.
95 changes: 93 additions & 2 deletions src/bin/rbw-agent/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,36 @@ pub async fn login(
)
.await?;
break;
} else {
return Err(anyhow::anyhow!("TODO"));
} else if providers.contains(

This comment has been minimized.

Copy link
@soenkeliebau

soenkeliebau Nov 30, 2021

Just tested this locally and it seems to work fine, thank you very much for this!
There are fairly long delays when logging in, but if feels like this is server side (full disclosure: pure conjecture, without having traced it).

&rbw::api::TwoFactorProviderType::Yubikey,
){
let (
access_token,
refresh_token,
iterations,
protected_key,
) = yubikey_auth(
tty,
&email,
password.clone(),
)
.await?;
login_success(
sock,
state,
access_token,
refresh_token,
iterations,
protected_key,
password,
db,
email,
)
.await?;
break;
}
else {
return Err(anyhow::anyhow!("no supported 2fa method found in {:?}", providers));
}
}
Err(rbw::error::Error::IncorrectPassword { message }) => {
Expand Down Expand Up @@ -277,6 +305,69 @@ async fn two_factor(
unreachable!()
}

async fn yubikey_auth(

This comment has been minimized.

Copy link
@soenkeliebau

soenkeliebau Nov 30, 2021

Is there a reason why you decided to go with a new fn here instead of extending the existing two_factor fn that take the authenticator mechanism as a parameter. It feels to me like that was intended to be extended at some point.

This comment has been minimized.

Copy link
@dave-tucker

dave-tucker Nov 30, 2021

Author Owner

two_factor does take provider as a parameter and it wasn't clear whether this was by design (so this fn could be reused for other 2fa mechanisms or not).

There were a couple of reasons I decided on a separate function.

  1. The message in the window should be different for YubiKey (but arguably you could also just let message = match provider { ....
  2. The error handling is very subtly different. For example, this case
    Err(rbw::error::Error::TwoFactorRequired { .. }) => {
    doesn't apply to YubiKey auth. I have a feeling - not verified - that the error codes from the API will be ever so slightly different for each auth mechanism and that we'll have to handle those separately.
  3. I started thinking ahead to about how to implement webauthn support, and I concluded it would need it's own function since it would have to launch a browser window...
tty: Option<&str>,
email: &str,
password: rbw::locked::Password,
) -> anyhow::Result<(String, String, u32, String)> {
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 code = rbw::pinentry::getpin(
&config_pinentry().await?,
"Authenticator App",
"Touch your Security Key",
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.clone(),
Some(code),
Some(rbw::api::TwoFactorProviderType::Yubikey),
)
.await
{
Ok((access_token, refresh_token, iterations, protected_key)) => {
return Ok((
access_token,
refresh_token,
iterations,
protected_key,
))
}
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")
}
}
}

unreachable!()
}

async fn login_success(
sock: &mut crate::sock::Sock,
state: std::sync::Arc<tokio::sync::RwLock<crate::agent::State>>,
Expand Down

0 comments on commit 290b0a9

Please sign in to comment.