mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
305 lines
11 KiB
Rust
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,
|
|
¶ms,
|
|
&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,
|
|
})
|
|
}
|