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
1 change: 1 addition & 0 deletions apps/desktop/desktop_native/Cargo.lock

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

8 changes: 8 additions & 0 deletions apps/desktop/desktop_native/fido2_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ futures-util = "0.3.31"
sha2 = { workspace = true }
webauthn-rs-proto = { path = "../../../../../webauthn-rs/webauthn-rs-proto" }
base64urlsafedata = { path= "../../../../../webauthn-rs/base64urlsafedata" }
windows = { version = "=0.52.0", features = [
"Win32_Graphics_Gdi",
"Win32_Networking_WindowsWebServices",
"Win32_Foundation",
"Win32_UI_WindowsAndMessaging",
"Win32_System_LibraryLoader",
"Win32_Graphics_Dwm",
]}

14 changes: 6 additions & 8 deletions apps/desktop/desktop_native/fido2_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ fn prf_to_hmac(prf_salt: &[u8]) -> [u8; 32] {
sha2::Sha256::digest(&[b"WebAuthn PRF".as_slice(), &[0], prf_salt].concat()).into()
}

#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum UserVerification {
Discouraged,
Preferred,
Required,
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct AssertionOptions {
pub challenge: Vec<u8>,
pub timeout: u64,
pub rpid: String,
pub user_verification: UserVerification,
pub allow_credentials: Vec<Vec<u8>>,
pub prf_eval_first: [u8; 32],
pub prf_eval_first: Option<[u8; 32]>,
pub prf_eval_second: Option<[u8; 32]>,
}

Expand Down Expand Up @@ -66,11 +66,9 @@ pub mod fido2_client {
) -> Result<super::PublicKeyCredential, super::Fido2ClientError> {
println!("Calling Windows FIDO2 client get()");
// run in new thread
std::thread::spawn(move || {
super::get(assertion_options)
})
.join()
.unwrap()
std::thread::spawn(move || super::get(assertion_options))
.join()
.unwrap()
}

pub fn available() -> bool {
Expand Down
221 changes: 177 additions & 44 deletions apps/desktop/desktop_native/fido2_client/src/windows.rs
Original file line number Diff line number Diff line change
@@ -1,55 +1,189 @@
use webauthn_authenticator_rs::{AuthenticatorBackend, prelude::Url, win10::Win10};
use webauthn_rs_proto::{HmacGetSecretInput, PublicKeyCredentialRequestOptions, RequestAuthenticationExtensions};
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
use windows::{
core::{w, HSTRING, PCWSTR},
Win32::{
Foundation::BOOL,
Networking::WindowsWebServices::{
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS,
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION, WEBAUTHN_CLIENT_DATA,
WEBAUTHN_CLIENT_DATA_CURRENT_VERSION, WEBAUTHN_CREDENTIALS, WEBAUTHN_CREDENTIAL_LIST,
WEBAUTHN_EXTENSION, WEBAUTHN_EXTENSIONS, WEBAUTHN_HMAC_SECRET_SALT,
WEBAUTHN_HMAC_SECRET_SALT_VALUES, WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED,
WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
UI::WindowsAndMessaging::GetForegroundWindow,
},
};

use crate::{AssertionOptions, Fido2ClientError, PublicKeyCredential};

fn to_native_uv(uv: crate::UserVerification) -> u32 {
match uv {
crate::UserVerification::Required => WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED,
crate::UserVerification::Preferred => WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED,
crate::UserVerification::Discouraged => WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
}
}

fn client_data_json(options: &AssertionOptions) -> String {
format!(
r#"{{"type":"webauthn.get","challenge":"{}","origin":"https://{}","crossOrigin": true}}"#,
BASE64_URL_SAFE_NO_PAD.encode(&options.challenge),
options.rpid
)
}

fn hmac_secret_salt(options: &AssertionOptions) -> WEBAUTHN_HMAC_SECRET_SALT {
let mut hmac_secret_salt = WEBAUTHN_HMAC_SECRET_SALT {
cbFirst: 0,
pbFirst: std::ptr::null_mut(),
cbSecond: 0,
pbSecond: std::ptr::null_mut(),
};
if let Some(first) = &options.prf_eval_first {
hmac_secret_salt.cbFirst = first.len() as u32;
hmac_secret_salt.pbFirst = first.as_ptr() as *mut _;
}
if let Some(second) = &options.prf_eval_second {
hmac_secret_salt.cbSecond = second.len() as u32;
hmac_secret_salt.pbSecond = second.as_ptr() as *mut _;
}
hmac_secret_salt
}

pub fn get(options: AssertionOptions) -> Result<PublicKeyCredential, Fido2ClientError> {
let uv = match options.user_verification {
crate::UserVerification::Required => webauthn_rs_proto::UserVerificationPolicy::Required,
crate::UserVerification::Preferred => webauthn_rs_proto::UserVerificationPolicy::Preferred,
crate::UserVerification::Discouraged => webauthn_rs_proto::UserVerificationPolicy::Discouraged_DO_NOT_USE,
let uv = to_native_uv(options.user_verification);
let rp_id: HSTRING = options.rpid.clone().into();

// HMAC secret extension
let hmac_extension_enabled = options.prf_eval_first.is_some();
let mut hmac_secret_salt = hmac_secret_salt(&options);
let mut hmac_secret_salt_values = WEBAUTHN_HMAC_SECRET_SALT_VALUES {
pGlobalHmacSalt: std::ptr::addr_of_mut!(hmac_secret_salt) as *mut _,
pCredWithHmacSecretSaltList: [].as_mut_ptr(),
cCredWithHmacSecretSaltList: 0,
};
let used: BOOL = true.into();
let hmac_extension: WEBAUTHN_EXTENSION = WEBAUTHN_EXTENSION {
pwszExtensionIdentifier: w!("hmacGetSecret"),
cbExtension: 1,
pvExtension: &used as *const _ as *mut _,
};
let extensions: WEBAUTHN_EXTENSIONS = if hmac_extension_enabled {
WEBAUTHN_EXTENSIONS {
cExtensions: 1,
pExtensions: &hmac_extension as *const _ as *mut _,
}
} else {
WEBAUTHN_EXTENSIONS {
cExtensions: 0,
pExtensions: std::ptr::null_mut(),
}
};
println!("User verification policy: {:?}", uv);

let opts = PublicKeyCredentialRequestOptions {
challenge: base64urlsafedata::Base64UrlSafeData::from(options.challenge),
timeout: Some(options.timeout as u32),
rp_id: options.rpid,
allow_credentials: vec![],
user_verification: uv,
hints: None,
extensions: Some(RequestAuthenticationExtensions {
hmac_get_secret: Some(HmacGetSecretInput {
output1: base64urlsafedata::Base64UrlSafeData::from(&options.prf_eval_first),
output2: None,
}),
appid: None,
uvm: None,

let client_data_json = client_data_json(&options.clone());
let client_data = WEBAUTHN_CLIENT_DATA {
dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
cbClientDataJSON: client_data_json.len() as u32,
pbClientDataJSON: client_data_json.as_ptr() as *mut _,
pwszHashAlgId: w!("SHA-256"),
};

let mut list = WEBAUTHN_CREDENTIAL_LIST {
cCredentials: 0,
ppCredentials: std::ptr::null_mut(),
};

let mut app_id_used: BOOL = false.into();
let getassertopts = WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS {
dwVersion: WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION,
dwTimeoutMilliseconds: options.timeout as u32,
CredentialList: WEBAUTHN_CREDENTIALS {
cCredentials: 0,
pCredentials: [].as_mut_ptr(),
},
Extensions: extensions,
dwAuthenticatorAttachment: 0,
dwUserVerificationRequirement: uv,
dwFlags: 0,
pwszU2fAppId: PCWSTR::null(),
pbU2fAppId: std::ptr::addr_of_mut!(app_id_used),
pCancellationId: std::ptr::null_mut(),
pAllowCredentialList: std::ptr::addr_of_mut!(list),
dwCredLargeBlobOperation: 0,
cbCredLargeBlob: 0,
pbCredLargeBlob: std::ptr::null_mut(),
bBrowserInPrivateMode: false.into(),
pHmacSecretSaltValues: if true {
std::ptr::addr_of_mut!(hmac_secret_salt_values) as *mut _
} else {
std::ptr::null_mut()
},
};

let assertion = unsafe {
*windows::Win32::Networking::WindowsWebServices::WebAuthNAuthenticatorGetAssertion(
GetForegroundWindow(),
&rp_id,
std::ptr::addr_of!(client_data) as *const _,
Some(&getassertopts),
)
.map_err(|_| Fido2ClientError::AssertionError)?
};

let id = unsafe {
std::slice::from_raw_parts(
assertion.Credential.pbId as *const u8,
assertion.Credential.cbId as usize,
)
};

let hmac = if hmac_extension_enabled {
Some(unsafe {
let hmac_secret = *assertion.pHmacSecret;
std::slice::from_raw_parts(
hmac_secret.pbFirst as *const u8,
hmac_secret.cbFirst as usize,
)
})
} else {
None
};
println!("Prepared PublicKeyCredentialRequestOptions: {:?}", opts);
let public_key_credential = Win10::default().perform_auth(Url::parse(
"https://vault.usdev.bitwarden.pw",
).unwrap(), opts, 60000)
.map_err(|_e| Fido2ClientError::AssertionError)?;
println!("PublicKeyCredential: {:?}", public_key_credential);

Ok(PublicKeyCredential {
id: public_key_credential.id,
raw_id: public_key_credential.raw_id.to_vec(),
id: BASE64_URL_SAFE_NO_PAD.encode(id),
raw_id: id.to_vec(),
response: crate::AuthenticatorAssertionResponse {
authenticator_data: public_key_credential.response.authenticator_data.to_vec(),
client_data_json: public_key_credential.response.client_data_json.to_vec(),
signature: public_key_credential.response.signature.to_vec(),
user_handle: public_key_credential.response.user_handle.map(|h| h.to_vec()).unwrap_or_default(),
authenticator_data: unsafe {
std::slice::from_raw_parts(
assertion.pbAuthenticatorData as *const u8,
assertion.cbAuthenticatorData as usize,
)
}
.to_vec(),
client_data_json: client_data_json.as_bytes().to_vec(),
signature: unsafe {
std::slice::from_raw_parts(
assertion.pbSignature as *const u8,
assertion.cbSignature as usize,
)
}
.to_vec(),
user_handle: unsafe {
std::slice::from_raw_parts(
assertion.pbUserId as *const u8,
assertion.cbUserId as usize,
)
}
.to_vec(),
},
prf: public_key_credential.extensions.hmac_get_secret.map(|hmac| {
let mut prf_bytes = [0u8; 32];
prf_bytes.copy_from_slice(&hmac.output1.to_vec().as_slice()[..32]);
prf_bytes
}),
// PRF (hmac-get-secret) extension parsing is not implemented here yet; return None.
prf: hmac
.map(|h| h.try_into().map_err(|_| Fido2ClientError::AssertionError))
.transpose()?,
authenticator_attachment: "cross-platform".to_string(),
r#type: public_key_credential.type_,
r#type: "public-key".to_string(),
})
}

Expand All @@ -65,17 +199,16 @@ mod tests {

#[test]
fn test_get() {
let options =
AssertionOptions {
let options = AssertionOptions {
challenge: vec![0u8; 32],
timeout: 0,
rpid: "vault.usdev.bitwarden.pw".to_string(),
user_verification: crate::UserVerification::Required,
allow_credentials: vec![],
prf_eval_first: [0u8; 32],
prf_eval_first: Some([0u8; 32]),
prf_eval_second: None,
};
let result = get(options);
println!("{:?}", result.unwrap());
}
}
}
Loading