Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ configuration options:
* `pinentry`: The
[pinentry](https://www.gnupg.org/related_software/pinentry/index.html)
executable to use. Defaults to `pinentry`.
* `biometric_unlock`: If `true`, `rbw` will first try to unlock using the
Bitwarden desktop app's biometric unlock (the same mechanism the official
browser extension uses): the desktop app must be running with "browser
integration" and biometric unlock enabled, and will show the OS biometric
prompt. If the desktop app is not available or the unlock fails, `rbw`
falls back to asking for the master password as usual. Note that this
relies on an internal Bitwarden protocol which may change in future
desktop app releases. Defaults to `false`.

### Profiles

Expand Down
11 changes: 11 additions & 0 deletions src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ pub fn unlock<S: std::hash::BuildHasher>(
Err(e) => return Err(e),
};

unlock_with_user_key(key, protected_private_key, protected_org_keys)
}

pub fn unlock_with_user_key<S: std::hash::BuildHasher>(
key: crate::locked::Keys,
protected_private_key: &str,
protected_org_keys: &std::collections::HashMap<String, String, S>,
) -> Result<(
crate::locked::Keys,
std::collections::HashMap<String, crate::locked::Keys>,
)> {
let protected_private_key =
crate::cipherstring::CipherString::new(protected_private_key)?;
let private_key =
Expand Down
18 changes: 18 additions & 0 deletions src/base64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,21 @@ pub fn decode<T: AsRef<[u8]>>(
) -> Result<Vec<u8>, base64::DecodeError> {
base64::engine::general_purpose::STANDARD.decode(input)
}

pub fn decode_url_safe_no_pad<T: AsRef<[u8]>>(
input: T,
) -> Result<Vec<u8>, base64::DecodeError> {
base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(input)
}

#[cfg(test)]
mod tests {
#[test]
fn decode_url_safe_no_pad() {
assert_eq!(
super::decode_url_safe_no_pad("eyJzdWIiOiJhYmMifQ").unwrap(),
br#"{"sub":"abc"}"#
);
assert!(super::decode_url_safe_no_pad("!!!").is_err());
}
}
56 changes: 56 additions & 0 deletions src/bin/rbw-agent/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,28 @@ async fn unlock_state(

let email = config_email().await?;

if rbw::config::Config::load_async().await?.biometric_unlock {
match biometric_unlock(
state.clone(),
db.access_token.as_deref(),
&protected_private_key,
&db.protected_org_keys,
)
.await
{
Ok((keys, org_keys)) => {
unlock_success(state, keys, org_keys).await?;
return Ok(());
}
Err(e) => {
log::warn!(
"biometric unlock failed, falling back to \
pinentry: {e:#}"
);
}
}
}

let mut err_msg = None;
for i in 1_u8..=3 {
let err = if i > 1 {
Expand Down Expand Up @@ -481,6 +503,40 @@ async fn unlock_success(
Ok(())
}

async fn biometric_unlock(
state: std::sync::Arc<tokio::sync::Mutex<crate::state::State>>,
access_token: Option<&str>,
protected_private_key: &str,
protected_org_keys: &std::collections::HashMap<String, String>,
) -> anyhow::Result<(
rbw::locked::Keys,
std::collections::HashMap<String, rbw::locked::Keys>,
)> {
let access_token = access_token.context("not logged in")?;
// reuse the already established channel if there is one; don't hold the
// state lock while waiting for the user to answer the biometric prompt
let cached = state.lock().await.bitwarden_desktop_channel.take();
let (res, channel) =
crate::bitwarden_desktop::unlock_user_key(cached, access_token)
.await;
state.lock().await.bitwarden_desktop_channel = channel;
let key = res?;
Ok(rbw::actions::unlock_with_user_key(
key,
protected_private_key,
protected_org_keys,
)?)
}

// used at agent startup to pre-establish the desktop app channel, so the
// first biometric unlock doesn't have to wait for it
pub async fn connect_bitwarden_desktop(
) -> anyhow::Result<crate::bitwarden_desktop::DesktopChannel> {
let db = load_db().await?;
let access_token = db.access_token.context("not logged in")?;
crate::bitwarden_desktop::connect_channel(&access_token).await
}

pub async fn lock(
sock: &mut crate::sock::Sock,
state: std::sync::Arc<tokio::sync::Mutex<crate::state::State>>,
Expand Down
Loading
Loading