1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 03:33:30 +00:00

UserId -> UserHandle

This commit is contained in:
Anders Åberg
2025-07-07 13:20:05 +02:00
parent af987a6ba7
commit 86dfc76b9f
12 changed files with 645 additions and 469 deletions

View File

@@ -194,7 +194,7 @@ export declare namespace passkey_authenticator {
credentialId: string
rpId: string
userName: string
userId: string
userHandle: string
}
export interface PasskeyAssertionRequest {
rpId: string
@@ -206,7 +206,7 @@ export declare namespace passkey_authenticator {
export interface PasskeyRegistrationRequest {
rpId: string
transactionId: string
userId: Array<number>
userHandle: Array<number>
userName: string
clientDataHash: Array<number>
userVerification: boolean

View File

@@ -825,7 +825,7 @@ pub mod passkey_authenticator {
pub credential_id: String, // base64url encoded
pub rp_id: String,
pub user_name: String,
pub user_id: String, // base64url encoded
pub user_handle: String, // base64url encoded
}
#[napi(object)]
@@ -846,7 +846,7 @@ pub mod passkey_authenticator {
pub struct PasskeyRegistrationRequest {
pub rp_id: String,
pub transaction_id: String,
pub user_id: Vec<u8>,
pub user_handle: Vec<u8>,
pub user_name: String,
pub client_data_hash: Vec<u8>,
pub user_verification: bool,

View File

@@ -98,7 +98,7 @@ impl From<windows_plugin_authenticator::SyncedCredential> for SyncedCredential {
.encode(&cred.credential_id),
rp_id: cred.rp_id,
user_name: cred.user_name,
user_id: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cred.user_id),
user_handle: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cred.user_handle),
}
}
}
@@ -112,8 +112,8 @@ impl From<SyncedCredential> for windows_plugin_authenticator::SyncedCredential {
.unwrap_or_default(),
rp_id: cred.rp_id,
user_name: cred.user_name,
user_id: base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&cred.user_id)
user_handle: base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&cred.user_handle)
.unwrap_or_default(),
}
}
@@ -134,10 +134,10 @@ pub fn sync_credentials_to_windows(credentials: Vec<SyncedCredential>) -> napi::
} else {
cred.credential_id.clone()
};
let truncated_user_id = if cred.user_id.len() > 16 {
format!("{}...", &cred.user_id[..16])
let truncated_user_id = if cred.user_handle.len() > 16 {
format!("{}...", &cred.user_handle[..16])
} else {
cred.user_id.clone()
cred.user_handle.clone()
};
log::info!(
"[NAPI] Credential {}: RP={}, User={}, CredID={}, UserID={}",

View File

@@ -1,18 +1,19 @@
use serde_json;
use std::alloc::{alloc, Layout};
use std::ptr;
use serde_json;
use windows_core::{HRESULT, s};
use windows_core::{s, HRESULT};
use crate::com_provider::ExperimentalWebAuthnPluginOperationResponse;
use crate::types::*;
use crate::utils::{self as util, delay_load};
use crate::com_provider::ExperimentalWebAuthnPluginOperationResponse;
use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST;
// Windows API types for WebAuthn (from webauthn.h.sample)
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST {
pub dwVersion: u32,
pub pwszRpId: *const u16, // PCWSTR
pub pwszRpId: *const u16, // PCWSTR
pub cbRpId: u32,
pub pbRpId: *const u8,
pub cbClientDataHash: u32,
@@ -20,28 +21,13 @@ pub struct EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST {
pub CredentialList: WEBAUTHN_CREDENTIAL_LIST,
pub cbCborExtensionsMap: u32,
pub pbCborExtensionsMap: *const u8,
pub pAuthenticatorOptions: *const crate::webauthn::ExperimentalWebAuthnCtapCborAuthenticatorOptions,
pub pAuthenticatorOptions:
*const crate::webauthn::ExperimentalWebAuthnCtapCborAuthenticatorOptions,
// Add other fields as needed...
}
pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST = *mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_CREDENTIAL_EX {
pub dwVersion: u32,
pub cbId: u32,
pub pbId: *const u8,
pub pwszCredentialType: *const u16, // LPCWSTR
pub dwTransports: u32,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_CREDENTIAL_LIST {
pub cCredentials: u32,
pub ppCredentials: *const *const WEBAUTHN_CREDENTIAL_EX,
}
pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST =
*mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST;
// Windows API function signatures for decoding get assertion requests
type EXPERIMENTAL_WebAuthNDecodeGetAssertionRequestFn = unsafe extern "stdcall" fn(
@@ -61,10 +47,13 @@ pub struct DecodedGetAssertionRequest {
}
impl DecodedGetAssertionRequest {
fn new(ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST, free_fn: Option<EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequestFn>) -> Self {
fn new(
ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST,
free_fn: Option<EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequestFn>,
) -> Self {
Self { ptr, free_fn }
}
pub fn as_ref(&self) -> &EXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST {
unsafe { &*self.ptr }
}
@@ -75,44 +64,55 @@ impl Drop for DecodedGetAssertionRequest {
if !self.ptr.is_null() {
if let Some(free_fn) = self.free_fn {
util::message("Freeing decoded get assertion request");
unsafe { free_fn(self.ptr); }
unsafe {
free_fn(self.ptr);
}
}
}
}
}
// Function to decode get assertion request using Windows API
pub unsafe fn decode_get_assertion_request(encoded_request: &[u8]) -> Result<DecodedGetAssertionRequest, String> {
pub unsafe fn decode_get_assertion_request(
encoded_request: &[u8],
) -> Result<DecodedGetAssertionRequest, String> {
util::message("Attempting to decode get assertion request using Windows API");
// Load the Windows WebAuthn API function
let decode_fn: Option<EXPERIMENTAL_WebAuthNDecodeGetAssertionRequestFn> = delay_load(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest")
s!("EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest"),
);
let decode_fn = decode_fn.ok_or("Failed to load EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest from webauthn.dll")?;
let decode_fn = decode_fn
.ok_or("Failed to load EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest from webauthn.dll")?;
// Load the free function
let free_fn: Option<EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequestFn> = delay_load(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequest")
s!("EXPERIMENTAL_WebAuthNFreeDecodedGetAssertionRequest"),
);
let mut pp_get_assertion_request: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST = ptr::null_mut();
let mut pp_get_assertion_request: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_GET_ASSERTION_REQUEST =
ptr::null_mut();
let result = decode_fn(
encoded_request.len() as u32,
encoded_request.as_ptr(),
&mut pp_get_assertion_request,
);
if result.is_err() || pp_get_assertion_request.is_null() {
return Err(format!("EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest failed with HRESULT: {}", result.0));
return Err(format!(
"EXPERIMENTAL_WebAuthNDecodeGetAssertionRequest failed with HRESULT: {}",
result.0
));
}
Ok(DecodedGetAssertionRequest::new(pp_get_assertion_request, free_fn))
Ok(DecodedGetAssertionRequest::new(
pp_get_assertion_request,
free_fn,
))
}
/// Windows WebAuthn assertion request context
@@ -124,65 +124,11 @@ pub struct WindowsAssertionRequest {
pub user_verification: UserVerificationRequirement,
}
/// Windows WebAuthn registration request context
#[derive(Debug, Clone)]
pub struct WindowsRegistrationRequest {
pub rpid: String,
pub user_id: Vec<u8>,
pub user_name: String,
pub user_display_name: Option<String>,
pub client_data_hash: Vec<u8>,
pub excluded_credentials: Vec<Vec<u8>>,
pub user_verification: UserVerificationRequirement,
pub supported_algorithms: Vec<i32>,
}
/// Parse allowed credentials from WEBAUTHN_CREDENTIAL_LIST
pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST) -> Vec<Vec<u8>> {
let mut allowed_credentials = Vec::new();
if credential_list.cCredentials == 0 || credential_list.ppCredentials.is_null() {
util::message("No credentials in credential list");
return allowed_credentials;
}
util::message(&format!("Parsing {} credentials from credential list", credential_list.cCredentials));
// ppCredentials is an array of pointers to WEBAUTHN_CREDENTIAL_EX
let credentials_array = std::slice::from_raw_parts(
credential_list.ppCredentials,
credential_list.cCredentials as usize
);
for (i, &credential_ptr) in credentials_array.iter().enumerate() {
if credential_ptr.is_null() {
util::message(&format!("WARNING: Credential {} is null, skipping", i));
continue;
}
let credential = &*credential_ptr;
if credential.cbId == 0 || credential.pbId.is_null() {
util::message(&format!("WARNING: Credential {} has invalid ID, skipping", i));
continue;
}
// Extract credential ID bytes
let credential_id_slice = std::slice::from_raw_parts(
credential.pbId,
credential.cbId as usize
);
allowed_credentials.push(credential_id_slice.to_vec());
util::message(&format!("Parsed credential {}: {} bytes", i, credential.cbId));
}
util::message(&format!("Successfully parsed {} allowed credentials", allowed_credentials.len()));
allowed_credentials
}
/// Helper for assertion requests
pub fn send_assertion_request(transaction_id: &str, request: &WindowsAssertionRequest) -> Option<PasskeyResponse> {
pub fn send_assertion_request(
transaction_id: &str,
request: &WindowsAssertionRequest,
) -> Option<PasskeyResponse> {
let passkey_request = PasskeyAssertionRequest {
rp_id: request.rpid.clone(),
transaction_id: transaction_id.to_string(),
@@ -190,17 +136,24 @@ pub fn send_assertion_request(transaction_id: &str, request: &WindowsAssertionRe
allowed_credentials: request.allowed_credentials.clone(),
user_verification: request.user_verification.clone(),
};
util::message(&format!("Assertion request data - RP ID: {}, Client data hash: {} bytes, Allowed credentials: {}",
request.rpid, request.client_data_hash.len(), request.allowed_credentials.len()));
util::message(&format!(
"Assertion request data - RP ID: {}, Client data hash: {} bytes, Allowed credentials: {}",
request.rpid,
request.client_data_hash.len(),
request.allowed_credentials.len()
));
match serde_json::to_string(&passkey_request) {
Ok(request_json) => {
util::message(&format!("Sending assertion request: {}", request_json));
crate::ipc::send_passkey_request(RequestType::Assertion, request_json, &request.rpid)
},
}
Err(e) => {
util::message(&format!("ERROR: Failed to serialize assertion request: {}", e));
util::message(&format!(
"ERROR: Failed to serialize assertion request: {}",
e
));
None
}
}
@@ -211,79 +164,93 @@ pub unsafe fn create_get_assertion_response(
credential_id: Vec<u8>,
authenticator_data: Vec<u8>,
signature: Vec<u8>,
user_handle: Vec<u8>
user_handle: Vec<u8>,
) -> std::result::Result<*mut ExperimentalWebAuthnPluginOperationResponse, HRESULT> {
// Construct a CTAP2 response with the proper structure
// Create CTAP2 GetAssertion response map according to CTAP2 specification
let mut cbor_response: Vec<(ciborium::Value, ciborium::Value)> = Vec::new();
// [1] credential (optional) - Always include credential descriptor
let credential_map = vec![
(ciborium::Value::Text("id".to_string()), ciborium::Value::Bytes(credential_id.clone())),
(ciborium::Value::Text("type".to_string()), ciborium::Value::Text("public-key".to_string())),
(
ciborium::Value::Text("id".to_string()),
ciborium::Value::Bytes(credential_id.clone()),
),
(
ciborium::Value::Text("type".to_string()),
ciborium::Value::Text("public-key".to_string()),
),
];
cbor_response.push((
ciborium::Value::Integer(1.into()),
ciborium::Value::Map(credential_map)
ciborium::Value::Integer(1.into()),
ciborium::Value::Map(credential_map),
));
// [2] authenticatorData (required)
cbor_response.push((
ciborium::Value::Integer(2.into()),
ciborium::Value::Bytes(authenticator_data)
ciborium::Value::Integer(2.into()),
ciborium::Value::Bytes(authenticator_data),
));
// [3] signature (required)
cbor_response.push((
ciborium::Value::Integer(3.into()),
ciborium::Value::Bytes(signature)
ciborium::Value::Integer(3.into()),
ciborium::Value::Bytes(signature),
));
// [4] user (optional) - include if user handle is provided
if !user_handle.is_empty() {
let user_map = vec![
(ciborium::Value::Text("id".to_string()), ciborium::Value::Bytes(user_handle)),
];
let user_map = vec![(
ciborium::Value::Text("id".to_string()),
ciborium::Value::Bytes(user_handle),
)];
cbor_response.push((
ciborium::Value::Integer(4.into()),
ciborium::Value::Map(user_map)
ciborium::Value::Integer(4.into()),
ciborium::Value::Map(user_map),
));
}
let cbor_value = ciborium::Value::Map(cbor_response);
// Encode to CBOR with error handling
let mut cbor_data = Vec::new();
if let Err(e) = ciborium::ser::into_writer(&cbor_value, &mut cbor_data) {
util::message(&format!("ERROR: Failed to encode CBOR assertion response: {:?}", e));
util::message(&format!(
"ERROR: Failed to encode CBOR assertion response: {:?}",
e
));
return Err(HRESULT(-1));
}
let response_len = cbor_data.len();
// Allocate memory for the response data
let layout = Layout::from_size_align(response_len, 1).map_err(|_| HRESULT(-1))?;
let response_ptr = alloc(layout);
if response_ptr.is_null() {
return Err(HRESULT(-1));
}
// Copy response data
ptr::copy_nonoverlapping(cbor_data.as_ptr(), response_ptr, response_len);
// Allocate memory for the response structure
let response_layout = Layout::new::<ExperimentalWebAuthnPluginOperationResponse>();
let operation_response_ptr = alloc(response_layout) as *mut ExperimentalWebAuthnPluginOperationResponse;
let operation_response_ptr =
alloc(response_layout) as *mut ExperimentalWebAuthnPluginOperationResponse;
if operation_response_ptr.is_null() {
return Err(HRESULT(-1));
}
// Initialize the response
ptr::write(operation_response_ptr, ExperimentalWebAuthnPluginOperationResponse {
encoded_response_byte_count: response_len as u32,
encoded_response_pointer: response_ptr,
});
ptr::write(
operation_response_ptr,
ExperimentalWebAuthnPluginOperationResponse {
encoded_response_byte_count: response_len as u32,
encoded_response_pointer: response_ptr,
},
);
Ok(operation_response_ptr)
}
}

View File

@@ -1,11 +1,18 @@
use std::ptr;
use windows::Win32::System::Com::*;
use windows_core::{implement, interface, IInspectable, IUnknown, Interface, HRESULT};
use std::ptr;
use crate::assert::{
create_get_assertion_response, decode_get_assertion_request, send_assertion_request,
WindowsAssertionRequest,
};
use crate::make_credential::{
create_make_credential_response, decode_make_credential_request, send_registration_request,
WindowsRegistrationRequest,
};
use crate::types::*;
use crate::utils::{self as util, wstr_to_string};
use crate::assert::{WindowsAssertionRequest, decode_get_assertion_request, create_get_assertion_response, send_assertion_request, parse_credential_list};
use crate::make_credential::{WindowsRegistrationRequest, decode_make_credential_request, create_make_credential_response, send_registration_request};
use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST;
/// Used when creating and asserting credentials.
/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST
@@ -62,6 +69,59 @@ pub unsafe trait EXPERIMENTAL_IPluginAuthenticator: windows_core::IUnknown {
) -> HRESULT;
}
pub unsafe fn parse_credential_list(credential_list: &WEBAUTHN_CREDENTIAL_LIST) -> Vec<Vec<u8>> {
let mut allowed_credentials = Vec::new();
if credential_list.cCredentials == 0 || credential_list.ppCredentials.is_null() {
util::message("No credentials in credential list");
return allowed_credentials;
}
util::message(&format!(
"Parsing {} credentials from credential list",
credential_list.cCredentials
));
// ppCredentials is an array of pointers to WEBAUTHN_CREDENTIAL_EX
let credentials_array = std::slice::from_raw_parts(
credential_list.ppCredentials,
credential_list.cCredentials as usize,
);
for (i, &credential_ptr) in credentials_array.iter().enumerate() {
if credential_ptr.is_null() {
util::message(&format!("WARNING: Credential {} is null, skipping", i));
continue;
}
let credential = &*credential_ptr;
if credential.cbId == 0 || credential.pbId.is_null() {
util::message(&format!(
"WARNING: Credential {} has invalid ID, skipping",
i
));
continue;
}
// Extract credential ID bytes
let credential_id_slice =
std::slice::from_raw_parts(credential.pbId, credential.cbId as usize);
allowed_credentials.push(credential_id_slice.to_vec());
util::message(&format!(
"Parsed credential {}: {} bytes",
i, credential.cbId
));
}
util::message(&format!(
"Successfully parsed {} allowed credentials",
allowed_credentials.len()
));
allowed_credentials
}
#[implement(EXPERIMENTAL_IPluginAuthenticator)]
pub struct PluginAuthenticatorComObject;
@@ -75,51 +135,60 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT {
util::message("=== EXPERIMENTAL_PluginMakeCredential() called ===");
if request.is_null() {
util::message("ERROR: NULL request pointer");
return HRESULT(-1);
}
if response.is_null() {
util::message("ERROR: NULL response pointer");
return HRESULT(-1);
}
let req = &*request;
let transaction_id = format!("{:?}", req.transaction_id);
util::message(&format!("Transaction ID: {}", transaction_id));
util::message(&format!("Window Handle: {:?}", req.window_handle));
util::message(&format!("Request Signature Byte Count: {}", req.request_signature_byte_count));
util::message(&format!("Encoded Request Byte Count: {}", req.encoded_request_byte_count));
util::message(&format!(
"Request Signature Byte Count: {}",
req.request_signature_byte_count
));
util::message(&format!(
"Encoded Request Byte Count: {}",
req.encoded_request_byte_count
));
if req.encoded_request_byte_count == 0 || req.encoded_request_pointer.is_null() {
util::message("ERROR: No encoded request data provided");
return HRESULT(-1);
}
let encoded_request_slice = std::slice::from_raw_parts(
req.encoded_request_pointer,
req.encoded_request_byte_count as usize
req.encoded_request_pointer,
req.encoded_request_byte_count as usize,
);
util::message(&format!("Encoded request: {} bytes", encoded_request_slice.len()));
util::message(&format!(
"Encoded request: {} bytes",
encoded_request_slice.len()
));
// Try to decode the request using Windows API
match decode_make_credential_request(encoded_request_slice) {
Ok(decoded_wrapper) => {
let decoded_request = decoded_wrapper.as_ref();
util::message("Successfully decoded make credential request using Windows API");
// Extract RP information
if decoded_request.pRpInformation.is_null() {
util::message("ERROR: RP information is null");
return HRESULT(-1);
}
let rp_info = &*decoded_request.pRpInformation;
let rpid = if rp_info.pwszId.is_null() {
util::message("ERROR: RP ID is null");
return HRESULT(-1);
@@ -132,21 +201,21 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
}
}
};
let rp_name = if rp_info.pwszName.is_null() {
String::new()
} else {
wstr_to_string(rp_info.pwszName).unwrap_or_default()
};
// Extract user information
if decoded_request.pUserInformation.is_null() {
util::message("ERROR: User information is null");
return HRESULT(-1);
}
let user = &*decoded_request.pUserInformation;
let user_id = if user.pbId.is_null() || user.cbId == 0 {
util::message("ERROR: User ID is required for registration");
return HRESULT(-1);
@@ -154,7 +223,7 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
let id_slice = std::slice::from_raw_parts(user.pbId, user.cbId as usize);
id_slice.to_vec()
};
let user_name = if user.pwszName.is_null() {
util::message("ERROR: User name is required for registration");
return HRESULT(-1);
@@ -167,51 +236,65 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
}
}
};
let user_display_name = if user.pwszDisplayName.is_null() {
None
} else {
wstr_to_string(user.pwszDisplayName).ok()
};
let user_info = (user_id, user_name, user_display_name);
// Extract client data hash
let client_data_hash = if decoded_request.cbClientDataHash == 0 || decoded_request.pbClientDataHash.is_null() {
let client_data_hash = if decoded_request.cbClientDataHash == 0
|| decoded_request.pbClientDataHash.is_null()
{
util::message("ERROR: Client data hash is required for registration");
return HRESULT(-1);
} else {
let hash_slice = std::slice::from_raw_parts(
decoded_request.pbClientDataHash,
decoded_request.cbClientDataHash as usize
decoded_request.pbClientDataHash,
decoded_request.cbClientDataHash as usize,
);
hash_slice.to_vec()
};
// Extract RP ID raw bytes for authenticator data
let rpid_bytes = if decoded_request.cbRpId > 0 && !decoded_request.pbRpId.is_null() {
let rpid_bytes = if decoded_request.cbRpId > 0 && !decoded_request.pbRpId.is_null()
{
let rpid_slice = std::slice::from_raw_parts(
decoded_request.pbRpId,
decoded_request.cbRpId as usize
decoded_request.cbRpId as usize,
);
rpid_slice.to_vec()
} else {
rpid.as_bytes().to_vec()
};
// Extract supported algorithms
let supported_algorithms = if decoded_request.WebAuthNCredentialParameters.cCredentialParameters > 0 &&
!decoded_request.WebAuthNCredentialParameters.pCredentialParameters.is_null() {
let params_count = decoded_request.WebAuthNCredentialParameters.cCredentialParameters as usize;
let params_ptr = decoded_request.WebAuthNCredentialParameters.pCredentialParameters;
let supported_algorithms = if decoded_request
.WebAuthNCredentialParameters
.cCredentialParameters
> 0
&& !decoded_request
.WebAuthNCredentialParameters
.pCredentialParameters
.is_null()
{
let params_count = decoded_request
.WebAuthNCredentialParameters
.cCredentialParameters as usize;
let params_ptr = decoded_request
.WebAuthNCredentialParameters
.pCredentialParameters;
(0..params_count)
.map(|i| unsafe { &*params_ptr.add(i) }.lAlg)
.collect()
} else {
Vec::new()
};
// Extract user verification requirement from authenticator options
let user_verification = if !decoded_request.pAuthenticatorOptions.is_null() {
let auth_options = &*decoded_request.pAuthenticatorOptions;
@@ -223,13 +306,16 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
} else {
None
};
// Extract excluded credentials from credential list (for make credential, these are credentials to exclude)
let excluded_credentials = parse_credential_list(&decoded_request.CredentialList);
if !excluded_credentials.is_empty() {
util::message(&format!("Found {} excluded credentials for make credential", excluded_credentials.len()));
util::message(&format!(
"Found {} excluded credentials for make credential",
excluded_credentials.len()
));
}
// Create Windows registration request
let registration_request = WindowsRegistrationRequest {
rpid: rpid.clone(),
@@ -241,40 +327,55 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
user_verification: user_verification.unwrap_or_default(),
supported_algorithms,
};
util::message(&format!("Make credential request - RP: {}, User: {}",
rpid,
registration_request.user_name));
util::message(&format!(
"Make credential request - RP: {}, User: {}",
rpid, registration_request.user_name
));
// Send registration request
if let Some(passkey_response) = send_registration_request(&transaction_id, &registration_request) {
util::message(&format!("Registration response received: {:?}", passkey_response));
if let Some(passkey_response) =
send_registration_request(&transaction_id, &registration_request)
{
util::message(&format!(
"Registration response received: {:?}",
passkey_response
));
// Create proper WebAuthn response from passkey_response
match passkey_response {
PasskeyResponse::RegistrationResponse { credential_id, attestation_object } => {
PasskeyResponse::RegistrationResponse {
credential_id,
attestation_object,
} => {
util::message("Creating WebAuthn make credential response");
match create_make_credential_response(credential_id, attestation_object) {
match create_make_credential_response(credential_id, attestation_object)
{
Ok(webauthn_response) => {
util::message("Successfully created WebAuthn response");
*response = webauthn_response;
HRESULT(0)
},
}
Err(e) => {
util::message(&format!("ERROR: Failed to create WebAuthn response: {}", e));
util::message(&format!(
"ERROR: Failed to create WebAuthn response: {}",
e
));
*response = ptr::null_mut();
HRESULT(-1)
}
}
},
}
PasskeyResponse::Error { message } => {
util::message(&format!("Registration request failed: {}", message));
*response = ptr::null_mut();
HRESULT(-1)
},
}
_ => {
util::message("ERROR: Unexpected response type for registration request");
util::message(
"ERROR: Unexpected response type for registration request",
);
*response = ptr::null_mut();
HRESULT(-1)
}
@@ -284,9 +385,12 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
*response = ptr::null_mut();
HRESULT(-1)
}
},
}
Err(e) => {
util::message(&format!("ERROR: Failed to decode make credential request: {}", e));
util::message(&format!(
"ERROR: Failed to decode make credential request: {}",
e
));
*response = ptr::null_mut();
HRESULT(-1)
}
@@ -299,35 +403,38 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
response: *mut *mut ExperimentalWebAuthnPluginOperationResponse,
) -> HRESULT {
util::message("EXPERIMENTAL_PluginGetAssertion() called");
// Validate input parameters
if request.is_null() || response.is_null() {
util::message("Invalid parameters passed to EXPERIMENTAL_PluginGetAssertion");
return HRESULT(-1);
}
let req = &*request;
let transaction_id = format!("{:?}", req.transaction_id);
util::message(&format!("Get assertion request - Transaction: {}", transaction_id));
util::message(&format!(
"Get assertion request - Transaction: {}",
transaction_id
));
if req.encoded_request_byte_count == 0 || req.encoded_request_pointer.is_null() {
util::message("ERROR: No encoded request data provided");
*response = ptr::null_mut();
return HRESULT(-1);
}
let encoded_request_slice = std::slice::from_raw_parts(
req.encoded_request_pointer,
req.encoded_request_byte_count as usize
req.encoded_request_pointer,
req.encoded_request_byte_count as usize,
);
// Try to decode the request using Windows API
match decode_get_assertion_request(encoded_request_slice) {
Ok(decoded_wrapper) => {
let decoded_request = decoded_wrapper.as_ref();
util::message("Successfully decoded get assertion request using Windows API");
// Extract RP information
let rpid = if decoded_request.pwszRpId.is_null() {
util::message("ERROR: RP ID is null");
@@ -343,20 +450,22 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
}
}
};
// Extract client data hash
let client_data_hash = if decoded_request.cbClientDataHash == 0 || decoded_request.pbClientDataHash.is_null() {
let client_data_hash = if decoded_request.cbClientDataHash == 0
|| decoded_request.pbClientDataHash.is_null()
{
util::message("ERROR: Client data hash is required for assertion");
*response = ptr::null_mut();
return HRESULT(-1);
} else {
let hash_slice = std::slice::from_raw_parts(
decoded_request.pbClientDataHash,
decoded_request.cbClientDataHash as usize
decoded_request.pbClientDataHash,
decoded_request.cbClientDataHash as usize,
);
hash_slice.to_vec()
};
// Extract user verification requirement from authenticator options
let user_verification = if !decoded_request.pAuthenticatorOptions.is_null() {
let auth_options = &*decoded_request.pAuthenticatorOptions;
@@ -368,10 +477,10 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
} else {
None
};
// Extract allowed credentials from credential list
let allowed_credentials = parse_credential_list(&decoded_request.CredentialList);
// Create Windows assertion request
let assertion_request = WindowsAssertionRequest {
rpid: rpid.clone(),
@@ -379,36 +488,60 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
allowed_credentials: allowed_credentials.clone(),
user_verification: user_verification.unwrap_or_default(),
};
util::message(&format!("Get assertion request - RP: {}, Allowed credentials: {}", rpid, allowed_credentials.len()));
util::message(&format!(
"Get assertion request - RP: {}, Allowed credentials: {}",
rpid,
allowed_credentials.len()
));
// Send assertion request
if let Some(passkey_response) = send_assertion_request(&transaction_id, &assertion_request) {
util::message(&format!("Assertion response received: {:?}", passkey_response));
if let Some(passkey_response) =
send_assertion_request(&transaction_id, &assertion_request)
{
util::message(&format!(
"Assertion response received: {:?}",
passkey_response
));
// Create proper WebAuthn response from passkey_response
match passkey_response {
PasskeyResponse::AssertionResponse { credential_id, authenticator_data, signature, user_handle } => {
PasskeyResponse::AssertionResponse {
credential_id,
authenticator_data,
signature,
user_handle,
} => {
util::message("Creating WebAuthn get assertion response");
match create_get_assertion_response(credential_id, authenticator_data, signature, user_handle) {
match create_get_assertion_response(
credential_id,
authenticator_data,
signature,
user_handle,
) {
Ok(webauthn_response) => {
util::message("Successfully created WebAuthn assertion response");
util::message(
"Successfully created WebAuthn assertion response",
);
*response = webauthn_response;
HRESULT(0)
},
}
Err(e) => {
util::message(&format!("ERROR: Failed to create WebAuthn assertion response: {}", e));
util::message(&format!(
"ERROR: Failed to create WebAuthn assertion response: {}",
e
));
*response = ptr::null_mut();
HRESULT(-1)
}
}
},
}
PasskeyResponse::Error { message } => {
util::message(&format!("Assertion request failed: {}", message));
*response = ptr::null_mut();
HRESULT(-1)
},
}
_ => {
util::message("ERROR: Unexpected response type for assertion request");
*response = ptr::null_mut();
@@ -420,9 +553,12 @@ impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Imp
*response = ptr::null_mut();
HRESULT(-1)
}
},
}
Err(e) => {
util::message(&format!("ERROR: Failed to decode get assertion request: {}", e));
util::message(&format!(
"ERROR: Failed to decode get assertion request: {}",
e
));
*response = ptr::null_mut();
HRESULT(-1)
}
@@ -445,11 +581,11 @@ impl IClassFactory_Impl for Factory_Impl {
iid: *const windows_core::GUID,
object: *mut *mut core::ffi::c_void,
) -> windows_core::Result<()> {
let unknown: IInspectable = PluginAuthenticatorComObject.into(); // TODO: IUnknown ?
let unknown: IInspectable = PluginAuthenticatorComObject.into(); // TODO: IUnknown ?
unsafe { unknown.query(iid, object).ok() }
}
fn LockServer(&self, lock: windows_core::BOOL) -> windows_core::Result<()> {
Ok(())
}
}
}

View File

@@ -9,23 +9,26 @@ use windows_core::*;
// New modular structure
mod assert;
mod com_buffer;
mod com_provider;
mod com_registration;
mod ipc;
mod make_credential;
mod sync;
mod ipc;
mod com_registration;
pub mod utils;
mod types;
pub mod utils;
mod webauthn;
mod com_provider;
mod com_buffer;
// Re-export main functionality
pub use sync::{send_sync_request, sync_credentials_to_windows, get_credentials_from_windows};
pub use ipc::{set_request_sender, send_passkey_request};
pub use types::{PasskeyRequest, PasskeyResponse, SyncedCredential, RequestEvent, RequestType, UserVerificationRequirement};
pub use assert::{WindowsAssertionRequest, WindowsRegistrationRequest};
pub use com_registration::{initialize_com_library, register_com_library, add_authenticator};
pub use assert::WindowsAssertionRequest;
pub use com_registration::{add_authenticator, initialize_com_library, register_com_library};
pub use ipc::{send_passkey_request, set_request_sender};
pub use make_credential::WindowsRegistrationRequest;
pub use sync::{get_credentials_from_windows, send_sync_request, sync_credentials_to_windows};
pub use types::{
PasskeyRequest, PasskeyResponse, RequestEvent, RequestType, SyncedCredential,
UserVerificationRequirement,
};
// Re-export utilities
pub use utils as util;
@@ -60,7 +63,7 @@ pub fn register() -> std::result::Result<(), String> {
let r = syncCredentials();
util::message(&format!("sync credentials: {:?}", r));
if let Err(e) = r {
util::message(&format!("syncCredentials failed: {}", e));
}
@@ -72,22 +75,17 @@ fn syncCredentials() -> std::result::Result<(), String> {
// Create a test credential using the new sync module with more realistic data
let test_credential = types::SyncedCredential {
credential_id: vec![
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
], // 32 byte credential ID
rp_id: "webauthn.io".to_string(),
user_name: "testuser".to_string(),
user_id: vec![0x75, 0x73, 0x65, 0x72, 0x31, 0x32, 0x33, 0x34], // "user1234" as bytes
user_handle: vec![0x75, 0x73, 0x65, 0x72, 0x31, 0x32, 0x33, 0x34], // "user1234" as bytes
};
let credentials = vec![test_credential];
// Use the sync module to sync credentials
sync_credentials_to_windows(credentials, CLSID)
}

View File

@@ -1,31 +1,44 @@
use serde_json;
use std::alloc::{alloc, Layout};
use std::ptr;
use serde_json;
use windows_core::{HRESULT, s};
use windows_core::{s, HRESULT};
use crate::com_provider::ExperimentalWebAuthnPluginOperationResponse;
use crate::types::*;
use crate::utils::{self as util, delay_load};
use crate::com_provider::ExperimentalWebAuthnPluginOperationResponse;
use crate::assert::{WindowsRegistrationRequest, parse_credential_list};
use crate::webauthn::WEBAUTHN_CREDENTIAL_LIST;
/// Windows WebAuthn registration request context
#[derive(Debug, Clone)]
pub struct WindowsRegistrationRequest {
pub rpid: String,
pub user_id: Vec<u8>,
pub user_name: String,
pub user_display_name: Option<String>,
pub client_data_hash: Vec<u8>,
pub excluded_credentials: Vec<Vec<u8>>,
pub user_verification: UserVerificationRequirement,
pub supported_algorithms: Vec<i32>,
}
// Windows API types for WebAuthn (from webauthn.h.sample)
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_RP_ENTITY_INFORMATION {
pub dwVersion: u32,
pub pwszId: *const u16, // PCWSTR
pub pwszName: *const u16, // PCWSTR
pub pwszIcon: *const u16, // PCWSTR
pub pwszId: *const u16, // PCWSTR
pub pwszName: *const u16, // PCWSTR
pub pwszIcon: *const u16, // PCWSTR
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_USER_ENTITY_INFORMATION {
pub dwVersion: u32,
pub cbId: u32, // DWORD
pub pbId: *const u8, // PBYTE
pub pwszName: *const u16, // PCWSTR
pub pwszIcon: *const u16, // PCWSTR
pub cbId: u32, // DWORD
pub pbId: *const u8, // PBYTE
pub pwszName: *const u16, // PCWSTR
pub pwszIcon: *const u16, // PCWSTR
pub pwszDisplayName: *const u16, // PCWSTR
}
@@ -33,8 +46,8 @@ pub struct WEBAUTHN_USER_ENTITY_INFORMATION {
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
pub dwVersion: u32,
pub pwszCredentialType: *const u16, // LPCWSTR
pub lAlg: i32, // LONG - COSE algorithm identifier
pub pwszCredentialType: *const u16, // LPCWSTR
pub lAlg: i32, // LONG - COSE algorithm identifier
}
#[repr(C)]
@@ -44,23 +57,6 @@ pub struct WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
pub pCredentialParameters: *const WEBAUTHN_COSE_CREDENTIAL_PARAMETER,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_CREDENTIAL_EX {
pub dwVersion: u32,
pub cbId: u32,
pub pbId: *const u8,
pub pwszCredentialType: *const u16, // LPCWSTR
pub dwTransports: u32,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_CREDENTIAL_LIST {
pub cCredentials: u32,
pub ppCredentials: *const *const WEBAUTHN_CREDENTIAL_EX,
}
// Make Credential Request structure (from sample header)
#[repr(C)]
#[derive(Debug, Copy, Clone)]
@@ -72,15 +68,17 @@ pub struct EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST {
pub pbClientDataHash: *const u8,
pub pRpInformation: *const WEBAUTHN_RP_ENTITY_INFORMATION,
pub pUserInformation: *const WEBAUTHN_USER_ENTITY_INFORMATION,
pub WebAuthNCredentialParameters: WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, // Matches C++ sample
pub WebAuthNCredentialParameters: WEBAUTHN_COSE_CREDENTIAL_PARAMETERS, // Matches C++ sample
pub CredentialList: WEBAUTHN_CREDENTIAL_LIST,
pub cbCborExtensionsMap: u32,
pub pbCborExtensionsMap: *const u8,
pub pAuthenticatorOptions: *const crate::webauthn::ExperimentalWebAuthnCtapCborAuthenticatorOptions,
pub pAuthenticatorOptions:
*const crate::webauthn::ExperimentalWebAuthnCtapCborAuthenticatorOptions,
// Add other fields as needed...
}
pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST = *mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST;
pub type PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST =
*mut EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST;
// Windows API function signatures
type EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequestFn = unsafe extern "stdcall" fn(
@@ -100,10 +98,13 @@ pub struct DecodedMakeCredentialRequest {
}
impl DecodedMakeCredentialRequest {
fn new(ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST, free_fn: Option<EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequestFn>) -> Self {
fn new(
ptr: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST,
free_fn: Option<EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequestFn>,
) -> Self {
Self { ptr, free_fn }
}
pub fn as_ref(&self) -> &EXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST {
unsafe { &*self.ptr }
}
@@ -114,16 +115,20 @@ impl Drop for DecodedMakeCredentialRequest {
if !self.ptr.is_null() {
if let Some(free_fn) = self.free_fn {
util::message("Freeing decoded make credential request");
unsafe { free_fn(self.ptr); }
unsafe {
free_fn(self.ptr);
}
}
}
}
}
// Function to decode make credential request using Windows API
pub unsafe fn decode_make_credential_request(encoded_request: &[u8]) -> Result<DecodedMakeCredentialRequest, String> {
pub unsafe fn decode_make_credential_request(
encoded_request: &[u8],
) -> Result<DecodedMakeCredentialRequest, String> {
util::message("Attempting to decode make credential request using Windows API");
// Try to load the Windows API decode function
let decode_fn = match delay_load::<EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequestFn>(
s!("webauthn.dll"),
@@ -131,102 +136,120 @@ pub unsafe fn decode_make_credential_request(encoded_request: &[u8]) -> Result<D
) {
Some(func) => func,
None => {
return Err("Failed to load EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest from webauthn.dll".to_string());
return Err(
"Failed to load EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest from webauthn.dll"
.to_string(),
);
}
};
// Try to load the free function (optional, might not be available in all versions)
let free_fn = delay_load::<EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequestFn>(
s!("webauthn.dll"),
s!("EXPERIMENTAL_WebAuthNFreeDecodedMakeCredentialRequest"),
);
// Prepare parameters for the API call
let cb_encoded = encoded_request.len() as u32;
let pb_encoded = encoded_request.as_ptr();
let mut pp_make_credential_request: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST = std::ptr::null_mut();
let mut pp_make_credential_request: PEXPERIMENTAL_WEBAUTHN_CTAPCBOR_MAKE_CREDENTIAL_REQUEST =
std::ptr::null_mut();
// Call the Windows API function
let result = decode_fn(
cb_encoded,
pb_encoded,
&mut pp_make_credential_request,
);
let result = decode_fn(cb_encoded, pb_encoded, &mut pp_make_credential_request);
// Check if the call succeeded (following C++ THROW_IF_FAILED pattern)
if result.is_err() {
util::message(&format!("ERROR: EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest failed with HRESULT: 0x{:08x}", result.0));
return Err(format!("Windows API call failed with HRESULT: 0x{:08x}", result.0));
util::message(&format!(
"ERROR: EXPERIMENTAL_WebAuthNDecodeMakeCredentialRequest failed with HRESULT: 0x{:08x}",
result.0
));
return Err(format!(
"Windows API call failed with HRESULT: 0x{:08x}",
result.0
));
}
if pp_make_credential_request.is_null() {
util::message("ERROR: Windows API succeeded but returned null pointer");
return Err("Windows API returned null pointer".to_string());
}
Ok(DecodedMakeCredentialRequest::new(pp_make_credential_request, free_fn))
Ok(DecodedMakeCredentialRequest::new(
pp_make_credential_request,
free_fn,
))
}
/// Helper for registration requests
pub fn send_registration_request(transaction_id: &str, request: &WindowsRegistrationRequest) -> Option<PasskeyResponse> {
pub fn send_registration_request(
transaction_id: &str,
request: &WindowsRegistrationRequest,
) -> Option<PasskeyResponse> {
util::message(&format!("Registration request data - RP ID: {}, User ID: {} bytes, User name: {}, Client data hash: {} bytes, Algorithms: {:?}, Excluded credentials: {}",
request.rpid, request.user_id.len(), request.user_name, request.client_data_hash.len(), request.supported_algorithms, request.excluded_credentials.len()));
let passkey_request = PasskeyRegistrationRequest {
rp_id: request.rpid.clone(),
transaction_id: transaction_id.to_string(),
user_id: request.user_id.clone(),
user_handle: request.user_id.clone(),
user_name: request.user_name.clone(),
client_data_hash: request.client_data_hash.clone(),
user_verification: request.user_verification.clone(),
supported_algorithms: request.supported_algorithms.clone(),
excluded_credentials: request.excluded_credentials.clone(),
};
match serde_json::to_string(&passkey_request) {
Ok(request_json) => {
util::message(&format!("Sending registration request: {}", request_json));
crate::ipc::send_passkey_request(RequestType::Registration, request_json, &request.rpid)
},
}
Err(e) => {
util::message(&format!("ERROR: Failed to serialize registration request: {}", e));
util::message(&format!(
"ERROR: Failed to serialize registration request: {}",
e
));
None
}
}
}
/// Creates a WebAuthn make credential response from Bitwarden's registration response
pub unsafe fn create_make_credential_response(credential_id: Vec<u8>, attestation_object: Vec<u8>) -> std::result::Result<*mut ExperimentalWebAuthnPluginOperationResponse, HRESULT> {
pub unsafe fn create_make_credential_response(
credential_id: Vec<u8>,
attestation_object: Vec<u8>,
) -> std::result::Result<*mut ExperimentalWebAuthnPluginOperationResponse, HRESULT> {
// Use the attestation object directly as the encoded response
let response_data = attestation_object;
let response_len = response_data.len();
// Allocate memory for the response data
let layout = Layout::from_size_align(response_len, 1).map_err(|_| HRESULT(-1))?;
let response_ptr = alloc(layout);
if response_ptr.is_null() {
return Err(HRESULT(-1));
}
// Copy response data
ptr::copy_nonoverlapping(response_data.as_ptr(), response_ptr, response_len);
// Allocate memory for the response structure
let response_layout = Layout::new::<ExperimentalWebAuthnPluginOperationResponse>();
let operation_response_ptr = alloc(response_layout) as *mut ExperimentalWebAuthnPluginOperationResponse;
let operation_response_ptr =
alloc(response_layout) as *mut ExperimentalWebAuthnPluginOperationResponse;
if operation_response_ptr.is_null() {
return Err(HRESULT(-1));
}
// Initialize the response
ptr::write(operation_response_ptr, ExperimentalWebAuthnPluginOperationResponse {
encoded_response_byte_count: response_len as u32,
encoded_response_pointer: response_ptr,
});
ptr::write(
operation_response_ptr,
ExperimentalWebAuthnPluginOperationResponse {
encoded_response_byte_count: response_len as u32,
encoded_response_pointer: response_ptr,
},
);
Ok(operation_response_ptr)
}

View File

@@ -1,103 +1,128 @@
use serde_json;
use hex;
use serde_json;
use crate::ipc::send_passkey_request;
use crate::types::*;
use crate::utils::{self as util, wstr_to_string};
use crate::webauthn::*;
use crate::ipc::send_passkey_request;
/// Helper for sync requests - requests credentials from Electron for a specific RP ID
pub fn send_sync_request(rpid: &str) -> Option<PasskeyResponse> {
util::message(&format!("[SYNC] send_sync_request called for RP ID: {}", rpid));
util::message(&format!(
"[SYNC] send_sync_request called for RP ID: {}",
rpid
));
let request = PasskeySyncRequest {
rp_id: rpid.to_string(),
};
util::message(&format!("[SYNC] Created sync request for RP ID: {}", rpid));
match serde_json::to_string(&request) {
Ok(request_json) => {
util::message(&format!("[SYNC] Serialized sync request to JSON: {}", request_json));
util::message(&format!(
"[SYNC] Serialized sync request to JSON: {}",
request_json
));
util::message(&format!("[SYNC] Sending sync request to Electron via IPC"));
let response = send_passkey_request(RequestType::Sync, request_json, rpid);
match &response {
Some(resp) => util::message(&format!("[SYNC] Received response from Electron: {:?}", resp)),
Some(resp) => util::message(&format!(
"[SYNC] Received response from Electron: {:?}",
resp
)),
None => util::message("[SYNC] No response received from Electron"),
}
response
},
}
Err(e) => {
util::message(&format!("[SYNC] ERROR: Failed to serialize sync request: {}", e));
util::message(&format!(
"[SYNC] ERROR: Failed to serialize sync request: {}",
e
));
None
}
}
}
/// Initiates credential sync from Electron to Windows - called when Electron wants to push credentials to Windows
pub fn sync_credentials_to_windows(credentials: Vec<SyncedCredential>, plugin_clsid: &str) -> Result<(), String> {
util::message(&format!("[SYNC_TO_WIN] sync_credentials_to_windows called with {} credentials for plugin CLSID: {}", credentials.len(), plugin_clsid));
pub fn sync_credentials_to_windows(
credentials: Vec<SyncedCredential>,
plugin_clsid: &str,
) -> Result<(), String> {
util::message(&format!(
"[SYNC_TO_WIN] sync_credentials_to_windows called with {} credentials for plugin CLSID: {}",
credentials.len(),
plugin_clsid
));
// Format CLSID with curly braces to match Windows registration format
let formatted_clsid = format!("{{{}}}", plugin_clsid);
if credentials.is_empty() {
util::message("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync");
}
// Convert Bitwarden credentials to Windows credential details
let mut win_credentials = Vec::new();
for (i, cred) in credentials.iter().enumerate() {
let truncated_cred_id = if cred.credential_id.len() > 16 {
format!("{}...", hex::encode(&cred.credential_id[..16]))
} else {
hex::encode(&cred.credential_id)
};
let truncated_user_id = if cred.user_id.len() > 16 {
format!("{}...", hex::encode(&cred.user_id[..16]))
let truncated_user_id = if cred.user_handle.len() > 16 {
format!("{}...", hex::encode(&cred.user_handle[..16]))
} else {
hex::encode(&cred.user_id)
hex::encode(&cred.user_handle)
};
util::message(&format!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {} ({} bytes), User ID: {} ({} bytes)",
i + 1, cred.rp_id, cred.user_name, truncated_cred_id, cred.credential_id.len(), truncated_user_id, cred.user_id.len()));
i + 1, cred.rp_id, cred.user_name, truncated_cred_id, cred.credential_id.len(), truncated_user_id, cred.user_handle.len()));
let win_cred = ExperimentalWebAuthnPluginCredentialDetails::create_from_bytes(
cred.credential_id.clone(), // Pass raw bytes
cred.rp_id.clone(),
cred.rp_id.clone(), // Use RP ID as friendly name for now
cred.user_id.clone(), // Pass raw bytes
cred.rp_id.clone(), // Use RP ID as friendly name for now
cred.user_handle.clone(), // Pass raw bytes
cred.user_name.clone(),
cred.user_name.clone(), // Use user name as display name for now
);
win_credentials.push(win_cred);
util::message(&format!("[SYNC_TO_WIN] Converted credential {} to Windows format", i + 1));
util::message(&format!(
"[SYNC_TO_WIN] Converted credential {} to Windows format",
i + 1
));
}
// Create credentials list
let credentials_list = ExperimentalWebAuthnPluginCredentialDetailsList::create(
formatted_clsid.clone(),
win_credentials
win_credentials,
);
// First try to remove all existing credentials for this plugin
util::message("Attempting to remove all existing credentials before sync...");
match remove_all_credentials(formatted_clsid.clone()) {
Ok(()) => {
util::message("Successfully removed existing credentials");
},
}
Err(e) if e.contains("can't be loaded") => {
util::message("RemoveAllCredentials function not available - this is expected for some Windows versions");
// This is fine, the function might not exist in all versions
},
}
Err(e) => {
util::message(&format!("Warning: Failed to remove existing credentials: {}", e));
util::message(&format!(
"Warning: Failed to remove existing credentials: {}",
e
));
// Continue anyway, as this might be the first sync or an older Windows version
}
}
// Add the new credentials (only if we have any)
if credentials.is_empty() {
util::message("No credentials to add to Windows - sync completed successfully");
@@ -108,9 +133,12 @@ pub fn sync_credentials_to_windows(credentials: Vec<SyncedCredential>, plugin_cl
Ok(()) => {
util::message("Successfully synced credentials to Windows");
Ok(())
},
}
Err(e) => {
util::message(&format!("ERROR: Failed to add credentials to Windows: {}", e));
util::message(&format!(
"ERROR: Failed to add credentials to Windows: {}",
e
));
Err(e)
}
}
@@ -119,89 +147,102 @@ pub fn sync_credentials_to_windows(credentials: Vec<SyncedCredential>, plugin_cl
/// Gets all credentials from Windows for a specific plugin - used when Electron requests current state
pub fn get_credentials_from_windows(plugin_clsid: &str) -> Result<Vec<SyncedCredential>, String> {
util::message(&format!("Getting all credentials from Windows for plugin CLSID: {}", plugin_clsid));
util::message(&format!(
"Getting all credentials from Windows for plugin CLSID: {}",
plugin_clsid
));
// Format CLSID with curly braces to match Windows registration format
let formatted_clsid = format!("{{{}}}", plugin_clsid);
match get_all_credentials(formatted_clsid) {
Ok(Some(credentials_list)) => {
util::message(&format!("Retrieved {} credentials from Windows", credentials_list.credential_count));
util::message(&format!(
"Retrieved {} credentials from Windows",
credentials_list.credential_count
));
let mut bitwarden_credentials = Vec::new();
// Convert Windows credentials to Bitwarden format
unsafe {
let credentials_array = std::slice::from_raw_parts(
credentials_list.credentials,
credentials_list.credential_count as usize
credentials_list.credential_count as usize,
);
for &cred_ptr in credentials_array {
if !cred_ptr.is_null() {
let cred = &*cred_ptr;
// Convert credential data back to Bitwarden format
let credential_id = if cred.credential_id_byte_count > 0 && !cred.credential_id_pointer.is_null() {
let credential_id = if cred.credential_id_byte_count > 0
&& !cred.credential_id_pointer.is_null()
{
let id_slice = std::slice::from_raw_parts(
cred.credential_id_pointer,
cred.credential_id_byte_count as usize
cred.credential_id_byte_count as usize,
);
// Assume it's hex-encoded, try to decode
hex::decode(std::str::from_utf8(id_slice).unwrap_or("")).unwrap_or_else(|_| id_slice.to_vec())
hex::decode(std::str::from_utf8(id_slice).unwrap_or(""))
.unwrap_or_else(|_| id_slice.to_vec())
} else {
Vec::new()
};
let rp_id = if !cred.rpid.is_null() {
wstr_to_string(cred.rpid).unwrap_or_default()
} else {
String::new()
};
let user_name = if !cred.user_name.is_null() {
wstr_to_string(cred.user_name).unwrap_or_default()
} else {
String::new()
};
let user_id = if cred.user_id_byte_count > 0 && !cred.user_id_pointer.is_null() {
// Convert from UTF-8 bytes back to Vec<u8>
let user_id_slice = std::slice::from_raw_parts(
cred.user_id_pointer,
cred.user_id_byte_count as usize
);
// Try to decode as hex string, or use raw bytes
let user_id_str = std::str::from_utf8(user_id_slice).unwrap_or("");
hex::decode(user_id_str).unwrap_or_else(|_| user_id_slice.to_vec())
} else {
Vec::new()
};
let user_id =
if cred.user_id_byte_count > 0 && !cred.user_id_pointer.is_null() {
// Convert from UTF-8 bytes back to Vec<u8>
let user_id_slice = std::slice::from_raw_parts(
cred.user_id_pointer,
cred.user_id_byte_count as usize,
);
// Try to decode as hex string, or use raw bytes
let user_id_str = std::str::from_utf8(user_id_slice).unwrap_or("");
hex::decode(user_id_str).unwrap_or_else(|_| user_id_slice.to_vec())
} else {
Vec::new()
};
let synced_cred = SyncedCredential {
credential_id,
rp_id,
user_name,
user_id,
user_handle: user_id,
};
util::message(&format!("Converted Windows credential: RP ID: {}, User: {}, Credential ID: {} bytes",
synced_cred.rp_id, synced_cred.user_name, synced_cred.credential_id.len()));
bitwarden_credentials.push(synced_cred);
}
}
}
Ok(bitwarden_credentials)
},
}
Ok(None) => {
util::message("No credentials found in Windows");
Ok(Vec::new())
},
}
Err(e) => {
util::message(&format!("ERROR: Failed to get credentials from Windows: {}", e));
util::message(&format!(
"ERROR: Failed to get credentials from Windows: {}",
e
));
Err(e)
}
}
}
}

View File

@@ -5,7 +5,7 @@ use tokio::sync::oneshot;
#[serde(rename_all = "lowercase")]
pub enum UserVerificationRequirement {
Required,
Preferred,
Preferred,
Discouraged,
}
@@ -53,12 +53,12 @@ pub struct PasskeyAssertionRequest {
pub struct PasskeyRegistrationRequest {
pub rp_id: String,
pub transaction_id: String,
pub user_id: Vec<u8>,
pub user_handle: Vec<u8>,
pub user_name: String,
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerificationRequirement,
pub supported_algorithms: Vec<i32>, // COSE algorithm identifiers
pub excluded_credentials: Vec<Vec<u8>>, // Credentials to exclude from creation
pub supported_algorithms: Vec<i32>, // COSE algorithm identifiers
pub excluded_credentials: Vec<Vec<u8>>, // Credentials to exclude from creation
}
/// Sync request structure
@@ -80,26 +80,22 @@ pub enum PasskeyRequest {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PasskeyResponse {
#[serde(rename = "assertion_response",rename_all = "camelCase")]
#[serde(rename = "assertion_response", rename_all = "camelCase")]
AssertionResponse {
credential_id: Vec<u8>,
authenticator_data: Vec<u8>,
signature: Vec<u8>,
user_handle: Vec<u8>,
},
#[serde(rename = "registration_response",rename_all = "camelCase")]
#[serde(rename = "registration_response", rename_all = "camelCase")]
RegistrationResponse {
credential_id: Vec<u8>,
attestation_object: Vec<u8>,
},
#[serde(rename = "sync_response",rename_all = "camelCase")]
SyncResponse {
credentials: Vec<SyncedCredential>,
},
#[serde(rename = "error",rename_all = "camelCase")]
Error {
message: String,
},
#[serde(rename = "sync_response", rename_all = "camelCase")]
SyncResponse { credentials: Vec<SyncedCredential> },
#[serde(rename = "error", rename_all = "camelCase")]
Error { message: String },
}
/// Credential data for sync operations
@@ -109,7 +105,7 @@ pub struct SyncedCredential {
pub credential_id: Vec<u8>,
pub rp_id: String,
pub user_name: String,
pub user_id: Vec<u8>,
pub user_handle: Vec<u8>,
}
/// Request type enumeration for type discrimination
@@ -126,4 +122,4 @@ pub struct RequestEvent {
pub request_type: RequestType,
pub request_json: String,
pub response_sender: oneshot::Sender<PasskeyResponse>,
}
}

View File

@@ -1,10 +1,10 @@
use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;
use std::fs::{OpenOptions, create_dir_all};
use std::fs::{create_dir_all, OpenOptions};
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use windows::Win32::Foundation::*;
use windows::Win32::System::LibraryLoader::*;
@@ -59,17 +59,13 @@ impl WindowsString for String {
pub fn file_log(msg: &str) {
let log_path = "C:\\temp\\bitwarden_com_debug.log";
// Create the temp directory if it doesn't exist
if let Some(parent) = Path::new(log_path).parent() {
let _ = create_dir_all(parent);
}
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
{
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_path) {
let now = SystemTime::now();
let timestamp = match now.duration_since(UNIX_EPOCH) {
Ok(duration) => {
@@ -79,10 +75,10 @@ pub fn file_log(msg: &str) {
let mins = (total_secs / 60) % 60;
let hours = (total_secs / 3600) % 24;
format!("{:02}:{:02}:{:02}.{:03}", hours, mins, secs, millis)
},
Err(_) => "??:??:??.???".to_string()
}
Err(_) => "??:??:??.???".to_string(),
};
let _ = writeln!(file, "[{}] {}", timestamp, msg);
}
}
@@ -92,18 +88,20 @@ pub fn message(message: &str) {
}
// Helper function to convert Windows wide string (UTF-16) to Rust String
pub unsafe fn wstr_to_string(wstr_ptr: *const u16) -> std::result::Result<String, std::string::FromUtf16Error> {
pub unsafe fn wstr_to_string(
wstr_ptr: *const u16,
) -> std::result::Result<String, std::string::FromUtf16Error> {
if wstr_ptr.is_null() {
return Ok(String::new());
}
// Find the length of the null-terminated wide string
let mut len = 0;
while *wstr_ptr.add(len) != 0 {
len += 1;
}
// Convert to Rust string
let wide_slice = std::slice::from_raw_parts(wstr_ptr, len);
String::from_utf16(wide_slice)
}
}

View File

@@ -407,10 +407,27 @@ pub fn remove_all_credentials(
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_CREDENTIAL_EX {
pub dwVersion: u32,
pub cbId: u32,
pub pbId: *const u8,
pub pwszCredentialType: *const u16, // LPCWSTR
pub dwTransports: u32,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct WEBAUTHN_CREDENTIAL_LIST {
pub cCredentials: u32,
pub ppCredentials: *const *const WEBAUTHN_CREDENTIAL_EX,
}
// Forward declarations for Windows types we need
type WEBAUTHN_ASSERTION = *const u8; // Placeholder - would need actual definition
type PCWEBAUTHN_USER_ENTITY_INFORMATION = *const u8; // Placeholder - would need actual definition
type WEBAUTHN_CREDENTIAL_LIST = *const u8; // Placeholder - would need actual definition
//type WEBAUTHN_CREDENTIAL_LIST = *const u8; // Placeholder - would need actual definition
type EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_AUTHENTICATOR_OPTIONS = *const u8; // Placeholder
type EXPERIMENTAL_PWEBAUTHN_CTAPCBOR_HMAC_SALT_EXTENSION = *const u8; // Placeholder

View File

@@ -121,7 +121,7 @@ export class NativeAutofillMain {
rpId: request.rpId,
clientDataHash: request.clientDataHash,
userName: request.userName,
userHandle: request.userId,
userHandle: request.userHandle,
userVerification: autofill.UserVerification.Required,
supportedAlgorithms: request.supportedAlgorithms,
windowXy: { x: 400, y: 400 },
@@ -221,7 +221,7 @@ export class NativeAutofillMain {
private async sendAndOptionallyWait<T = any>(
channel: string,
data: any,
options?: { waitForResponse?: boolean; timeout?: number; responseChannel?: string },
options?: { waitForResponse?: boolean; timeout?: number },
): Promise<T | void> {
if (!options?.waitForResponse) {
// Just send without waiting for response (existing behavior)
@@ -232,7 +232,6 @@ export class NativeAutofillMain {
// Use clientId and sequenceNumber as the tracking key
const trackingKey = `${data.clientId}_${data.sequenceNumber}`;
const responseChannel = options.responseChannel || `${channel}_response`;
const timeout = options.timeout || 30000; // 30 second default timeout
// Send the original data without adding requestId
@@ -268,27 +267,29 @@ export class NativeAutofillMain {
async init() {
this.initWindows();
ipcMain.handle("autofill.syncPasskeys", async (event, data: NativeAutofillSyncParams): Promise<string> => {
this.logService.info("autofill.syncPasskeys", data);
const { credentials } = data;
const mapped = credentials.map((cred: NativeAutofillFido2Credential) => {
const x: passkey_authenticator.SyncedCredential = {
credentialId: cred.credentialId,
rpId: cred.rpId,
userName: cred.userName,
userId: cred.userHandle
};
this.logService.info("Mapped credential:", x);
return x;
});
ipcMain.handle(
"autofill.syncPasskeys",
async (event, data: NativeAutofillSyncParams): Promise<string> => {
this.logService.info("autofill.syncPasskeys", data);
const { credentials } = data;
const mapped = credentials.map((cred: NativeAutofillFido2Credential) => {
const x: passkey_authenticator.SyncedCredential = {
credentialId: cred.credentialId,
rpId: cred.rpId,
userName: cred.userName,
userHandle: cred.userHandle,
};
this.logService.info("Mapped credential:", x);
return x;
});
this.logService.info("Syncing passkeys to Windows:", mapped);
passkey_authenticator.syncCredentialsToWindows(mapped);
this.logService.info("Syncing passkeys to Windows:", mapped);
return "worked";
});
passkey_authenticator.syncCredentialsToWindows(mapped);
return "worked";
},
);
ipcMain.handle(
"autofill.runCommand",
@@ -383,7 +384,6 @@ export class NativeAutofillMain {
}
});
ipcMain.on("autofill.completePasskeySync", (event, data) => {
this.logService.warning("autofill.completePasskeySync", data);
const { clientId, sequenceNumber, response, requestId } = data;