Files
fluxer/fluxer_desktop/native/webauthn/src/windows.rs
T

305 lines
11 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-or-later
use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;
use napi::Result;
use windows::Win32::Foundation::{HWND, LPARAM};
use windows::Win32::Networking::WindowsWebServices::{
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM, WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM,
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS,
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION,
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS,
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION, WEBAUTHN_CLIENT_DATA,
WEBAUTHN_CLIENT_DATA_CURRENT_VERSION, WEBAUTHN_COSE_CREDENTIAL_PARAMETER,
WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION, WEBAUTHN_COSE_CREDENTIAL_PARAMETERS,
WEBAUTHN_CREDENTIAL, WEBAUTHN_CREDENTIAL_CURRENT_VERSION, WEBAUTHN_CREDENTIALS,
WEBAUTHN_CTAP_TRANSPORT_INTERNAL, WEBAUTHN_CTAP_TRANSPORT_USB, WEBAUTHN_HASH_ALGORITHM_SHA_256,
WEBAUTHN_LARGE_BLOB_SUPPORT_NONE, WEBAUTHN_RP_ENTITY_INFORMATION,
WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION, WEBAUTHN_USER_ENTITY_INFORMATION,
WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION, WebAuthNAuthenticatorGetAssertion,
WebAuthNAuthenticatorMakeCredential, WebAuthNFreeAssertion, WebAuthNFreeCredentialAttestation,
WebAuthNGetApiVersionNumber, WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable,
};
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetDesktopWindow, GetForegroundWindow, GetWindowThreadProcessId, IsWindow,
IsWindowVisible,
};
use windows_core::{BOOL, PCWSTR};
use crate::common::{
CREATE_PREFIX, CreateInput, CreateResult, DescriptorInput, GET_PREFIX, GetInput, GetResult,
ceremony_error,
};
pub fn api_version() -> u32 {
unsafe { WebAuthNGetApiVersionNumber() }
}
pub fn is_user_verifying_platform_authenticator_available() -> bool {
match unsafe { WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable() } {
Ok(b) => b.as_bool(),
Err(_) => false,
}
}
fn to_wide(value: &str) -> Vec<u16> {
OsString::from(value).encode_wide().chain(Some(0)).collect()
}
fn window_from_handle(handle: u64) -> Option<HWND> {
if handle == 0 {
return None;
}
let hwnd = HWND(handle as usize as *mut core::ffi::c_void);
if unsafe { IsWindow(Some(hwnd)) }.as_bool() {
Some(hwnd)
} else {
None
}
}
fn window_process_id(hwnd: HWND) -> u32 {
let mut pid = 0u32;
unsafe { GetWindowThreadProcessId(hwnd, Some(&mut pid)) };
pid
}
unsafe extern "system" fn collect_own_visible_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
let slot = unsafe { &mut *(lparam.0 as *mut Option<HWND>) };
if window_process_id(hwnd) == unsafe { GetCurrentProcessId() }
&& unsafe { IsWindowVisible(hwnd) }.as_bool()
{
*slot = Some(hwnd);
return BOOL(0);
}
BOOL(1)
}
fn interaction_window(handle: u64) -> HWND {
if let Some(hwnd) = window_from_handle(handle) {
return hwnd;
}
let current_pid = unsafe { GetCurrentProcessId() };
let foreground = unsafe { GetForegroundWindow() };
if !foreground.is_invalid() && window_process_id(foreground) == current_pid {
return foreground;
}
let mut own_window: Option<HWND> = None;
let _ = unsafe {
EnumWindows(
Some(collect_own_visible_window),
LPARAM(&raw mut own_window as isize),
)
};
if let Some(hwnd) = own_window {
return hwnd;
}
if !foreground.is_invalid() {
return foreground;
}
unsafe { GetDesktopWindow() }
}
const PUBLIC_KEY_WIDE: &[u16] = &[
'p' as u16, 'u' as u16, 'b' as u16, 'l' as u16, 'i' as u16, 'c' as u16, '-' as u16, 'k' as u16,
'e' as u16, 'y' as u16, 0,
];
fn build_credential_list(
descriptors: &[DescriptorInput],
) -> (Vec<WEBAUTHN_CREDENTIAL>, WEBAUTHN_CREDENTIALS) {
let mut backing: Vec<WEBAUTHN_CREDENTIAL> = descriptors
.iter()
.map(|d| WEBAUTHN_CREDENTIAL {
dwVersion: WEBAUTHN_CREDENTIAL_CURRENT_VERSION,
cbId: d.id.len() as u32,
pbId: d.id.as_ptr() as *mut u8,
pwszCredentialType: PCWSTR(PUBLIC_KEY_WIDE.as_ptr()),
})
.collect();
let list = WEBAUTHN_CREDENTIALS {
cCredentials: backing.len() as u32,
pCredentials: if backing.is_empty() {
std::ptr::null_mut()
} else {
backing.as_mut_ptr()
},
};
(backing, list)
}
fn copy_buffer(ptr: *const u8, len: u32) -> Result<Vec<u8>> {
if len == 0 {
return Ok(Vec::new());
}
if ptr.is_null() {
return Err(ceremony_error(CREATE_PREFIX, "InvalidWebAuthnResult"));
}
let slice = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
Ok(slice.to_vec())
}
pub fn make_credential(input: &mut CreateInput) -> Result<CreateResult> {
let rp_id_w = to_wide(&input.rp_id);
let rp_name_w = to_wide(&input.rp_name);
let user_name_w = to_wide(&input.user_name);
let display_w = to_wide(&input.user_display_name);
let rp = WEBAUTHN_RP_ENTITY_INFORMATION {
dwVersion: WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION,
pwszId: PCWSTR(rp_id_w.as_ptr()),
pwszName: PCWSTR(rp_name_w.as_ptr()),
pwszIcon: PCWSTR::null(),
};
let user = WEBAUTHN_USER_ENTITY_INFORMATION {
dwVersion: WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION,
cbId: input.user_id.len() as u32,
pbId: input.user_id.as_ptr() as *mut u8,
pwszName: PCWSTR(user_name_w.as_ptr()),
pwszIcon: PCWSTR::null(),
pwszDisplayName: PCWSTR(display_w.as_ptr()),
};
let mut pub_key_params: Vec<WEBAUTHN_COSE_CREDENTIAL_PARAMETER> = input
.pub_key_algs
.iter()
.map(|&alg| WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
dwVersion: WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION,
pwszCredentialType: PCWSTR(PUBLIC_KEY_WIDE.as_ptr()),
lAlg: alg,
})
.collect();
let params = WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
cCredentialParameters: pub_key_params.len() as u32,
pCredentialParameters: pub_key_params.as_mut_ptr(),
};
let client_data = WEBAUTHN_CLIENT_DATA {
dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
cbClientDataJSON: input.client_data_json.len() as u32,
pbClientDataJSON: input.client_data_json.as_ptr() as *mut u8,
pwszHashAlgId: WEBAUTHN_HASH_ALGORITHM_SHA_256,
};
let (_exclude_keep, exclude_list) = build_credential_list(&input.exclude_credentials);
let options = WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS {
dwVersion: api_version()
.min(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION),
dwTimeoutMilliseconds: input.timeout_ms,
CredentialList: exclude_list,
dwAuthenticatorAttachment: input.authenticator_attachment,
bRequireResidentKey: BOOL(input.require_resident_key as i32),
dwUserVerificationRequirement: input.user_verification,
dwAttestationConveyancePreference: input.attestation,
dwEnterpriseAttestation: input.enterprise_attestation,
dwLargeBlobSupport: WEBAUTHN_LARGE_BLOB_SUPPORT_NONE,
bPreferResidentKey: BOOL(input.prefer_resident_key as i32),
..Default::default()
};
let attestation_ptr = unsafe {
WebAuthNAuthenticatorMakeCredential(
interaction_window(input.window_handle),
&rp,
&user,
&params,
&client_data,
Some(&options),
)
};
let attestation_ptr = attestation_ptr
.map_err(|err| ceremony_error(CREATE_PREFIX, &format!("WebAuthnCallFailed: {err}")))?;
if attestation_ptr.is_null() {
return Err(ceremony_error(CREATE_PREFIX, "InvalidWebAuthnResult"));
}
let att = unsafe { &*attestation_ptr };
let raw_id = copy_buffer(att.pbCredentialId, att.cbCredentialId)?;
let attestation_object = copy_buffer(att.pbAttestationObject, att.cbAttestationObject)?;
let used_transport = if att.dwVersion >= 3 {
att.dwUsedTransport
} else {
0
};
unsafe { WebAuthNFreeCredentialAttestation(Some(attestation_ptr)) };
Ok(CreateResult {
raw_id,
attestation_object,
client_data_json: input.client_data_json.clone(),
used_transport,
})
}
pub fn get_assertion(input: &mut GetInput) -> Result<GetResult> {
let rp_id_w = to_wide(&input.rp_id);
let client_data = WEBAUTHN_CLIENT_DATA {
dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
cbClientDataJSON: input.client_data_json.len() as u32,
pbClientDataJSON: input.client_data_json.as_ptr() as *mut u8,
pwszHashAlgId: WEBAUTHN_HASH_ALGORITHM_SHA_256,
};
let (_allow_keep, allow_list) = build_credential_list(&input.allow_credentials);
let options = WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS {
dwVersion: api_version().min(WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION),
dwTimeoutMilliseconds: input.timeout_ms,
CredentialList: allow_list,
dwAuthenticatorAttachment: input.authenticator_attachment,
dwUserVerificationRequirement: input.user_verification,
..Default::default()
};
let assertion_ptr = unsafe {
WebAuthNAuthenticatorGetAssertion(
interaction_window(input.window_handle),
PCWSTR(rp_id_w.as_ptr()),
&client_data,
Some(&options),
)
};
let assertion_ptr = assertion_ptr
.map_err(|err| ceremony_error(GET_PREFIX, &format!("WebAuthnCallFailed: {err}")))?;
if assertion_ptr.is_null() {
return Err(ceremony_error(GET_PREFIX, "InvalidWebAuthnResult"));
}
let asrt = unsafe { &*assertion_ptr };
let raw_id = copy_buffer(asrt.Credential.pbId, asrt.Credential.cbId)?;
let authenticator_data = copy_buffer(asrt.pbAuthenticatorData, asrt.cbAuthenticatorData)?;
let signature = copy_buffer(asrt.pbSignature, asrt.cbSignature)?;
let user_handle = if asrt.cbUserId > 0 {
Some(copy_buffer(asrt.pbUserId, asrt.cbUserId)?)
} else {
None
};
let used_transport = if asrt.dwVersion >= 4 {
asrt.dwUsedTransport
} else if input.authenticator_attachment == WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM {
WEBAUTHN_CTAP_TRANSPORT_INTERNAL
} else if input.authenticator_attachment == WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM {
WEBAUTHN_CTAP_TRANSPORT_USB
} else {
0
};
unsafe { WebAuthNFreeAssertion(assertion_ptr) };
Ok(GetResult {
raw_id,
authenticator_data,
signature,
user_handle,
client_data_json: input.client_data_json.clone(),
used_transport,
})
}