diff --git a/Cargo.lock b/Cargo.lock index 866943af1..19282703c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,6 +363,7 @@ dependencies = [ "thiserror", "tokio", "uniffi", + "url", "uuid", "wiremock", "zeroize", diff --git a/crates/bitwarden-json/src/client.rs b/crates/bitwarden-json/src/client.rs index e8fd99abe..c48bb790c 100644 --- a/crates/bitwarden-json/src/client.rs +++ b/crates/bitwarden-json/src/client.rs @@ -1,5 +1,9 @@ use async_lock::Mutex; -use bitwarden::client::client_settings::ClientSettings; +use bitwarden::{ + client::client_settings::ClientSettings, + error::Result, + platform::{Fido2ClientGetAssertionRequest, Fido2GetAssertionUserInterface}, +}; #[cfg(feature = "secrets")] use crate::command::{ProjectsCommand, SecretsCommand}; @@ -16,6 +20,19 @@ impl Client { Self(Mutex::new(bitwarden::Client::new(settings))) } + pub async fn client_get_assertion( + &self, + request: Fido2ClientGetAssertionRequest, + user_interface: impl Fido2GetAssertionUserInterface, + ) -> Result { + let mut client = self.0.lock().await; + + client + .platform() + .client_get_assertion(request, user_interface) + .await + } + pub async fn run_command(&self, input_str: &str) -> String { const SUBCOMMANDS_TO_CLEAN: &[&str] = &["Secrets"]; let mut cmd_value: serde_json::Value = match serde_json::from_str(input_str) { @@ -63,11 +80,6 @@ impl Client { #[cfg(feature = "internal")] Command::Fingerprint(req) => client.platform().fingerprint(&req).into_string(), - #[cfg(feature = "internal")] - Command::Fido2ClientGetAssertion(req) => { - client.platform().client_get_assertion(req).into_string() - } - #[cfg(feature = "secrets")] Command::Secrets(cmd) => match cmd { SecretsCommand::Get(req) => client.secrets().get(&req).await.into_string(), diff --git a/crates/bitwarden-json/src/command.rs b/crates/bitwarden-json/src/command.rs index 251eaf3ea..8da5e8cbd 100644 --- a/crates/bitwarden-json/src/command.rs +++ b/crates/bitwarden-json/src/command.rs @@ -15,9 +15,7 @@ use bitwarden::{ #[cfg(feature = "internal")] use bitwarden::{ auth::login::{ApiKeyLoginRequest, PasswordLoginRequest}, - platform::{ - Fido2ClientGetAssertionRequest, FingerprintRequest, SecretVerificationRequest, SyncRequest, - }, + platform::{FingerprintRequest, SecretVerificationRequest, SyncRequest}, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -73,12 +71,6 @@ pub enum Command { /// Returns: [SyncResponse](bitwarden::platform::SyncResponse) Sync(SyncRequest), - #[cfg(feature = "internal")] - /// > Requires Authentication - /// - /// Returns: ? - Fido2ClientGetAssertion(Fido2ClientGetAssertionRequest), - #[cfg(feature = "secrets")] Secrets(SecretsCommand), #[cfg(feature = "secrets")] diff --git a/crates/bitwarden-json/src/lib.rs b/crates/bitwarden-json/src/lib.rs index 832fa3168..e900b94c7 100644 --- a/crates/bitwarden-json/src/lib.rs +++ b/crates/bitwarden-json/src/lib.rs @@ -1,3 +1,8 @@ pub mod client; pub mod command; pub mod response; + +pub use bitwarden::{ + error::Result, + platform::{Fido2ClientGetAssertionRequest, Fido2GetAssertionUserInterface, VaultItem}, +}; diff --git a/crates/bitwarden-wasm/src/client.rs b/crates/bitwarden-wasm/src/client.rs index 542759731..2d1d64715 100644 --- a/crates/bitwarden-wasm/src/client.rs +++ b/crates/bitwarden-wasm/src/client.rs @@ -1,8 +1,11 @@ extern crate console_error_panic_hook; -use std::rc::Rc; +use std::{fmt::Result, process::Output, rc::Rc}; -use bitwarden_json::client::Client as JsonClient; -use js_sys::Promise; +use bitwarden_json::{ + client::Client as JsonClient, Fido2ClientGetAssertionRequest, Fido2GetAssertionUserInterface, + VaultItem, +}; +use js_sys::{Object, Promise}; use log::Level; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; @@ -26,6 +29,36 @@ fn convert_level(level: LogLevel) -> Level { } } +#[wasm_bindgen] +extern "C" { + pub type JSFido2GetAssertionUserInterface; + + #[wasm_bindgen(structural, method)] + pub fn pick_credential( + this: &JSFido2GetAssertionUserInterface, + cipher_ids: Vec, + rp_id: String, + ) -> Promise; +} + +impl Fido2GetAssertionUserInterface for JSFido2GetAssertionUserInterface { + async fn pick_credential( + &self, + cipher_ids: Vec, + rp_id: &str, + ) -> bitwarden_json::Result { + log::debug!("JSFido2GetAssertionUserInterface.pick_credential"); + let picked_id_promise = self.pick_credential(cipher_ids.clone(), rp_id.to_string()); + + let picked_id = wasm_bindgen_futures::JsFuture::from(picked_id_promise).await; + + Ok(VaultItem::new( + picked_id.unwrap().as_string().unwrap(), + "name".to_string(), + )) + } +} + // Rc<...> is to avoid needing to take ownership of the Client during our async run_command // function https://github.com/rustwasm/wasm-bindgen/issues/2195#issuecomment-799588401 #[wasm_bindgen] @@ -53,4 +86,27 @@ impl BitwardenClient { Ok(result.into()) }) } + + #[wasm_bindgen] + pub async fn client_get_assertion( + &mut self, + param: String, + user_interface: JSFido2GetAssertionUserInterface, + ) { + log::info!("wasm_bindgen.client_get_assertion"); + log::debug!("wasm_bindgen.client_get_assertion"); + // let rc = self.0.clone(); + // future_to_promise(async move { + // let result = rc.run_command(&js_input).await; + // Ok(result.into()) + // }) + let request = Fido2ClientGetAssertionRequest { + webauthn_json: param, + }; + + self.0 + .client_get_assertion(request, user_interface) + .await + .unwrap(); + } } diff --git a/crates/bitwarden/Cargo.toml b/crates/bitwarden/Cargo.toml index 4c6fc47d2..b04c3ad16 100644 --- a/crates/bitwarden/Cargo.toml +++ b/crates/bitwarden/Cargo.toml @@ -57,6 +57,7 @@ sha1 = ">=0.10.5, <0.11" sha2 = ">=0.10.6, <0.11" thiserror = ">=1.0.40, <2.0" uniffi = { version = "=0.26.1", optional = true, features = ["tokio"] } +url = "2.5.0" uuid = { version = ">=1.3.3, <2.0", features = ["serde"] } zxcvbn = ">= 2.2.2, <3.0" diff --git a/crates/bitwarden/src/platform/client_platform.rs b/crates/bitwarden/src/platform/client_platform.rs index 8632f2b0b..462bb256c 100644 --- a/crates/bitwarden/src/platform/client_platform.rs +++ b/crates/bitwarden/src/platform/client_platform.rs @@ -1,5 +1,6 @@ use super::{ client_get_assertion, + fido2::Fido2GetAssertionUserInterface, generate_fingerprint::{generate_fingerprint, generate_user_fingerprint}, Fido2ClientGetAssertionRequest, FingerprintRequest, FingerprintResponse, }; @@ -18,8 +19,13 @@ impl<'a> ClientPlatform<'a> { generate_user_fingerprint(self.client, fingerprint_material) } - pub fn client_get_assertion(&self, request: Fido2ClientGetAssertionRequest) -> Result { - client_get_assertion(request) + pub async fn client_get_assertion( + &self, + request: Fido2ClientGetAssertionRequest, + user_interface: impl Fido2GetAssertionUserInterface, + ) -> Result { + log::debug!("client_platform.client_get_assertion"); + client_get_assertion(request, user_interface).await } } diff --git a/crates/bitwarden/src/platform/fido2.rs b/crates/bitwarden/src/platform/fido2.rs index 383e6b8bb..978568c1a 100644 --- a/crates/bitwarden/src/platform/fido2.rs +++ b/crates/bitwarden/src/platform/fido2.rs @@ -1,8 +1,168 @@ +use std::{borrow::Borrow, cell::Cell}; + use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::error::Result; +use url::Url; + +use passkey::{ + authenticator::{Authenticator, CredentialStore, UserValidationMethod}, + client::Client, + types::{ + ctap2::{self, *}, + webauthn::*, + Passkey, + }, +}; + +pub trait Fido2GetAssertionUserInterface { + async fn pick_credential(&self, ids: Vec, rp_id: &str) -> Result; +} + +#[derive(Default)] +pub struct VaultItem { + cipher_id: String, + name: String, +} + +impl VaultItem { + pub fn new(cipher_id: String, name: String) -> Self { + Self { cipher_id, name } + } +} + +// impl TryInto for VaultItem { +// type Error = WebauthnError; + +// fn try_into(self) -> Result { +// todo!(); +// } +// } + +// TODO: Use Mutex instead Wrap +#[derive(Default)] +struct Wrap(T); +unsafe impl Sync for Wrap {} + +impl From for Passkey { + fn from(value: VaultItem) -> Self { + todo!() + } +} + +// Split session into attest and assert +#[derive(Default)] +struct Fido2Session { + user_interface: Wrap, + user_presence: Wrap>, +} + +impl Fido2Session +where + U: Fido2GetAssertionUserInterface, +{ + fn new(user_interface: U) -> Self { + Self { + user_interface: Wrap(user_interface), + user_presence: Wrap(Cell::new(false)), + } + } +} + +struct Fido2CredentialStore<'a, U> +where + U: Fido2GetAssertionUserInterface, +{ + session: &'a Fido2Session, +} + +fn uuid_raw_to_standard_format(uuid: &Vec) -> String { + let mut uuid_str = String::with_capacity(36); + uuid_str.push_str(&format!( + "{:02X}{:02X}{:02X}{:02X}-", + uuid[0], uuid[1], uuid[2], uuid[3] + )); + uuid_str.push_str(&format!("{:02X}{:02X}-", uuid[4], uuid[5])); + uuid_str.push_str(&format!("{:02X}{:02X}-", uuid[6], uuid[7])); + uuid_str.push_str(&format!("{:02X}{:02X}-", uuid[8], uuid[9])); + for i in 10..uuid.len() { + uuid_str.push_str(&format!("{:02X}", uuid[i])); + } + uuid_str +} + +#[async_trait::async_trait] +impl<'a, U> CredentialStore for Fido2CredentialStore<'a, U> +where + U: Fido2GetAssertionUserInterface, +{ + type PasskeyItem = VaultItem; + + async fn find_credentials( + &self, + ids: Option<&[PublicKeyCredentialDescriptor]>, + rp_id: &str, + ) -> Result, StatusCode> { + let id_strs = ids + .map(|ids| { + ids.iter() + .map(|id| uuid_raw_to_standard_format(&id.id)) + .collect::>() + }) + .unwrap_or_default(); + let result = self + .session + .user_interface + .0 + .pick_credential(id_strs, rp_id); + // .await <-- awaiting here causes error + + // match result { + // Ok(item) => Ok(vec![item]), + // Err(e) => Err(StatusCode::Ctap2(Ctap2Code::Known(Ctap2Error::NotAllowed))), + // } + + todo!() + } + + async fn save_credential( + &mut self, + cred: Passkey, + user: ctap2::make_credential::PublicKeyCredentialUserEntity, + rp: ctap2::make_credential::PublicKeyCredentialRpEntity, + ) -> Result<(), StatusCode> { + todo!(); + } +} + +struct Fido2UserValidationMethod<'a, U> { + session: &'a Fido2Session, +} + +#[async_trait::async_trait] +impl<'a, U> UserValidationMethod for Fido2UserValidationMethod<'a, U> +where + U: Fido2GetAssertionUserInterface, +{ + async fn check_user_verification(&self) -> bool { + false + } + + async fn check_user_presence(&self) -> bool { + self.session.user_presence.0.get() + } + + fn is_presence_enabled(&self) -> bool { + false + } + + fn is_verification_enabled(&self) -> Option { + Some(false) + } +} + #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Fido2ClientGetAssertionRequest { @@ -10,6 +170,43 @@ pub struct Fido2ClientGetAssertionRequest { pub webauthn_json: String, } -pub(crate) fn client_get_assertion(request: Fido2ClientGetAssertionRequest) -> Result { +pub(crate) async fn client_get_assertion( + request: Fido2ClientGetAssertionRequest, + user_interface: impl Fido2GetAssertionUserInterface, +) -> Result { + log::debug!("fido2.client_get_assertion"); + // First create an Authenticator for the Client to use. + let my_aaguid = Aaguid::new_empty(); + let session = Fido2Session::new(user_interface); + let store = Fido2CredentialStore { session: &session }; + let user_validation = Fido2UserValidationMethod { session: &session }; + // Create the CredentialStore for the Authenticator. + // Option is the simplest possible implementation of CredentialStore + let my_authenticator = Authenticator::new(my_aaguid, store, user_validation); + + // Create the Client + // If you are creating credentials, you need to declare the Client as mut + let my_client = Client::new(my_authenticator.into()); + + let challenge = vec![0; 32]; + let options = CredentialRequestOptions { + public_key: PublicKeyCredentialRequestOptions { + allow_credentials: None, + attestation: AttestationConveyancePreference::None, + challenge: challenge.into(), + timeout: None, + rp_id: Some("bitwarden.com".to_string()), + user_verification: UserVerificationRequirement::Preferred, + hints: None, + attestation_formats: None, + extensions: None, + }, + }; + + my_client + .authenticate(&Url::parse("https://bitwarden.com").unwrap(), options, None) + .await + .unwrap(); + Ok("client_get_assertion".to_string()) } diff --git a/crates/bitwarden/src/platform/mod.rs b/crates/bitwarden/src/platform/mod.rs index 2a06d03ba..9f0ced8b5 100644 --- a/crates/bitwarden/src/platform/mod.rs +++ b/crates/bitwarden/src/platform/mod.rs @@ -7,7 +7,7 @@ mod secret_verification_request; mod sync; pub(crate) use fido2::client_get_assertion; -pub use fido2::Fido2ClientGetAssertionRequest; +pub use fido2::{Fido2ClientGetAssertionRequest, Fido2GetAssertionUserInterface, VaultItem}; 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/languages/js/sdk-client/src/client.ts b/languages/js/sdk-client/src/client.ts index 1cf593f05..66cb8c438 100644 --- a/languages/js/sdk-client/src/client.ts +++ b/languages/js/sdk-client/src/client.ts @@ -8,8 +8,20 @@ import { SecretsDeleteResponse, } from "./schemas"; +export interface Fido2GetAssertionUserInterface { + /** + pub fn pick_credential( + this: &JSFido2GetAssertionUserInterface, + cipher_ids: Vec, + rp_id: String, + ) -> String; + */ + pick_credential(cipherIds: string[], rpId: string): string; +} + interface BitwardenSDKClient { run_command(js_input: string): Promise; + client_get_assertion(param: string, user_interface: Fido2GetAssertionUserInterface); } function handleResponse(response: { success: boolean; errorMessage?: string; data?: T }): T { @@ -40,18 +52,6 @@ export class BitwardenClient { return Convert.toResponseForFingerprintResponse(response).data.fingerprint; }; - async client_get_assertion(): Promise { - const response = await this.client.run_command( - Convert.commandToJson({ - fido2ClientGetAssertion: { - webauthnJson: "" - }, - }) - ); - - return response; - } - async accessTokenLogin(accessToken: string): Promise { const response = await this.client.run_command( Convert.commandToJson({