From 57fa7faf8c7a94781b59a570395c31da417347c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Fri, 3 May 2024 17:35:57 +0200 Subject: [PATCH] Stubbed Passkey Uniffi API layer (#765) Created passkeys Uniffi API layer --- Cargo.lock | 314 ++++++++++++++++- crates/bitwarden-uniffi/Cargo.toml | 1 + crates/bitwarden-uniffi/src/error.rs | 17 + crates/bitwarden-uniffi/src/platform/fido2.rs | 256 ++++++++++++++ crates/bitwarden-uniffi/src/platform/mod.rs | 7 + crates/bitwarden/Cargo.toml | 2 + crates/bitwarden/src/error.rs | 14 + .../bitwarden/src/platform/client_platform.rs | 10 +- .../src/platform/fido2/authenticator.rs | 333 ++++++++++++++++++ crates/bitwarden/src/platform/fido2/client.rs | 315 +++++++++++++++++ crates/bitwarden/src/platform/fido2/mod.rs | 72 ++++ crates/bitwarden/src/platform/fido2/traits.rs | 50 +++ crates/bitwarden/src/platform/mod.rs | 2 + .../bitwarden/src/vault/cipher/attachment.rs | 4 +- crates/bitwarden/src/vault/cipher/card.rs | 4 +- crates/bitwarden/src/vault/cipher/cipher.rs | 4 +- crates/bitwarden/src/vault/cipher/field.rs | 4 +- crates/bitwarden/src/vault/cipher/identity.rs | 4 +- .../bitwarden/src/vault/cipher/local_data.rs | 4 +- crates/bitwarden/src/vault/cipher/login.rs | 72 +++- crates/bitwarden/src/vault/cipher/mod.rs | 1 + .../bitwarden/src/vault/password_history.rs | 4 +- 22 files changed, 1472 insertions(+), 22 deletions(-) create mode 100644 crates/bitwarden-uniffi/src/platform/fido2.rs create mode 100644 crates/bitwarden/src/platform/fido2/authenticator.rs create mode 100644 crates/bitwarden/src/platform/fido2/client.rs create mode 100644 crates/bitwarden/src/platform/fido2/mod.rs create mode 100644 crates/bitwarden/src/platform/fido2/traits.rs diff --git a/Cargo.lock b/Cargo.lock index 65e6c1e1b..ee8e80374 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,6 +242,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -337,6 +343,7 @@ checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" name = "bitwarden" version = "0.5.0" dependencies = [ + "async-trait", "base64 0.21.7", "bitwarden-api-api", "bitwarden-api-identity", @@ -347,6 +354,7 @@ dependencies = [ "getrandom", "hmac", "log", + "passkey", "rand", "rand_chacha", "reqwest", @@ -515,6 +523,7 @@ name = "bitwarden-uniffi" version = "0.1.0" dependencies = [ "async-lock", + "async-trait", "bitwarden", "bitwarden-crypto", "bitwarden-generators", @@ -727,6 +736,33 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -859,8 +895,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" dependencies = [ "crossterm 0.27.0", - "strum", - "strum_macros", + "strum 0.26.2", + "strum_macros 0.26.2", "unicode-width", ] @@ -947,6 +983,16 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "coset" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8aad850c1f86daa47e812913051eb5a26c4d9fb4242a89178bf99b946e4e3c" +dependencies = [ + "ciborium", + "ciborium-io", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1028,6 +1074,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1104,6 +1168,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "deadpool" version = "0.10.0" @@ -1212,12 +1282,49 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "either" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "base64ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "serde_json", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1319,6 +1426,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "flate2" version = "1.0.30" @@ -1502,6 +1619,17 @@ dependencies = [ "scroll", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.4.4" @@ -1521,6 +1649,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1758,6 +1896,7 @@ checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.5", + "serde", ] [[package]] @@ -2217,6 +2356,18 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking" version = "2.2.0" @@ -2246,6 +2397,71 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "passkey" +version = "0.3.1" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" +dependencies = [ + "passkey-authenticator", + "passkey-client", + "passkey-transports", + "passkey-types", +] + +[[package]] +name = "passkey-authenticator" +version = "0.3.0" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" +dependencies = [ + "async-trait", + "coset", + "log", + "p256", + "passkey-types", + "rand", +] + +[[package]] +name = "passkey-client" +version = "0.3.1" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" +dependencies = [ + "ciborium", + "coset", + "idna", + "passkey-authenticator", + "passkey-types", + "public-suffix", + "serde", + "serde_json", + "typeshare", + "url", +] + +[[package]] +name = "passkey-transports" +version = "0.1.0" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" + +[[package]] +name = "passkey-types" +version = "0.2.1" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" +dependencies = [ + "bitflags 2.5.0", + "ciborium", + "coset", + "data-encoding", + "getrandom", + "indexmap 2.2.6", + "rand", + "serde", + "serde_json", + "sha2", + "strum 0.25.0", + "typeshare", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -2393,6 +2609,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.81" @@ -2402,6 +2627,11 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "public-suffix" +version = "0.1.1" +source = "git+https://github.com/bitwarden/passkey-rs?rev=12da886102707f87ad97e499c857c0857ece0b85#12da886102707f87ad97e499c857c0857ece0b85" + [[package]] name = "pyo3" version = "0.20.3" @@ -2667,6 +2897,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.37" @@ -2919,6 +3159,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.10.0" @@ -2989,6 +3244,7 @@ version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -3050,6 +3306,16 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3204,12 +3470,34 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.3", +] + [[package]] name = "strum" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.60", +] + [[package]] name = "strum_macros" version = "0.26.2" @@ -3569,6 +3857,28 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typeshare" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f17399b76c2e743d58eac0635d7686e9c00f48cd4776f00695d9882a7d3187" +dependencies = [ + "chrono", + "serde", + "serde_json", + "typeshare-annotation", +] + +[[package]] +name = "typeshare-annotation" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" +dependencies = [ + "quote", + "syn 2.0.60", +] + [[package]] name = "unicase" version = "2.7.0" diff --git a/crates/bitwarden-uniffi/Cargo.toml b/crates/bitwarden-uniffi/Cargo.toml index 540415318..8b62ce5c5 100644 --- a/crates/bitwarden-uniffi/Cargo.toml +++ b/crates/bitwarden-uniffi/Cargo.toml @@ -19,6 +19,7 @@ bench = false [dependencies] async-lock = "3.3.0" +async-trait = "0.1.80" bitwarden = { workspace = true, features = ["mobile", "internal"] } bitwarden-crypto = { workspace = true, features = ["mobile"] } bitwarden-generators = { workspace = true, features = ["mobile"] } diff --git a/crates/bitwarden-uniffi/src/error.rs b/crates/bitwarden-uniffi/src/error.rs index 5eef9bbd5..cf07a07a1 100644 --- a/crates/bitwarden-uniffi/src/error.rs +++ b/crates/bitwarden-uniffi/src/error.rs @@ -14,6 +14,23 @@ impl From for BitwardenError { } } +impl From for bitwarden::error::Error { + fn from(val: BitwardenError) -> Self { + match val { + BitwardenError::E(e) => e, + } + } +} + +// Need to implement this From<> impl in order to handle unexpected callback errors. See the +// following page in the Uniffi user guide: +// +impl From for BitwardenError { + fn from(e: uniffi::UnexpectedUniFFICallbackError) -> Self { + Self::E(bitwarden::error::Error::UniffiCallback(e)) + } +} + impl Display for BitwardenError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/bitwarden-uniffi/src/platform/fido2.rs b/crates/bitwarden-uniffi/src/platform/fido2.rs new file mode 100644 index 000000000..25756bdab --- /dev/null +++ b/crates/bitwarden-uniffi/src/platform/fido2.rs @@ -0,0 +1,256 @@ +use std::sync::Arc; + +use bitwarden::{ + error::Result as BitResult, + platform::fido2::{ + CheckUserOptions, ClientData, GetAssertionRequest, GetAssertionResult, + MakeCredentialRequest, MakeCredentialResult, + PublicKeyCredentialAuthenticatorAssertionResponse, + PublicKeyCredentialAuthenticatorAttestationResponse, + }, + vault::{Cipher, CipherView, Fido2Credential, Fido2CredentialView}, +}; + +use crate::{error::Result, Client}; + +/// At the moment this is just a stub implementation that doesn't do anything. It's here to make +/// it possible to check the usability API on the native clients. +#[derive(uniffi::Object)] +pub struct ClientFido2(pub(crate) Arc); + +#[uniffi::export] +impl ClientFido2 { + pub fn authenticator( + self: Arc, + user_interface: Arc, + credential_store: Arc, + ) -> Arc { + Arc::new(ClientFido2Authenticator( + self.0.clone(), + user_interface, + credential_store, + )) + } + + pub fn client( + self: Arc, + user_interface: Arc, + credential_store: Arc, + ) -> Arc { + Arc::new(ClientFido2Client(ClientFido2Authenticator( + self.0.clone(), + user_interface, + credential_store, + ))) + } +} + +#[derive(uniffi::Object)] +pub struct ClientFido2Authenticator( + pub(crate) Arc, + pub(crate) Arc, + pub(crate) Arc, +); + +#[uniffi::export] +impl ClientFido2Authenticator { + pub async fn make_credential( + &self, + request: MakeCredentialRequest, + ) -> Result { + let mut client = self.0 .0.write().await; + + let mut platform = client.platform(); + let mut 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.make_credential(request).await?; + Ok(result) + } + + pub async fn get_assertion(&self, request: GetAssertionRequest) -> Result { + let mut client = self.0 .0.write().await; + + let mut platform = client.platform(); + let mut 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.get_assertion(request).await?; + Ok(result) + } + + pub async fn silently_discover_credentials( + &self, + rp_id: String, + ) -> Result> { + let mut client = self.0 .0.write().await; + + let mut platform = client.platform(); + let mut 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.silently_discover_credentials(rp_id).await?; + Ok(result) + } +} + +#[derive(uniffi::Object)] +pub struct ClientFido2Client(pub(crate) ClientFido2Authenticator); + +#[uniffi::export] +impl ClientFido2Client { + pub async fn register( + &self, + origin: String, + request: String, + client_data: ClientData, + ) -> Result { + let mut client = self.0 .0 .0.write().await; + + let mut platform = client.platform(); + let mut fido2 = platform.fido2(); + let ui = UniffiTraitBridge(self.0 .1.as_ref()); + let cs = UniffiTraitBridge(self.0 .2.as_ref()); + let mut client = fido2.create_client(&ui, &cs)?; + + let result = client.register(origin, request, client_data).await?; + Ok(result) + } + + pub async fn authenticate( + &self, + origin: String, + request: String, + client_data: ClientData, + ) -> Result { + let mut client = self.0 .0 .0.write().await; + + let mut platform = client.platform(); + let mut fido2 = platform.fido2(); + let ui = UniffiTraitBridge(self.0 .1.as_ref()); + let cs = UniffiTraitBridge(self.0 .2.as_ref()); + let mut client = fido2.create_client(&ui, &cs)?; + + let result = client.authenticate(origin, request, client_data).await?; + Ok(result) + } +} + +// Note that uniffi doesn't support external traits for now it seems, so we have to duplicate them +// here. + +#[allow(dead_code)] +#[derive(uniffi::Record)] +pub struct CheckUserResult { + user_present: bool, + user_verified: bool, +} + +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait UserInterface: Send + Sync { + async fn check_user( + &self, + options: CheckUserOptions, + credential: Option, + ) -> Result; + async fn pick_credential_for_authentication( + &self, + available_credentials: Vec, + ) -> Result; + async fn pick_credential_for_creation( + &self, + available_credentials: Vec, + new_credential: Fido2Credential, + ) -> Result; +} + +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait CredentialStore: Send + Sync { + async fn find_credentials( + &self, + ids: Option>>, + rip_id: String, + ) -> Result>; + + async fn save_credential(&self, cred: Cipher) -> Result<()>; +} + +// Because uniffi doesn't support external traits, we have to make a copy of the trait here. +// Ideally we'd want to implement the original trait for every item that implements our local copy, +// but the orphan rules don't allow us to blanket implement an external trait. So we have to wrap +// the trait in a newtype and implement the trait for the newtype. +struct UniffiTraitBridge(T); + +#[async_trait::async_trait] +impl bitwarden::platform::fido2::CredentialStore for UniffiTraitBridge<&dyn CredentialStore> { + async fn find_credentials( + &self, + ids: Option>>, + rip_id: String, + ) -> BitResult> { + self.0 + .find_credentials(ids, rip_id) + .await + .map_err(Into::into) + } + + async fn save_credential(&self, cred: Cipher) -> BitResult<()> { + self.0.save_credential(cred).await.map_err(Into::into) + } +} + +// Uniffi seems to have trouble generating code for Android when a local trait returns a type from +// an external crate. If the type is small we can just copy it over and convert back and forth, but +// Cipher is too big for that to be practical. So we wrap it in a newtype, which is local to the +// trait and so we can sidestep the Uniffi issue +#[derive(uniffi::Record)] +pub struct CipherViewWrapper { + cipher: CipherView, +} + +#[async_trait::async_trait] +impl bitwarden::platform::fido2::UserInterface for UniffiTraitBridge<&dyn UserInterface> { + async fn check_user( + &self, + options: CheckUserOptions, + credential: Option, + ) -> BitResult { + self.0 + .check_user(options, credential) + .await + .map(|r| bitwarden::platform::fido2::CheckUserResult { + user_present: r.user_present, + user_verified: r.user_verified, + }) + .map_err(Into::into) + } + async fn pick_credential_for_authentication( + &self, + available_credentials: Vec, + ) -> BitResult { + self.0 + .pick_credential_for_authentication(available_credentials) + .await + .map(|v| v.cipher) + .map_err(Into::into) + } + async fn pick_credential_for_creation( + &self, + available_credentials: Vec, + new_credential: Fido2Credential, + ) -> BitResult { + self.0 + .pick_credential_for_creation(available_credentials, new_credential) + .await + .map(|v| v.cipher) + .map_err(Into::into) + } +} diff --git a/crates/bitwarden-uniffi/src/platform/mod.rs b/crates/bitwarden-uniffi/src/platform/mod.rs index 33b14d345..458306676 100644 --- a/crates/bitwarden-uniffi/src/platform/mod.rs +++ b/crates/bitwarden-uniffi/src/platform/mod.rs @@ -4,6 +4,8 @@ use bitwarden::platform::FingerprintRequest; use crate::{error::Result, Client}; +mod fido2; + #[derive(uniffi::Object)] pub struct ClientPlatform(pub(crate) Arc); @@ -37,4 +39,9 @@ impl ClientPlatform { self.0 .0.write().await.load_flags(flags); Ok(()) } + + /// FIDO2 operations + pub fn fido2(self: Arc) -> Arc { + Arc::new(fido2::ClientFido2(self.0.clone())) + } } diff --git a/crates/bitwarden/Cargo.toml b/crates/bitwarden/Cargo.toml index 3e92981f0..0289e5925 100644 --- a/crates/bitwarden/Cargo.toml +++ b/crates/bitwarden/Cargo.toml @@ -31,6 +31,7 @@ mobile = [ wasm-bindgen = ["chrono/wasmbind"] [dependencies] +async-trait = ">=0.1.80, <0.2" base64 = ">=0.21.2, <0.22" bitwarden-api-api = { workspace = true } bitwarden-api-identity = { workspace = true } @@ -46,6 +47,7 @@ chrono = { version = ">=0.4.26, <0.5", features = [ getrandom = { version = ">=0.2.9, <0.3", features = ["js"] } hmac = ">=0.12.1, <0.13" log = ">=0.4.18, <0.5" +passkey = { git = "https://github.com/bitwarden/passkey-rs", rev = "12da886102707f87ad97e499c857c0857ece0b85" } rand = ">=0.8.5, <0.9" reqwest = { version = ">=0.12, <0.13", features = [ "http2", diff --git a/crates/bitwarden/src/error.rs b/crates/bitwarden/src/error.rs index 59fae58d2..fdae93fac 100644 --- a/crates/bitwarden/src/error.rs +++ b/crates/bitwarden/src/error.rs @@ -8,6 +8,7 @@ use bitwarden_api_identity::apis::Error as IdentityError; use bitwarden_exporters::ExportError; #[cfg(feature = "internal")] use bitwarden_generators::{PassphraseError, PasswordError, UsernameError}; +use passkey::client::WebauthnError; use reqwest::StatusCode; use thiserror::Error; @@ -68,10 +69,23 @@ pub enum Error { #[error(transparent)] ExportError(#[from] ExportError), + #[error("Webauthn error: {0:?}")] + WebauthnError(passkey::client::WebauthnError), + + #[cfg(feature = "mobile")] + #[error("Uniffi callback error: {0}")] + UniffiCallback(#[from] uniffi::UnexpectedUniFFICallbackError), + #[error("Internal error: {0}")] Internal(Cow<'static, str>), } +impl From for Error { + fn from(e: WebauthnError) -> Self { + Self::WebauthnError(e) + } +} + impl From for Error { fn from(s: String) -> Self { Self::Internal(s.into()) diff --git a/crates/bitwarden/src/platform/client_platform.rs b/crates/bitwarden/src/platform/client_platform.rs index ada8b92b8..91c15c358 100644 --- a/crates/bitwarden/src/platform/client_platform.rs +++ b/crates/bitwarden/src/platform/client_platform.rs @@ -1,6 +1,6 @@ use super::{ generate_fingerprint::{generate_fingerprint, generate_user_fingerprint}, - FingerprintRequest, FingerprintResponse, + ClientFido2, FingerprintRequest, FingerprintResponse, }; use crate::{error::Result, Client}; @@ -16,6 +16,14 @@ impl<'a> ClientPlatform<'a> { pub fn user_fingerprint(self, fingerprint_material: String) -> Result { generate_user_fingerprint(self.client, fingerprint_material) } + + /// At the moment this is just a stub implementation that doesn't do anything. It's here to make + /// it possible to check the usability API on the native clients. + pub fn fido2(&'a mut self) -> ClientFido2<'a> { + ClientFido2 { + client: self.client, + } + } } impl<'a> Client { diff --git a/crates/bitwarden/src/platform/fido2/authenticator.rs b/crates/bitwarden/src/platform/fido2/authenticator.rs new file mode 100644 index 000000000..9f35a682d --- /dev/null +++ b/crates/bitwarden/src/platform/fido2/authenticator.rs @@ -0,0 +1,333 @@ +#![allow(dead_code, unused_mut, unused_imports, unused_variables)] + +use bitwarden_crypto::{EncString, KeyEncryptable, SensitiveString}; +use chrono::DateTime; +use passkey::{ + authenticator::{Authenticator, UserCheck}, + types::{ + ctap2::{make_credential::Request, Aaguid, Ctap2Error, StatusCode}, + Passkey, + }, +}; +use uuid::Uuid; + +use super::{CredentialStore, SelectedCredential, UserInterface}; +use crate::{ + error::{Error, Result}, + vault::{ + login::{Fido2CredentialView, LoginView}, + CipherView, Fido2Credential, + }, + Client, +}; + +pub struct Fido2Authenticator<'a> { + pub(crate) client: &'a mut Client, + pub(crate) user_interface: &'a dyn UserInterface, + pub(crate) credential_store: &'a dyn CredentialStore, +} + +impl<'a> Fido2Authenticator<'a> { + pub async fn make_credential( + &mut self, + request: MakeCredentialRequest, + ) -> Result { + // TODO: Placeholder value + let my_aaguid = Aaguid::new_empty(); + + let mut authenticator = Authenticator::new( + my_aaguid, + self.to_credential_store(), + self.to_user_interface(), + ); + + /*let response = authenticator + .make_credential(Request { + client_data_hash: request.client_data_hash.into(), + rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity { + id: request.rp.id, + name: request.rp.name, + }, + user: passkey::types::webauthn::PublicKeyCredentialUserEntity { + id: request.user.id.into(), + display_name: request.user.display_name, + name: request.user.name, + }, + pub_key_cred_params: request + .pub_key_cred_params + .into_iter() + .map( + |x| passkey::types::webauthn::PublicKeyCredentialParameters { + ty: todo!(), + alg: todo!(), + }, + ) + .collect(), + exclude_list: request + .exclude_list + .map(|x: Vec| { + x.into_iter() + .map( + |x| passkey::types::webauthn::PublicKeyCredentialDescriptor { + ty: todo!(), + id: todo!(), + transports: None, + }, + ) + .collect() + }), + extensions: None, // TODO: request.extensions, + options: passkey::types::ctap2::make_credential::Options { + rk: true, + up: true, + uv: true, + }, + pin_auth: None, + pin_protocol: None, + }) + .await; + + let response = match response { + Ok(x) => x, + Err(e) => return Err(format!("make_credential error: {e:?}").into()), + }; + + Ok(MakeCredentialResult { + credential_id: response + .auth_data + .attested_credential_data + .expect("Missing attested_credential_data") + .credential_id() + .to_vec(), + })*/ + + Ok(MakeCredentialResult { + credential_id: vec![], + }) + } + + pub async fn get_assertion( + &mut self, + request: GetAssertionRequest, + ) -> Result { + let enc = self.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + Ok(GetAssertionResult { + credential_id: vec![], + authenticator_data: vec![], + signature: vec![], + user_handle: vec![], + selected_credential: SelectedCredential { + cipher: CipherView { + id: Some(Uuid::new_v4()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: SensitiveString::new(Box::new("".to_string())), + notes: Some(SensitiveString::new(Box::new("".to_string()))), + r#type: crate::vault::CipherType::Login, + login: Some(LoginView { + username: None, + password: None, + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: Some(vec![]), + }), + identity: None, + card: None, + secure_note: None, + favorite: false, + reprompt: crate::vault::CipherRepromptType::None, + organization_use_totp: true, + edit: true, + view_password: true, + local_data: None, + attachments: Some(vec![]), + fields: Some(vec![]), + password_history: Some(vec![]), + creation_date: chrono::offset::Utc::now(), + deleted_date: None, + revision_date: chrono::offset::Utc::now(), + }, + credential: Fido2Credential { + credential_id: "".to_owned().encrypt_with_key(key)?, + key_type: "".to_owned().encrypt_with_key(key)?, + key_algorithm: "".to_owned().encrypt_with_key(key)?, + key_curve: "".to_owned().encrypt_with_key(key)?, + key_value: "".to_owned().encrypt_with_key(key)?, + rp_id: "".to_owned().encrypt_with_key(key)?, + user_handle: Some("".to_owned().encrypt_with_key(key)?), + user_name: Some("".to_owned().encrypt_with_key(key)?), + counter: "".to_owned().encrypt_with_key(key)?, + rp_name: Some("".to_owned().encrypt_with_key(key)?), + user_display_name: Some("".to_owned().encrypt_with_key(key)?), + discoverable: "".to_owned().encrypt_with_key(key)?, + creation_date: chrono::offset::Utc::now(), + }, + }, + }) + } + + // TODO: Fido2CredentialView contains all the fields, maybe we need a Fido2CredentialListView? + pub async fn silently_discover_credentials( + &mut self, + rp_id: String, + ) -> Result> { + Ok(vec![]) + } + + pub(crate) fn to_user_interface(&'a self) -> UserInterfaceImpl<'_> { + UserInterfaceImpl { + authenticator: self, + } + } + pub(crate) fn to_credential_store(&'a self) -> CredentialStoreImpl<'_> { + CredentialStoreImpl { + authenticator: self, + } + } +} + +pub(crate) struct CredentialStoreImpl<'a> { + authenticator: &'a Fido2Authenticator<'a>, +} +pub(crate) struct UserInterfaceImpl<'a> { + authenticator: &'a Fido2Authenticator<'a>, +} + +#[async_trait::async_trait] +impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { + type PasskeyItem = CipherView; + + async fn find_credentials( + &self, + ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>, + rp_id: &str, + ) -> Result, StatusCode> { + Ok(vec![]) + } + + async fn save_credential( + &mut self, + cred: Passkey, + user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity, + rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity, + ) -> Result<(), StatusCode> { + Ok(()) + } + + async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> { + Ok(()) + } +} + +#[async_trait::async_trait] +impl passkey::authenticator::UserValidationMethod for UserInterfaceImpl<'_> { + type PasskeyItem = CipherView; + + async fn check_user( + &self, + credential: Option, + presence: bool, + verification: bool, + ) -> Result { + Ok(UserCheck { + presence, + verification, + }) + } + + fn is_presence_enabled(&self) -> bool { + true + } + + fn is_verification_enabled(&self) -> Option { + Some(true) + } +} + +// What type do we need this to be? We probably can't use Serialize over the FFI boundary +pub type Extensions = Option>; + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialRpEntity { + pub id: String, + pub name: Option, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialUserEntity { + pub id: Vec, + pub display_name: String, + pub name: String, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialParameters { + pub ty: String, + pub alg: i64, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialDescriptor { + pub ty: i64, + pub id: Vec, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct MakeCredentialRequest { + client_data_hash: Vec, + rp: PublicKeyCredentialRpEntity, + user: PublicKeyCredentialUserEntity, + pub_key_cred_params: Vec, + exclude_list: Option>, + require_resident_key: bool, + extensions: Extensions, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct MakeCredentialResult { + // TODO + // authenticator_data: Vec, + // attested_credential_data: Vec, + credential_id: Vec, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct GetAssertionRequest { + rp_id: String, + client_data_hash: Vec, + allow_list: Option>, + options: Options, + extensions: Extensions, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct Options { + rk: bool, + uv: UV, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum UV { + Discouraged, + Preferred, + Required, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct GetAssertionResult { + credential_id: Vec, + authenticator_data: Vec, + signature: Vec, + user_handle: Vec, + /** + * SDK IMPL NOTE: This is not part of the spec and is not returned by passkey-rs. + * The SDK needs to add this after the response from passkey-rs is received. + */ + selected_credential: SelectedCredential, +} diff --git a/crates/bitwarden/src/platform/fido2/client.rs b/crates/bitwarden/src/platform/fido2/client.rs new file mode 100644 index 000000000..a175e17bb --- /dev/null +++ b/crates/bitwarden/src/platform/fido2/client.rs @@ -0,0 +1,315 @@ +#![allow(dead_code, unused_variables)] + +use std::collections::HashMap; + +use bitwarden_crypto::{KeyEncryptable, SensitiveString}; +use passkey::{authenticator::Authenticator, types::ctap2::Aaguid}; +use reqwest::Url; +use serde::Serialize; +use uuid::Uuid; + +use super::{Fido2Authenticator, SelectedCredential}; +use crate::{ + error::{Error, Result}, + vault::{login::LoginView, CipherView, Fido2Credential}, +}; + +pub struct Fido2Client<'a> { + pub(crate) authenticator: Fido2Authenticator<'a>, +} + +impl<'a> Fido2Client<'a> { + pub async fn register( + &mut self, + origin: String, + request: String, + client_data: ClientData, + ) -> Result { + // TODO: Placeholder value + let my_aaguid = Aaguid::new_empty(); + + let authenticator = Authenticator::new( + my_aaguid, + self.authenticator.to_credential_store(), + self.authenticator.to_user_interface(), + ); + let mut client = passkey::client::Client::new(authenticator); + + let origin = Url::parse(&origin).expect("Invalid URL"); + + let result = client + .register(&origin, serde_json::from_str(&request)?, client_data) + .await?; + + /*Ok(PublicKeyCredentialAuthenticatorAttestationResponse { + id: result.id, + raw_id: result.raw_id.into(), + ty: "public-key".to_string(), + authenticator_attachment: todo!(), + client_extension_results: todo!(), + response: AuthenticatorAttestationResponse { + client_data_json: result.response.client_data_json.into(), + authenticator_data: result.response.authenticator_data.into(), + public_key: result.response.public_key.map(|x| x.into()), + public_key_algorithm: result.response.public_key_algorithm, + attestation_object: result.response.attestation_object.into(), + transports: todo!(), + }, + selected_credential: SelectedCredential { + cipher: todo!(), + credential: todo!(), + }, + })*/ + let enc = self.authenticator.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + Ok(PublicKeyCredentialAuthenticatorAttestationResponse { + id: String::new(), + raw_id: vec![], + ty: "public-key".to_string(), + authenticator_attachment: String::new(), + client_extension_results: HashMap::new(), + response: AuthenticatorAttestationResponse { + client_data_json: vec![], + authenticator_data: vec![], + public_key: None, + public_key_algorithm: 0, + attestation_object: vec![], + transports: None, + }, + selected_credential: SelectedCredential { + cipher: CipherView { + id: Some(Uuid::new_v4()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: SensitiveString::new(Box::new("".to_string())), + notes: Some(SensitiveString::new(Box::new("".to_string()))), + r#type: crate::vault::CipherType::Login, + login: Some(LoginView { + username: None, + password: None, + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: Some(vec![]), + }), + identity: None, + card: None, + secure_note: None, + favorite: false, + reprompt: crate::vault::CipherRepromptType::None, + organization_use_totp: true, + edit: true, + view_password: true, + local_data: None, + attachments: Some(vec![]), + fields: Some(vec![]), + password_history: Some(vec![]), + creation_date: chrono::offset::Utc::now(), + deleted_date: None, + revision_date: chrono::offset::Utc::now(), + }, + credential: Fido2Credential { + credential_id: "".to_owned().encrypt_with_key(key)?, + key_type: "".to_owned().encrypt_with_key(key)?, + key_algorithm: "".to_owned().encrypt_with_key(key)?, + key_curve: "".to_owned().encrypt_with_key(key)?, + key_value: "".to_owned().encrypt_with_key(key)?, + rp_id: "".to_owned().encrypt_with_key(key)?, + user_handle: Some("".to_owned().encrypt_with_key(key)?), + user_name: Some("".to_owned().encrypt_with_key(key)?), + counter: "".to_owned().encrypt_with_key(key)?, + rp_name: Some("".to_owned().encrypt_with_key(key)?), + user_display_name: Some("".to_owned().encrypt_with_key(key)?), + discoverable: "".to_owned().encrypt_with_key(key)?, + creation_date: chrono::offset::Utc::now(), + }, + }, + }) + } + pub async fn authenticate( + &mut self, + origin: String, + request: String, + client_data: ClientData, + ) -> Result { + // TODO: Placeholder value + let my_aaguid = Aaguid::new_empty(); + + let authenticator = Authenticator::new( + my_aaguid, + self.authenticator.to_credential_store(), + self.authenticator.to_user_interface(), + ); + let mut client = passkey::client::Client::new(authenticator); + + let origin = Url::parse(&origin).expect("Invalid URL"); + + let result = client + .authenticate(&origin, serde_json::from_str(&request)?, client_data) + .await?; + + /*Ok(PublicKeyCredentialAuthenticatorAssertionResponse { + id: result.id, + raw_id: result.raw_id.into(), + ty: "public-key".to_string(), + authenticator_attachment: todo!(), + client_extension_results: todo!(), + response: AuthenticatorAssertionResponse { + client_data_json: result.response.client_data_json.into(), + authenticator_data: result.response.authenticator_data.into(), + signature: result.response.signature.into(), + user_handle: result.response.user_handle.unwrap_or_default().into(), + }, + selected_credential: SelectedCredential { + cipher: todo!(), + credential: todo!(), + }, + })*/ + + let enc = self.authenticator.client.get_encryption_settings()?; + let key = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + Ok(PublicKeyCredentialAuthenticatorAssertionResponse { + id: String::new(), + raw_id: vec![], + ty: "public-key".to_string(), + authenticator_attachment: String::new(), + client_extension_results: HashMap::new(), + response: AuthenticatorAssertionResponse { + client_data_json: vec![], + authenticator_data: vec![], + signature: vec![], + user_handle: vec![], + }, + selected_credential: SelectedCredential { + cipher: CipherView { + id: Some(Uuid::new_v4()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: SensitiveString::new(Box::new("".to_string())), + notes: Some(SensitiveString::new(Box::new("".to_string()))), + r#type: crate::vault::CipherType::Login, + login: Some(LoginView { + username: None, + password: None, + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: Some(vec![]), + }), + identity: None, + card: None, + secure_note: None, + favorite: false, + reprompt: crate::vault::CipherRepromptType::None, + organization_use_totp: true, + edit: true, + view_password: true, + local_data: None, + attachments: Some(vec![]), + fields: Some(vec![]), + password_history: Some(vec![]), + creation_date: chrono::offset::Utc::now(), + deleted_date: None, + revision_date: chrono::offset::Utc::now(), + }, + credential: Fido2Credential { + credential_id: "".to_owned().encrypt_with_key(key)?, + key_type: "".to_owned().encrypt_with_key(key)?, + key_algorithm: "".to_owned().encrypt_with_key(key)?, + key_curve: "".to_owned().encrypt_with_key(key)?, + key_value: "".to_owned().encrypt_with_key(key)?, + rp_id: "".to_owned().encrypt_with_key(key)?, + user_handle: Some("".to_owned().encrypt_with_key(key)?), + user_name: Some("".to_owned().encrypt_with_key(key)?), + counter: "".to_owned().encrypt_with_key(key)?, + rp_name: Some("".to_owned().encrypt_with_key(key)?), + user_display_name: Some("".to_owned().encrypt_with_key(key)?), + discoverable: "".to_owned().encrypt_with_key(key)?, + creation_date: chrono::offset::Utc::now(), + }, + }, + }) + } +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum ClientData { + DefaultWithExtraData { android_package_name: String }, + DefaultWithCustomHash { hash: Vec }, +} + +#[derive(Serialize, Clone)] +struct AndroidClientData { + android_package_name: String, +} + +// TODO: I'm implementing this to convert from a basic enum into the generic +// passkey::client::ClientData Not fully sure that it's correct to return None for extra_client_data +// instead of () +impl passkey::client::ClientData> for ClientData { + fn extra_client_data(&self) -> Option { + match self { + ClientData::DefaultWithExtraData { + android_package_name, + } => Some(AndroidClientData { + android_package_name: android_package_name.clone(), + }), + ClientData::DefaultWithCustomHash { .. } => None, + } + } + + fn client_data_hash(&self) -> Option> { + match self { + ClientData::DefaultWithExtraData { .. } => None, + ClientData::DefaultWithCustomHash { hash } => Some(hash.clone()), + } + } +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialAuthenticatorAttestationResponse { + id: String, + raw_id: Vec, + ty: String, + authenticator_attachment: String, + client_extension_results: HashMap, + response: AuthenticatorAttestationResponse, + selected_credential: SelectedCredential, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct AuthenticatorAttestationResponse { + client_data_json: Vec, + authenticator_data: Vec, + public_key: Option>, + public_key_algorithm: i64, + attestation_object: Vec, + transports: Option>, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct PublicKeyCredentialAuthenticatorAssertionResponse { + id: String, + raw_id: Vec, + ty: String, + authenticator_attachment: String, + client_extension_results: HashMap, + response: AuthenticatorAssertionResponse, + selected_credential: SelectedCredential, +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct AuthenticatorAssertionResponse { + client_data_json: Vec, + authenticator_data: Vec, + signature: Vec, + user_handle: Vec, +} diff --git a/crates/bitwarden/src/platform/fido2/mod.rs b/crates/bitwarden/src/platform/fido2/mod.rs new file mode 100644 index 000000000..38dc965cb --- /dev/null +++ b/crates/bitwarden/src/platform/fido2/mod.rs @@ -0,0 +1,72 @@ +use crate::{ + error::Result, + vault::{login::Fido2Credential, CipherView}, + Client, +}; + +mod authenticator; +mod client; +mod traits; + +pub use authenticator::{ + Fido2Authenticator, GetAssertionRequest, GetAssertionResult, MakeCredentialRequest, + MakeCredentialResult, +}; +pub use client::{ + AuthenticatorAssertionResponse, AuthenticatorAttestationResponse, ClientData, Fido2Client, + PublicKeyCredentialAuthenticatorAssertionResponse, + PublicKeyCredentialAuthenticatorAttestationResponse, +}; +use passkey::types::Passkey; +pub use traits::{CheckUserOptions, CheckUserResult, CredentialStore, UserInterface, Verification}; + +pub struct ClientFido2<'a> { + #[allow(dead_code)] + pub(crate) client: &'a mut Client, +} + +impl<'a> ClientFido2<'a> { + pub fn create_authenticator( + &'a mut self, + + user_interface: &'a dyn UserInterface, + credential_store: &'a dyn CredentialStore, + ) -> Result> { + Ok(Fido2Authenticator { + client: self.client, + user_interface, + credential_store, + }) + } + + pub fn create_client( + &'a mut self, + + user_interface: &'a dyn UserInterface, + credential_store: &'a dyn CredentialStore, + ) -> Result> { + Ok(Fido2Client { + authenticator: self.create_authenticator(user_interface, credential_store)?, + }) + } +} + +#[allow(dead_code)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct SelectedCredential { + cipher: CipherView, + credential: Fido2Credential, +} + +impl TryFrom for Passkey { + type Error = crate::error::Error; + + fn try_from(value: CipherView) -> std::prelude::v1::Result { + let _creds = value + .login + .and_then(|l| l.fido2_credentials) + .ok_or("No Fido2Credential")?; + + todo!("We have more than one credential, we need to pick one?") + } +} diff --git a/crates/bitwarden/src/platform/fido2/traits.rs b/crates/bitwarden/src/platform/fido2/traits.rs new file mode 100644 index 000000000..e7429e0a5 --- /dev/null +++ b/crates/bitwarden/src/platform/fido2/traits.rs @@ -0,0 +1,50 @@ +use crate::{ + error::Result, + vault::{login::Fido2Credential, Cipher, CipherView}, +}; + +#[async_trait::async_trait] +pub trait UserInterface: Send + Sync { + async fn check_user( + &self, + options: CheckUserOptions, + credential: Option, + ) -> Result; + async fn pick_credential_for_authentication( + &self, + available_credentials: Vec, + ) -> Result; + async fn pick_credential_for_creation( + &self, + available_credentials: Vec, + new_credential: Fido2Credential, + ) -> Result; +} + +#[async_trait::async_trait] +pub trait CredentialStore: Send + Sync { + async fn find_credentials( + &self, + ids: Option>>, + rip_id: String, + ) -> Result>; + + async fn save_credential(&self, cred: Cipher) -> Result<()>; +} + +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum CheckUserOptions { + RequirePresence(bool), + RequireVerification(Verification), +} +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum Verification { + Discouraged, + Preferred, + Required, +} + +pub struct CheckUserResult { + pub user_present: bool, + pub user_verified: bool, +} diff --git a/crates/bitwarden/src/platform/mod.rs b/crates/bitwarden/src/platform/mod.rs index b78cb27d4..a51b94b94 100644 --- a/crates/bitwarden/src/platform/mod.rs +++ b/crates/bitwarden/src/platform/mod.rs @@ -1,10 +1,12 @@ pub mod client_platform; mod domain; +pub mod fido2; mod generate_fingerprint; mod get_user_api_key; mod secret_verification_request; mod sync; +pub use fido2::{ClientFido2, Fido2Authenticator, Fido2Client}; pub use generate_fingerprint::{FingerprintRequest, FingerprintResponse}; pub(crate) use get_user_api_key::get_user_api_key; pub use get_user_api_key::UserApiKeyResponse; diff --git a/crates/bitwarden/src/vault/cipher/attachment.rs b/crates/bitwarden/src/vault/cipher/attachment.rs index 9c0fa6426..7dbec6a3f 100644 --- a/crates/bitwarden/src/vault/cipher/attachment.rs +++ b/crates/bitwarden/src/vault/cipher/attachment.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use super::Cipher; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Attachment { @@ -21,7 +21,7 @@ pub struct Attachment { pub key: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct AttachmentView { diff --git a/crates/bitwarden/src/vault/cipher/card.rs b/crates/bitwarden/src/vault/cipher/card.rs index be1574905..3635cb2ec 100644 --- a/crates/bitwarden/src/vault/cipher/card.rs +++ b/crates/bitwarden/src/vault/cipher/card.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Card { @@ -19,7 +19,7 @@ pub struct Card { pub number: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct CardView { diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index fc4972107..5e69063c0 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -37,7 +37,7 @@ pub enum CipherRepromptType { Password = 1, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Cipher { @@ -75,7 +75,7 @@ pub struct Cipher { pub revision_date: DateTime, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct CipherView { diff --git a/crates/bitwarden/src/vault/cipher/field.rs b/crates/bitwarden/src/vault/cipher/field.rs index 48b31b22f..04837bf9e 100644 --- a/crates/bitwarden/src/vault/cipher/field.rs +++ b/crates/bitwarden/src/vault/cipher/field.rs @@ -19,7 +19,7 @@ pub enum FieldType { Linked = 3, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Field { @@ -30,7 +30,7 @@ pub struct Field { linked_id: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct FieldView { diff --git a/crates/bitwarden/src/vault/cipher/identity.rs b/crates/bitwarden/src/vault/cipher/identity.rs index efb247a09..f131fd597 100644 --- a/crates/bitwarden/src/vault/cipher/identity.rs +++ b/crates/bitwarden/src/vault/cipher/identity.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Identity { @@ -31,7 +31,7 @@ pub struct Identity { pub license_number: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct IdentityView { diff --git a/crates/bitwarden/src/vault/cipher/local_data.rs b/crates/bitwarden/src/vault/cipher/local_data.rs index 6a85512c2..fd5897066 100644 --- a/crates/bitwarden/src/vault/cipher/local_data.rs +++ b/crates/bitwarden/src/vault/cipher/local_data.rs @@ -2,7 +2,7 @@ use bitwarden_crypto::{CryptoError, KeyDecryptable, KeyEncryptable, SymmetricCry use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LocalData { @@ -10,7 +10,7 @@ pub struct LocalData { last_launched: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LocalDataView { diff --git a/crates/bitwarden/src/vault/cipher/login.rs b/crates/bitwarden/src/vault/cipher/login.rs index 456d6223b..fd4e291bf 100644 --- a/crates/bitwarden/src/vault/cipher/login.rs +++ b/crates/bitwarden/src/vault/cipher/login.rs @@ -2,7 +2,7 @@ use base64::engine::general_purpose::STANDARD; use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel}; use bitwarden_crypto::{ CryptoError, DecryptedString, EncString, KeyDecryptable, KeyEncryptable, Sensitive, - SensitiveVec, SymmetricCryptoKey, + SensitiveString, SensitiveVec, SymmetricCryptoKey, }; use chrono::{DateTime, Utc}; use hmac::digest::generic_array::GenericArray; @@ -26,7 +26,7 @@ pub enum UriMatchType { Never = 5, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LoginUri { @@ -35,7 +35,7 @@ pub struct LoginUri { pub uri_checksum: Option, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LoginUriView { @@ -97,7 +97,26 @@ pub struct Fido2Credential { pub creation_date: DateTime, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct Fido2CredentialView { + pub credential_id: SensitiveString, + pub key_type: SensitiveString, + pub key_algorithm: SensitiveString, + pub key_curve: SensitiveString, + pub key_value: SensitiveString, + pub rp_id: SensitiveString, + pub user_handle: Option, + pub user_name: Option, + pub counter: SensitiveString, + pub rp_name: Option, + pub user_display_name: Option, + pub discoverable: SensitiveString, + pub creation_date: DateTime, +} + +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct Login { @@ -112,7 +131,7 @@ pub struct Login { pub fido2_credentials: Option>, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct LoginView { @@ -176,6 +195,49 @@ impl KeyDecryptable for Login { } } +impl KeyEncryptable for Fido2CredentialView { + fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result { + Ok(Fido2Credential { + credential_id: self.credential_id.encrypt_with_key(key)?, + key_type: self.key_type.encrypt_with_key(key)?, + key_algorithm: self.key_algorithm.encrypt_with_key(key)?, + key_curve: self.key_curve.encrypt_with_key(key)?, + key_value: self.key_value.encrypt_with_key(key)?, + rp_id: self.rp_id.encrypt_with_key(key)?, + user_handle: self.user_handle.encrypt_with_key(key)?, + user_name: self.user_name.encrypt_with_key(key)?, + counter: self.counter.encrypt_with_key(key)?, + rp_name: self.rp_name.encrypt_with_key(key)?, + user_display_name: self.user_display_name.encrypt_with_key(key)?, + discoverable: self.discoverable.encrypt_with_key(key)?, + creation_date: self.creation_date, + }) + } +} + +impl KeyDecryptable for Fido2Credential { + fn decrypt_with_key( + &self, + key: &SymmetricCryptoKey, + ) -> Result { + Ok(Fido2CredentialView { + credential_id: self.credential_id.decrypt_with_key(key)?, + key_type: self.key_type.decrypt_with_key(key)?, + key_algorithm: self.key_algorithm.decrypt_with_key(key)?, + key_curve: self.key_curve.decrypt_with_key(key)?, + key_value: self.key_value.decrypt_with_key(key)?, + rp_id: self.rp_id.decrypt_with_key(key)?, + user_handle: self.user_handle.decrypt_with_key(key)?, + user_name: self.user_name.decrypt_with_key(key)?, + counter: self.counter.decrypt_with_key(key)?, + rp_name: self.rp_name.decrypt_with_key(key)?, + user_display_name: self.user_display_name.decrypt_with_key(key)?, + discoverable: self.discoverable.decrypt_with_key(key)?, + creation_date: self.creation_date, + }) + } +} + impl TryFrom for Login { type Error = Error; diff --git a/crates/bitwarden/src/vault/cipher/mod.rs b/crates/bitwarden/src/vault/cipher/mod.rs index c2b49eb37..89f1bc911 100644 --- a/crates/bitwarden/src/vault/cipher/mod.rs +++ b/crates/bitwarden/src/vault/cipher/mod.rs @@ -14,4 +14,5 @@ pub use attachment::{ }; pub use cipher::{Cipher, CipherListView, CipherRepromptType, CipherType, CipherView}; pub use field::FieldView; +pub use login::{Fido2Credential, Fido2CredentialView}; pub use secure_note::SecureNoteType; diff --git a/crates/bitwarden/src/vault/password_history.rs b/crates/bitwarden/src/vault/password_history.rs index 2a67d3a2e..f862bf2c5 100644 --- a/crates/bitwarden/src/vault/password_history.rs +++ b/crates/bitwarden/src/vault/password_history.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct PasswordHistory { @@ -17,7 +17,7 @@ pub struct PasswordHistory { last_used_date: DateTime, } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "mobile", derive(uniffi::Record))] pub struct PasswordHistoryView {