Skip to content

Commit

Permalink
Add fido2 credential autofill view (#849)
Browse files Browse the repository at this point in the history
## 🎟️ Tracking

<!-- Paste the link to the Jira or GitHub issue or otherwise describe /
point to where this change is coming from. -->

## 📔 Objective

This PR adds a function on the Fido2 authenticator which returns all
available credentials as a View tailored specifically for integrating
with OS-level autofill APIs

## 📸 Screenshots

<!-- Required for any UI changes; delete if not applicable. Use fixed
width images for better display. -->

## ⏰ Reminders before review

- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Protected functional changes with optionality (feature flags)
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or
informed the documentation
  team

## 🦮 Reviewer guidelines

<!-- Suggested interactions but feel free to use (or not) as you desire!
-->

- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry
that's not quite a confirmed
  issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or
concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or
indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes
  • Loading branch information
coroiu authored Jun 18, 2024
1 parent 331c321 commit 547d0f2
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 21 deletions.
21 changes: 16 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/bitwarden-fido/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ chrono = { version = ">=0.4.26, <0.5", features = [
"serde",
], default-features = false }
coset = { version = "0.3.7" }
itertools = "0.13.0"
log = ">=0.4.18, <0.5"
p256 = { version = ">=0.13.2, <0.14" }
passkey = { git = "https://github.com/bitwarden/passkey-rs", rev = "c48c2ddfd6b884b2d754432576c66cb2b1985a3a" }
reqwest = { version = ">=0.12, <0.13", default-features = false }
schemars = { version = "0.8.21", features = ["uuid1", "chrono"] }
serde = { version = ">=1.0, <2.0", features = ["derive"] }
serde_json = ">=1.0.96, <2.0"
thiserror = ">=1.0.40, <2.0"
Expand Down
58 changes: 52 additions & 6 deletions crates/bitwarden-fido/src/authenticator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use std::sync::{Arc, Mutex};

use bitwarden_core::VaultLocked;
use bitwarden_crypto::{CryptoError, KeyContainer, KeyEncryptable};
use bitwarden_vault::{CipherError, CipherView, Fido2CredentialView};
use bitwarden_vault::{CipherError, CipherView};
use itertools::Itertools;
use log::error;
use passkey::{
authenticator::{Authenticator, DiscoverabilitySupport, StoreInfo, UIHint, UserCheck},
Expand Down Expand Up @@ -67,10 +68,30 @@ pub enum GetAssertionError {

#[derive(Debug, Error)]
pub enum SilentlyDiscoverCredentialsError {
#[error(transparent)]
CipherError(#[from] CipherError),
#[error(transparent)]
VaultLocked(#[from] VaultLocked),
#[error(transparent)]
InvalidGuid(#[from] InvalidGuid),
#[error(transparent)]
Fido2CallbackError(#[from] Fido2CallbackError),
#[error(transparent)]
FromCipherViewError(#[from] Fido2CredentialAutofillViewError),
}

#[derive(Debug, Error)]
pub enum CredentialsForAutofillError {
#[error(transparent)]
CipherError(#[from] CipherError),
#[error(transparent)]
VaultLocked(#[from] VaultLocked),
#[error(transparent)]
InvalidGuid(#[from] InvalidGuid),
#[error(transparent)]
Fido2CallbackError(#[from] Fido2CallbackError),
#[error(transparent)]
FromCipherViewError(#[from] Fido2CredentialAutofillViewError),
}

/// Temporary trait for solving a circular dependency. When moving `Client` to `bitwarden-core`
Expand Down Expand Up @@ -236,15 +257,40 @@ impl<'a> Fido2Authenticator<'a> {
pub async fn silently_discover_credentials(
&mut self,
rp_id: String,
) -> Result<Vec<Fido2CredentialView>, SilentlyDiscoverCredentialsError> {
) -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
let enc = self.client.get_encryption_settings()?;
let result = self.credential_store.find_credentials(None, rp_id).await?;

Ok(result
result
.into_iter()
.flat_map(|c| c.decrypt_fido2_credentials(&*enc))
.flatten()
.collect())
.map(
|cipher| -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher, &*enc)?)
},
)
.flatten_ok()
.collect()
}

/// Returns all Fido2 credentials that can be used for autofill, in a view
/// tailored for integration with OS autofill systems.
pub async fn credentials_for_autofill(
&mut self,
) -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
let enc = self.client.get_encryption_settings()?;
let all_credentials = self.credential_store.all_credentials().await?;

all_credentials
.into_iter()
.map(
|cipher| -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
Ok(Fido2CredentialAutofillView::from_cipher_view(
&cipher, &*enc,
)?)
},
)
.flatten_ok()
.collect()
}

pub(super) fn get_authenticator(
Expand Down
9 changes: 6 additions & 3 deletions crates/bitwarden-fido/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ use passkey::types::{ctap2::Aaguid, Passkey};

#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();
#[cfg(feature = "uniffi")]
mod uniffi_support;

mod authenticator;
mod client;
mod crypto;
mod traits;
mod types;
pub use authenticator::{
Fido2Authenticator, FidoEncryptionSettingStore, GetAssertionError, MakeCredentialError,
SilentlyDiscoverCredentialsError,
CredentialsForAutofillError, Fido2Authenticator, FidoEncryptionSettingStore, GetAssertionError,
MakeCredentialError, SilentlyDiscoverCredentialsError,
};
pub use client::{Fido2Client, Fido2ClientError};
pub use passkey::authenticator::UIHint;
Expand All @@ -27,7 +29,8 @@ pub use traits::{
};
pub use types::{
AuthenticatorAssertionResponse, AuthenticatorAttestationResponse, ClientData,
GetAssertionRequest, GetAssertionResult, MakeCredentialRequest, MakeCredentialResult, Options,
Fido2CredentialAutofillView, Fido2CredentialAutofillViewError, GetAssertionRequest,
GetAssertionResult, MakeCredentialRequest, MakeCredentialResult, Options,
PublicKeyCredentialAuthenticatorAssertionResponse,
PublicKeyCredentialAuthenticatorAttestationResponse, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden-fido/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub trait Fido2CredentialStore: Send + Sync {
rip_id: String,
) -> Result<Vec<CipherView>, Fido2CallbackError>;

async fn all_credentials(&self) -> Result<Vec<CipherView>, Fido2CallbackError>;

async fn save_credential(&self, cred: Cipher) -> Result<(), Fido2CallbackError>;
}

Expand Down
85 changes: 83 additions & 2 deletions crates/bitwarden-fido/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,89 @@
use bitwarden_crypto::KeyContainer;
use bitwarden_vault::{CipherError, CipherView};
use passkey::types::webauthn::UserVerificationRequirement;
use serde::Serialize;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use super::{get_enum_from_string_name, SelectedCredential, UnknownEnum, Verification};
use super::{
get_enum_from_string_name, string_to_guid_bytes, InvalidGuid, SelectedCredential, UnknownEnum,
Verification,
};

#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct Fido2CredentialAutofillView {
pub credential_id: Vec<u8>,
pub cipher_id: uuid::Uuid,
pub rp_id: String,
pub user_name_for_ui: Option<String>,
pub user_handle: Vec<u8>,
}

trait NoneWhitespace {
/// Convert only whitespace to None
fn none_whitespace(&self) -> Option<String>;
}

impl NoneWhitespace for String {
fn none_whitespace(&self) -> Option<String> {
match self.trim() {
"" => None,
s => Some(s.to_owned()),
}
}
}

impl NoneWhitespace for Option<String> {
fn none_whitespace(&self) -> Option<String> {
self.as_ref().and_then(|s| s.none_whitespace())
}
}

#[derive(Debug, Error)]
pub enum Fido2CredentialAutofillViewError {
#[error(
"Autofill credentials can only be created from existing ciphers that have a cipher id"
)]
MissingCipherId,

#[error(transparent)]
InvalidGuid(#[from] InvalidGuid),

#[error(transparent)]
CipherError(#[from] CipherError),
}

impl Fido2CredentialAutofillView {
pub fn from_cipher_view(
cipher: &CipherView,
enc: &dyn KeyContainer,
) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
let credentials = cipher.decrypt_fido2_credentials(enc)?;

credentials
.into_iter()
.filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
c.user_handle.map(|user_handle| {
Ok(Fido2CredentialAutofillView {
credential_id: string_to_guid_bytes(&c.credential_id)?,
cipher_id: cipher
.id
.ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?,
rp_id: c.rp_id.clone(),
user_handle,
user_name_for_ui: c
.user_name
.none_whitespace()
.or(c.user_display_name.none_whitespace())
.or(cipher.name.none_whitespace()),
})
})
})
.collect()
}
}

#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct PublicKeyCredentialRpEntity {
Expand Down
3 changes: 3 additions & 0 deletions crates/bitwarden-fido/src/uniffi_support.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
use uuid::Uuid;

uniffi::ffi_converter_forward!(Uuid, bitwarden_core::UniFfiTag, crate::UniFfiTag);
42 changes: 39 additions & 3 deletions crates/bitwarden-uniffi/src/platform/fido2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use bitwarden::{
error::Error,
platform::fido2::{
CheckUserOptions, ClientData, Fido2CallbackError as BitFido2CallbackError,
GetAssertionRequest, GetAssertionResult, MakeCredentialRequest, MakeCredentialResult,
Fido2CredentialAutofillView, GetAssertionRequest, GetAssertionResult,
MakeCredentialRequest, MakeCredentialResult,
PublicKeyCredentialAuthenticatorAssertionResponse,
PublicKeyCredentialAuthenticatorAttestationResponse, PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
},
vault::{Cipher, CipherView, Fido2CredentialNewView, Fido2CredentialView},
vault::{Cipher, CipherView, Fido2CredentialNewView},
};

use crate::{error::Result, Client};
Expand Down Expand Up @@ -42,6 +43,21 @@ impl ClientFido2 {
credential_store,
)))
}

pub fn decrypt_fido2_autofill_credentials(
self: Arc<Self>,
cipher_view: CipherView,
) -> Result<Vec<Fido2CredentialAutofillView>> {
let result = self
.0
.0
.platform()
.fido2()
.decrypt_fido2_autofill_credentials(cipher_view)
.map_err(Error::DecryptFido2AutofillCredentialsError)?;

Ok(result)
}
}

#[derive(uniffi::Object)]
Expand Down Expand Up @@ -87,7 +103,7 @@ impl ClientFido2Authenticator {
pub async fn silently_discover_credentials(
&self,
rp_id: String,
) -> Result<Vec<Fido2CredentialView>> {
) -> Result<Vec<Fido2CredentialAutofillView>> {
let platform = self.0 .0.platform();
let fido2 = platform.fido2();
let ui = UniffiTraitBridge(self.1.as_ref());
Expand All @@ -100,6 +116,20 @@ impl ClientFido2Authenticator {
.map_err(Error::SilentlyDiscoverCredentials)?;
Ok(result)
}

pub async fn credentials_for_autofill(&self) -> Result<Vec<Fido2CredentialAutofillView>> {
let platform = self.0 .0.platform();
let fido2 = platform.fido2();
let ui = UniffiTraitBridge(self.1.as_ref());
let cs = UniffiTraitBridge(self.2.as_ref());
let mut auth = fido2.create_authenticator(&ui, &cs);

let result = auth
.credentials_for_autofill()
.await
.map_err(Error::CredentialsForAutofillError)?;
Ok(result)
}
}

#[derive(uniffi::Object)]
Expand Down Expand Up @@ -216,6 +246,8 @@ pub trait Fido2CredentialStore: Send + Sync {
rip_id: String,
) -> Result<Vec<CipherView>, Fido2CallbackError>;

async fn all_credentials(&self) -> Result<Vec<CipherView>, Fido2CallbackError>;

async fn save_credential(&self, cred: Cipher) -> Result<(), Fido2CallbackError>;
}

Expand All @@ -240,6 +272,10 @@ impl bitwarden::platform::fido2::Fido2CredentialStore
.map_err(Into::into)
}

async fn all_credentials(&self) -> Result<Vec<CipherView>, BitFido2CallbackError> {
self.0.all_credentials().await.map_err(Into::into)
}

async fn save_credential(&self, cred: Cipher) -> Result<(), BitFido2CallbackError> {
self.0.save_credential(cred).await.map_err(Into::into)
}
Expand Down
Loading

0 comments on commit 547d0f2

Please sign in to comment.