#![allow(clippy::too_many_lines)] // SPDX-License-Identifier: AGPL-3.0-or-later use napi::Task; use napi::bindgen_prelude::{ AsyncTask, Buffer, Env, Error, JsObjectValue, Object, Result, Status, Unknown, }; use napi::{JsValue, ValueType}; use napi_derive::napi; mod common; use common::{ CreateInput, CreateResult, DescriptorInput, GetInput, GetResult, attachment_from_transport, }; #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "macos")] const BACKEND_NAME: &str = "macos-authenticationservices"; #[cfg(target_os = "windows")] const BACKEND_NAME: &str = "windows-webauthn"; #[cfg(target_os = "linux")] const BACKEND_NAME: &str = "linux-libfido2"; #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] const BACKEND_NAME: &str = "unavailable"; #[cfg(target_os = "macos")] const BACKEND_REASON: &str = "macOS WebAuthn backend (AuthenticationServices). \ Requires macOS 12.0+ (ASAuthorizationPlatformPublicKeyCredentialProvider, \ introduced in Monterey). On older macOS the bundled \ ASAuthorizationPlatform/SecurityKey provider classes are not present and \ `isSupported()` returns false."; #[cfg(target_os = "windows")] const BACKEND_REASON: &str = "Windows WebAuthn broker backend (webauthn.dll). \ Requires Windows 10 build 18362 (May 2019 / version 1903) or newer; \ webauthn.dll is delay-loaded via /DELAYLOAD, so the addon still loads on \ older builds but `isSupported()` returns false there."; #[cfg(target_os = "linux")] const BACKEND_REASON: &str = "Linux WebAuthn backend (libfido2). Requires \ libfido2 >= 1.5.0 (HID transport, U2F fallback disabled) and a CTAP \ authenticator on USB-HID; `isSupported()` returns false when no \ authenticator is enumerated."; #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] const BACKEND_REASON: &str = "no WebAuthn backend is compiled in for this platform"; const TARGET_PLATFORM: &str = if cfg!(target_os = "macos") { "darwin" } else if cfg!(target_os = "windows") { "win32" } else if cfg!(target_os = "linux") { "linux" } else { "unknown" }; const TARGET_ARCH: &str = if cfg!(target_arch = "x86_64") { "x64" } else if cfg!(target_arch = "aarch64") { "arm64" } else { "unknown" }; fn target_string() -> String { format!("{TARGET_PLATFORM}/{TARGET_ARCH}") } fn platform_broker_available() -> bool { #[cfg(target_os = "windows")] { windows::api_version() > 0 } #[cfg(target_os = "linux")] { linux::is_supported() } #[cfg(target_os = "macos")] { macos::is_supported() } #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] { false } } fn platform_authenticator_available() -> bool { #[cfg(target_os = "windows")] { windows::is_user_verifying_platform_authenticator_available() } #[cfg(not(target_os = "windows"))] { false } } fn ceremonies_implemented() -> bool { cfg!(any( target_os = "macos", target_os = "windows", target_os = "linux" )) } fn is_supported_native() -> bool { ceremonies_implemented() && platform_broker_available() } fn api_version() -> u32 { #[cfg(target_os = "windows")] { windows::api_version() } #[cfg(not(target_os = "windows"))] { 0 } } fn backend_reason() -> String { #[cfg(target_os = "linux")] { format!("{} {}", BACKEND_REASON, linux::support_diagnostics()) } #[cfg(not(target_os = "linux"))] { BACKEND_REASON.to_owned() } } #[napi(object, js_name = "WebAuthnBackendInfo")] pub struct WebAuthnBackendInfoJs { pub target: String, pub backend: String, #[napi(js_name = "nativeLoaded")] pub native_loaded: bool, #[napi(js_name = "ceremoniesImplemented")] pub ceremonies_implemented: bool, #[napi(js_name = "platformBrokerAvailable")] pub platform_broker_available: bool, #[napi(js_name = "platformAuthenticatorAvailable")] pub platform_authenticator_available: bool, pub supported: bool, #[napi(js_name = "apiVersion")] pub api_version: u32, pub reason: String, } #[napi(js_name = "getBackendInfo")] pub fn get_backend_info() -> WebAuthnBackendInfoJs { WebAuthnBackendInfoJs { target: target_string(), backend: BACKEND_NAME.to_owned(), native_loaded: true, ceremonies_implemented: ceremonies_implemented(), platform_broker_available: platform_broker_available(), platform_authenticator_available: platform_authenticator_available(), supported: is_supported_native(), api_version: api_version(), reason: backend_reason(), } } pub struct IsSupportedTask; impl Task for IsSupportedTask { type Output = bool; type JsValue = bool; fn compute(&mut self) -> Result { Ok(is_supported_native()) } fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { Ok(output) } } #[napi(js_name = "isSupported")] pub fn is_supported() -> AsyncTask { AsyncTask::new(IsSupportedTask) } #[napi(object)] pub struct CreateCeremonyResultJs { #[napi(js_name = "rawId")] pub raw_id: Buffer, #[napi(js_name = "attestationObject")] pub attestation_object: Buffer, #[napi(js_name = "clientDataJSON")] pub client_data_json: Buffer, #[napi(js_name = "authenticatorAttachment")] pub authenticator_attachment: Option, } #[napi(object)] pub struct GetCeremonyResultJs { #[napi(js_name = "rawId")] pub raw_id: Buffer, #[napi(js_name = "authenticatorData")] pub authenticator_data: Buffer, pub signature: Buffer, #[napi(js_name = "userHandle")] pub user_handle: Option, #[napi(js_name = "clientDataJSON")] pub client_data_json: Buffer, #[napi(js_name = "authenticatorAttachment")] pub authenticator_attachment: Option, } pub struct CreateTask { input: CreateInput, } impl Task for CreateTask { type Output = CreateResult; type JsValue = CreateCeremonyResultJs; fn compute(&mut self) -> Result { run_create(&mut self.input) } fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { Ok(CreateCeremonyResultJs { raw_id: Buffer::from(output.raw_id), attestation_object: Buffer::from(output.attestation_object), client_data_json: Buffer::from(output.client_data_json), authenticator_attachment: attachment_from_transport(output.used_transport) .map(str::to_owned), }) } } pub struct GetTask { input: GetInput, } impl Task for GetTask { type Output = GetResult; type JsValue = GetCeremonyResultJs; fn compute(&mut self) -> Result { run_get(&mut self.input) } fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { Ok(GetCeremonyResultJs { raw_id: Buffer::from(output.raw_id), authenticator_data: Buffer::from(output.authenticator_data), signature: Buffer::from(output.signature), user_handle: output.user_handle.map(Buffer::from), client_data_json: Buffer::from(output.client_data_json), authenticator_attachment: attachment_from_transport(output.used_transport) .map(str::to_owned), }) } } fn run_create(input: &mut CreateInput) -> Result { #[cfg(target_os = "windows")] { windows::make_credential(input) } #[cfg(target_os = "linux")] { linux::make_credential(input) } #[cfg(target_os = "macos")] { macos::make_credential(input) } #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] { let _ = input; Err(common::ceremony_error( common::CREATE_PREFIX, "UnknownPlatform", )) } } fn run_get(input: &mut GetInput) -> Result { #[cfg(target_os = "windows")] { windows::get_assertion(input) } #[cfg(target_os = "linux")] { linux::get_assertion(input) } #[cfg(target_os = "macos")] { macos::get_assertion(input) } #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] { let _ = input; Err(common::ceremony_error( common::GET_PREFIX, "UnknownPlatform", )) } } #[napi(js_name = "create")] pub fn create(options: Object) -> Result> { let input = parse_create_input(&options)?; Ok(AsyncTask::new(CreateTask { input })) } #[napi(js_name = "get")] pub fn get(options: Object) -> Result> { let input = parse_get_input(&options)?; Ok(AsyncTask::new(GetTask { input })) } fn invalid_create_options() -> Error { Error::new( Status::InvalidArg, "invalid WebAuthn registration options".to_owned(), ) } fn invalid_get_options() -> Error { Error::new( Status::InvalidArg, "invalid WebAuthn assertion options".to_owned(), ) } fn parse_create_input(object: &Object) -> Result { let map_err = |_e: Error| invalid_create_options(); Ok(CreateInput { rp_id: required_string(object, "rpId").map_err(map_err)?, rp_name: required_string(object, "rpName").map_err(map_err)?, challenge: required_buffer(object, "challenge").map_err(map_err)?, user_id: required_buffer(object, "userId").map_err(map_err)?, user_name: required_string(object, "userName").map_err(map_err)?, user_display_name: required_string(object, "userDisplayName").map_err(map_err)?, client_data_json: required_buffer(object, "clientDataJSON").map_err(map_err)?, client_data_hash: required_buffer(object, "clientDataHash").map_err(map_err)?, pub_key_algs: required_alg_array(object).map_err(map_err)?, exclude_credentials: optional_descriptor_array(object, "excludeCredentials") .map_err(map_err)?, timeout_ms: optional_u32(object, "timeout", 0).map_err(map_err)?, authenticator_attachment: optional_u32( object, "authenticatorAttachment", default_attachment(), ) .map_err(map_err)?, user_verification: optional_u32(object, "userVerification", default_user_verification()) .map_err(map_err)?, attestation: optional_u32(object, "attestation", default_attestation()).map_err(map_err)?, enterprise_attestation: optional_u32( object, "enterpriseAttestation", common::ENTERPRISE_NONE, ) .map_err(map_err)?, require_resident_key: optional_bool(object, "requireResidentKey", false) .map_err(map_err)?, prefer_resident_key: optional_bool(object, "preferResidentKey", false).map_err(map_err)?, window_handle: optional_window_handle(object).map_err(map_err)?, pin: optional_string(object, "pin").map_err(map_err)?, }) } fn parse_get_input(object: &Object) -> Result { let map_err = |_e: Error| invalid_get_options(); Ok(GetInput { rp_id: required_string(object, "rpId").map_err(map_err)?, challenge: required_buffer(object, "challenge").map_err(map_err)?, client_data_json: required_buffer(object, "clientDataJSON").map_err(map_err)?, client_data_hash: required_buffer(object, "clientDataHash").map_err(map_err)?, allow_credentials: optional_descriptor_array(object, "allowCredentials") .map_err(map_err)?, timeout_ms: optional_u32(object, "timeout", 0).map_err(map_err)?, authenticator_attachment: optional_u32( object, "authenticatorAttachment", default_attachment(), ) .map_err(map_err)?, user_verification: optional_u32(object, "userVerification", default_user_verification()) .map_err(map_err)?, window_handle: optional_window_handle(object).map_err(map_err)?, pin: optional_string(object, "pin").map_err(map_err)?, }) } fn optional_string(object: &Object, key: &str) -> Result> { let Some(value) = read_unknown(object, key) else { return Ok(None); }; if is_nullish(&value) { return Ok(None); } if value.get_type()? != ValueType::String { return Err(Error::new( Status::InvalidArg, format!("{key} must be a string"), )); } let s: String = unsafe { value.cast::()? }; if s.is_empty() { Ok(None) } else { Ok(Some(s)) } } fn optional_window_handle(object: &Object) -> Result { let Some(value) = read_unknown(object, "windowHandle") else { return Ok(0); }; if is_nullish(&value) { return Ok(0); } let bytes = read_buffer_bytes(&value, "windowHandle")?; if bytes.len() > 8 { return Err(Error::new( Status::InvalidArg, "windowHandle must be at most 8 bytes".to_owned(), )); } let mut padded = [0u8; 8]; padded[..bytes.len()].copy_from_slice(&bytes); Ok(u64::from_le_bytes(padded)) } #[cfg(target_os = "windows")] fn default_attachment() -> u32 { common::ATTACHMENT_ANY } #[cfg(target_os = "windows")] fn default_user_verification() -> u32 { common::USER_VERIFICATION_PREFERRED } #[cfg(target_os = "windows")] fn default_attestation() -> u32 { common::ATTESTATION_NONE } #[cfg(not(target_os = "windows"))] fn default_attachment() -> u32 { 0 } #[cfg(not(target_os = "windows"))] fn default_user_verification() -> u32 { 0 } #[cfg(not(target_os = "windows"))] fn default_attestation() -> u32 { 0 } fn read_unknown<'a>(object: &Object<'a>, key: &str) -> Option> { object.get::(key).ok().flatten() } fn is_nullish(v: &Unknown<'_>) -> bool { match v.get_type() { Ok(t) => t == ValueType::Null || t == ValueType::Undefined, Err(_) => true, } } fn required_string(object: &Object, key: &str) -> Result { let value = read_unknown(object, key) .ok_or_else(|| Error::new(Status::InvalidArg, format!("missing {key}")))?; if is_nullish(&value) { return Err(Error::new(Status::InvalidArg, format!("missing {key}"))); } if value.get_type()? != ValueType::String { return Err(Error::new( Status::InvalidArg, format!("{key} must be a string"), )); } let s: String = unsafe { value.cast::()? }; Ok(s) } fn optional_u32(object: &Object, key: &str, default: u32) -> Result { let Some(value) = read_unknown(object, key) else { return Ok(default); }; if is_nullish(&value) { return Ok(default); } if value.get_type()? != ValueType::Number { return Err(Error::new( Status::InvalidArg, format!("{key} must be a number"), )); } let n: u32 = unsafe { value.cast::()? }; Ok(n) } fn optional_bool(object: &Object, key: &str, default: bool) -> Result { let Some(value) = read_unknown(object, key) else { return Ok(default); }; if is_nullish(&value) { return Ok(default); } if value.get_type()? != ValueType::Boolean { return Err(Error::new( Status::InvalidArg, format!("{key} must be a boolean"), )); } let b: bool = unsafe { value.cast::()? }; Ok(b) } fn required_buffer(object: &Object, key: &str) -> Result> { let value = read_unknown(object, key) .ok_or_else(|| Error::new(Status::InvalidArg, format!("missing {key}")))?; if is_nullish(&value) { return Err(Error::new(Status::InvalidArg, format!("missing {key}"))); } read_buffer_bytes(&value, key) } fn read_buffer_bytes(value: &Unknown<'_>, key: &str) -> Result> { let raw = value.raw(); let raw_env = value.value().env; let mut is_buffer = false; let status = unsafe { napi::sys::napi_is_buffer(raw_env, raw, &mut is_buffer) }; if status != napi::sys::Status::napi_ok || !is_buffer { return Err(Error::new( Status::InvalidArg, format!("{key} must be a Buffer"), )); } let mut data_ptr: *mut std::ffi::c_void = std::ptr::null_mut(); let mut len: usize = 0; let status = unsafe { napi::sys::napi_get_buffer_info(raw_env, raw, &mut data_ptr, &mut len) }; if status != napi::sys::Status::napi_ok { return Err(Error::new( Status::InvalidArg, format!("{key} buffer read failed"), )); } if len == 0 { return Ok(Vec::new()); } if data_ptr.is_null() { return Err(Error::new( Status::InvalidArg, format!("{key} buffer pointer was null"), )); } let slice = unsafe { std::slice::from_raw_parts(data_ptr.cast::(), len) }; Ok(slice.to_vec()) } fn required_alg_array(object: &Object) -> Result> { let Some(value) = read_unknown(object, "pubKeyCredParams") else { return Err(Error::new( Status::InvalidArg, "missing pubKeyCredParams".to_owned(), )); }; if is_nullish(&value) { return Err(Error::new( Status::InvalidArg, "missing pubKeyCredParams".to_owned(), )); } let raw = value.raw(); let raw_env = value.value().env; let mut is_array = false; let status = unsafe { napi::sys::napi_is_array(raw_env, raw, &mut is_array) }; if status != napi::sys::Status::napi_ok || !is_array { return Err(Error::new( Status::InvalidArg, "pubKeyCredParams must be an array".to_owned(), )); } let arr: Object = unsafe { value.cast::()? }; let len = read_array_length(&arr)?; if len == 0 { return Err(Error::new( Status::InvalidArg, "pubKeyCredParams must not be empty".to_owned(), )); } let mut algs = Vec::with_capacity(len as usize); for i in 0..len { let mut elem_raw: napi::sys::napi_value = std::ptr::null_mut(); let status = unsafe { napi::sys::napi_get_element(raw_env, raw, i, &mut elem_raw) }; if status != napi::sys::Status::napi_ok { return Err(Error::new( Status::InvalidArg, "pubKeyCredParams element read failed".to_owned(), )); } let elem_value: Unknown = unsafe { Unknown::from_raw_unchecked(raw_env, elem_raw) }; if elem_value.get_type()? != ValueType::Object { return Err(Error::new( Status::InvalidArg, "pubKeyCredParams[] must be objects".to_owned(), )); } let elem: Object = unsafe { elem_value.cast::()? }; algs.push(optional_i32_field(&elem, "alg", 0)?); } Ok(algs) } fn optional_i32_field(object: &Object, key: &str, default: i32) -> Result { let Some(value) = read_unknown(object, key) else { return Ok(default); }; if is_nullish(&value) { return Ok(default); } if value.get_type()? != ValueType::Number { return Err(Error::new( Status::InvalidArg, format!("{key} must be a number"), )); } let n: i32 = unsafe { value.cast::()? }; Ok(n) } fn optional_descriptor_array(object: &Object, key: &str) -> Result> { let Some(value) = read_unknown(object, key) else { return Ok(Vec::new()); }; if is_nullish(&value) { return Ok(Vec::new()); } let raw = value.raw(); let raw_env = value.value().env; let mut is_array = false; let status = unsafe { napi::sys::napi_is_array(raw_env, raw, &mut is_array) }; if status != napi::sys::Status::napi_ok || !is_array { return Err(Error::new( Status::InvalidArg, format!("{key} must be an array"), )); } let arr: Object = unsafe { value.cast::()? }; let len = read_array_length(&arr)?; let mut out = Vec::with_capacity(len as usize); for i in 0..len { let mut elem_raw: napi::sys::napi_value = std::ptr::null_mut(); let status = unsafe { napi::sys::napi_get_element(raw_env, raw, i, &mut elem_raw) }; if status != napi::sys::Status::napi_ok { return Err(Error::new( Status::InvalidArg, format!("{key}[] read failed"), )); } let elem_value: Unknown = unsafe { Unknown::from_raw_unchecked(raw_env, elem_raw) }; if elem_value.get_type()? != ValueType::Object { return Err(Error::new( Status::InvalidArg, format!("{key}[] must be objects"), )); } let elem: Object = unsafe { elem_value.cast::()? }; let id = required_buffer(&elem, "id")?; let transports = optional_u32(&elem, "transports", 0)?; out.push(DescriptorInput { id, transports }); } Ok(out) } fn read_array_length(obj: &Object) -> Result { let length: u32 = obj.get_named_property::("length")?; Ok(length) }