mod payload;
use crate::format::AuthBlob;
use crate::key::{MASTER_KEY_LEN, MasterKey};
use crate::traits::{AuthBoundKeyStore, Availability, Backend};
use crate::{Error, Result};
use chacha20poly1305::aead::{Aead, KeyInit, Payload as AeadPayload};
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
use std::sync::{Mutex, PoisonError};
use windows_sys::Win32::Foundation::HWND;
use windows_sys::Win32::Networking::WindowsWebServices::{
WEBAUTHN_ASSERTION, WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM, WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS,
WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG, WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS,
WEBAUTHN_CLIENT_DATA, WEBAUTHN_COSE_CREDENTIAL_PARAMETER, WEBAUTHN_COSE_CREDENTIAL_PARAMETERS,
WEBAUTHN_CREDENTIAL, WEBAUTHN_CREDENTIAL_ATTESTATION, WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY,
WEBAUTHN_CREDENTIALS, WEBAUTHN_EXTENSION, WEBAUTHN_EXTENSIONS,
WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET, WEBAUTHN_HMAC_SECRET_SALT,
WEBAUTHN_HMAC_SECRET_SALT_VALUES, WEBAUTHN_RP_ENTITY_INFORMATION,
WEBAUTHN_USER_ENTITY_INFORMATION, WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED,
WebAuthNAuthenticatorGetAssertion, WebAuthNAuthenticatorMakeCredential,
WebAuthNDeletePlatformCredential, WebAuthNFreeAssertion, WebAuthNFreeCredentialAttestation,
WebAuthNGetApiVersionNumber, WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable,
};
use windows_sys::Win32::Security::Cryptography::{
BCRYPT_USE_SYSTEM_PREFERRED_RNG, BCryptGenRandom,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{GetDesktopWindow, GetForegroundWindow};
use zeroize::Zeroize;
const RP_ID: &str = "qualia-bioauth.invalid";
const RP_NAME: &str = "Qualia2FA";
const ORIGIN: &str = "https://qualia-bioauth.invalid";
const COSE_ES256: i32 = -7;
const HASH_ALGORITHM: &str = "SHA-256";
const TIMEOUT_MS: u32 = 60_000;
const MIN_API_VERSION: u32 = 7;
const NTE_USER_CANCELLED: i32 = 0x8009_0036_u32 as i32;
const ERROR_CANCELLED: i32 = 0x8007_04C7_u32 as i32;
const NTE_AUTHENTICATION_IGNORED: i32 = 0x8009_0031_u32 as i32;
const NTE_NOT_SUPPORTED: i32 = 0x8009_0029_u32 as i32;
const NTE_DEVICE_NOT_READY: i32 = 0x8009_0030_u32 as i32;
const NTE_NOT_FOUND: i32 = 0x8009_0011_u32 as i32;
#[allow(non_snake_case, clippy::missing_docs_in_private_items)]
#[repr(C)]
struct MakeCredentialOptionsV9 {
dwVersion: u32,
dwTimeoutMilliseconds: u32,
CredentialList: WEBAUTHN_CREDENTIALS,
Extensions: WEBAUTHN_EXTENSIONS,
dwAuthenticatorAttachment: u32,
bRequireResidentKey: i32,
dwUserVerificationRequirement: u32,
dwAttestationConveyancePreference: u32,
dwFlags: u32,
pCancellationId: *mut core::ffi::c_void,
pExcludeCredentialList: *mut core::ffi::c_void,
dwEnterpriseAttestation: u32,
dwLargeBlobSupport: u32,
bPreferResidentKey: i32,
bBrowserInPrivateMode: i32,
bEnablePrf: i32,
pLinkedDevice: *mut core::ffi::c_void,
cbJsonExt: u32,
pbJsonExt: *mut u8,
pPRFGlobalEval: *mut WEBAUTHN_HMAC_SECRET_SALT,
cCredentialHints: u32,
ppwszCredentialHints: *mut *const u16,
bThirdPartyPayment: i32,
pwszRemoteWebOrigin: *const u16,
cbPublicKeyCredentialCreationOptionsJSON: u32,
pbPublicKeyCredentialCreationOptionsJSON: *mut u8,
cbAuthenticatorId: u32,
pbAuthenticatorId: *mut u8,
}
#[allow(non_snake_case, clippy::missing_docs_in_private_items)]
#[repr(C)]
struct GetAssertionOptionsV9 {
dwVersion: u32,
dwTimeoutMilliseconds: u32,
CredentialList: WEBAUTHN_CREDENTIALS,
Extensions: WEBAUTHN_EXTENSIONS,
dwAuthenticatorAttachment: u32,
dwUserVerificationRequirement: u32,
dwFlags: u32,
pwszU2fAppId: *const u16,
pbU2fAppId: *mut i32,
pCancellationId: *mut core::ffi::c_void,
pAllowCredentialList: *mut core::ffi::c_void,
dwCredLargeBlobOperation: u32,
cbCredLargeBlob: u32,
pbCredLargeBlob: *mut u8,
pHmacSecretSaltValues: *mut WEBAUTHN_HMAC_SECRET_SALT_VALUES,
bBrowserInPrivateMode: i32,
pLinkedDevice: *mut core::ffi::c_void,
bAutoFill: i32,
cbJsonExt: u32,
pbJsonExt: *mut u8,
cCredentialHints: u32,
ppwszCredentialHints: *mut *const u16,
pwszRemoteWebOrigin: *const u16,
cbPublicKeyCredentialRequestOptionsJSON: u32,
pbPublicKeyCredentialRequestOptionsJSON: *mut u8,
cbAuthenticatorId: u32,
pbAuthenticatorId: *mut u8,
}
pub struct WindowsKeyStore {
credential: Mutex<Option<Vec<u8>>>,
unavailable: Mutex<Option<&'static str>>,
operation: Mutex<()>,
}
impl WindowsKeyStore {
#[must_use]
pub fn new() -> Self {
Self {
credential: Mutex::new(None),
unavailable: Mutex::new(None),
operation: Mutex::new(()),
}
}
pub fn delete_blob(&self, blob: &[u8]) -> Result<()> {
let blob = AuthBlob::parse(blob)?;
blob.expect_backend(Backend::WindowsWebAuthn)?;
let payload = payload::Payload::parse(blob.wrapped())?;
let _operation = self
.operation
.lock()
.unwrap_or_else(PoisonError::into_inner);
delete_credential(payload.credential_id())?;
let mut current = self
.credential
.lock()
.unwrap_or_else(PoisonError::into_inner);
if current.as_deref() == Some(payload.credential_id()) {
*current = None;
}
Ok(())
}
fn ensure_credential(&self) -> Result<Vec<u8>> {
if let Some(reason) = self.unavailable_reason() {
return Err(Error::Unavailable { reason });
}
if let Some(credential) = self
.credential
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
{
return Ok(credential);
}
let credential = match make_credential(foreground_window()) {
Ok(credential) => credential,
Err(Error::Unavailable { reason }) => {
self.remember_unavailable(reason);
return Err(Error::Unavailable { reason });
}
Err(error) => return Err(error),
};
*self
.credential
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some(credential.clone());
Ok(credential)
}
fn unavailable_reason(&self) -> Option<&'static str> {
*self
.unavailable
.lock()
.unwrap_or_else(PoisonError::into_inner)
}
fn remember_unavailable(&self, reason: &'static str) {
*self
.unavailable
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some(reason);
}
fn hmac_secret(
&self,
credential_id: &[u8],
salt: &[u8; payload::SALT_LEN],
) -> Result<[u8; 32]> {
match get_hmac_secret(foreground_window(), credential_id, salt) {
Ok(secret) => Ok(secret),
Err(Error::Unavailable { reason }) => {
self.remember_unavailable(reason);
Err(Error::Unavailable { reason })
}
Err(error) => Err(error),
}
}
fn current_credential(&self) -> Option<Vec<u8>> {
self.credential
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
}
impl Default for WindowsKeyStore {
fn default() -> Self {
Self::new()
}
}
impl core::fmt::Debug for WindowsKeyStore {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("WindowsKeyStore")
.field("has_credential", &self.current_credential().is_some())
.finish_non_exhaustive()
}
}
impl AuthBoundKeyStore for WindowsKeyStore {
fn availability(&self) -> Availability {
if let Some(reason) = self.unavailable_reason() {
return Availability::Unavailable { reason };
}
availability()
}
fn create(&self) -> Result<()> {
let _operation = self
.operation
.lock()
.unwrap_or_else(PoisonError::into_inner);
self.ensure_credential().map(|_| ())
}
fn wrap(&self, master: &MasterKey) -> Result<Vec<u8>> {
let _operation = self
.operation
.lock()
.unwrap_or_else(PoisonError::into_inner);
let credential_id = self.ensure_credential()?;
let mut salt = [0_u8; payload::SALT_LEN];
random_bytes(&mut salt)?;
let mut hmac_secret = self.hmac_secret(&credential_id, &salt)?;
let mut nonce = [0_u8; payload::NONCE_LEN];
random_bytes(&mut nonce)?;
let aad = associated_data(&credential_id, &salt);
let cipher = XChaCha20Poly1305::new(&Key::from(hmac_secret));
let encrypted = cipher.encrypt(
&XNonce::from(nonce),
AeadPayload {
msg: master.expose_secret(),
aad: &aad,
},
);
hmac_secret.zeroize();
let ciphertext = encrypted.map_err(|_| Error::Platform {
operation: "encrypt the master key",
code: 0,
})?;
if ciphertext.len() != payload::CIPHERTEXT_LEN {
return Err(Error::WrappedPayloadInvalid {
reason: "the Windows ciphertext does not have the fixed length",
});
}
let mut fixed = [0_u8; payload::CIPHERTEXT_LEN];
fixed.copy_from_slice(&ciphertext);
let payload = payload::Payload::new(credential_id, salt, nonce, fixed)?;
let blob = AuthBlob::new(Backend::WindowsWebAuthn, payload.encode())?;
Ok(blob.encode())
}
fn unwrap(&self, blob: &[u8]) -> Result<MasterKey> {
let blob = AuthBlob::parse(blob)?;
blob.expect_backend(Backend::WindowsWebAuthn)?;
let payload = payload::Payload::parse(blob.wrapped())?;
if let Some(reason) = self.unavailable_reason() {
return Err(Error::Unavailable { reason });
}
let _operation = self
.operation
.lock()
.unwrap_or_else(PoisonError::into_inner);
let mut hmac_secret = self.hmac_secret(payload.credential_id(), payload.salt())?;
let aad = associated_data(payload.credential_id(), payload.salt());
let cipher = XChaCha20Poly1305::new(&Key::from(hmac_secret));
let decrypted = cipher.decrypt(
&XNonce::from(*payload.nonce()),
AeadPayload {
msg: payload.ciphertext(),
aad: &aad,
},
);
hmac_secret.zeroize();
let mut plaintext = decrypted.map_err(|_| Error::AuthenticationFailed)?;
let master = MasterKey::from_bytes(&plaintext);
plaintext.zeroize();
master
}
fn delete(&self) -> Result<()> {
let _operation = self
.operation
.lock()
.unwrap_or_else(PoisonError::into_inner);
let current = self.current_credential();
if let Some(credential) = current {
delete_credential(&credential)?;
let mut stored = self
.credential
.lock()
.unwrap_or_else(PoisonError::into_inner);
if stored.as_deref() == Some(credential.as_slice()) {
*stored = None;
}
}
Ok(())
}
fn delete_blob(&self, blob: &[u8]) -> Result<()> {
WindowsKeyStore::delete_blob(self, blob)
}
}
#[must_use]
pub fn api_version() -> u32 {
unsafe { WebAuthNGetApiVersionNumber() }
}
fn availability() -> Availability {
if api_version() < MIN_API_VERSION {
return Availability::Unavailable {
reason: "this Windows version does not expose the WebAuthn hmac-secret extension",
};
}
let mut available: i32 = 0;
let status = unsafe { WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(&mut available) };
if status < 0 || available == 0 {
return Availability::Unavailable {
reason: "this machine has no user-verifying platform authenticator",
};
}
Availability::Available {
backend: Backend::WindowsWebAuthn,
hardware_backed: false,
}
}
fn foreground_window() -> HWND {
let foreground = unsafe { GetForegroundWindow() };
if !foreground.is_null() {
return foreground;
}
unsafe { GetDesktopWindow() }
}
fn random_bytes(out: &mut [u8]) -> Result<()> {
let status = unsafe {
BCryptGenRandom(
std::ptr::null_mut(),
out.as_mut_ptr(),
out.len() as u32,
BCRYPT_USE_SYSTEM_PREFERRED_RNG,
)
};
if status < 0 {
Err(Error::RandomnessUnavailable)
} else {
Ok(())
}
}
fn associated_data(credential_id: &[u8], salt: &[u8; payload::SALT_LEN]) -> Vec<u8> {
let mut aad =
Vec::with_capacity(payload::AAD_PREFIX.len() + 2 + credential_id.len() + salt.len());
aad.extend_from_slice(payload::AAD_PREFIX);
aad.extend_from_slice(&(credential_id.len() as u16).to_be_bytes());
aad.extend_from_slice(credential_id);
aad.extend_from_slice(salt);
aad
}
fn make_credential(hwnd: HWND) -> Result<Vec<u8>> {
let rp_id = wide(RP_ID);
let rp_name = wide(RP_NAME);
let user_name = wide("local");
let user_display = wide("Qualia2FA");
let hash_algorithm = wide(HASH_ALGORITHM);
let relying_party = WEBAUTHN_RP_ENTITY_INFORMATION {
dwVersion: 1,
pwszId: rp_id.as_ptr(),
pwszName: rp_name.as_ptr(),
pwszIcon: std::ptr::null(),
};
let mut user_id = [0_u8; 16];
random_bytes(&mut user_id)?;
let user = WEBAUTHN_USER_ENTITY_INFORMATION {
dwVersion: 1,
cbId: user_id.len() as u32,
pbId: user_id.as_mut_ptr(),
pwszName: user_name.as_ptr(),
pwszIcon: std::ptr::null(),
pwszDisplayName: user_display.as_ptr(),
};
let mut parameters = [WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
dwVersion: 1,
pwszCredentialType: WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY,
lAlg: COSE_ES256,
}];
let parameter_list = WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
cCredentialParameters: parameters.len() as u32,
pCredentialParameters: parameters.as_mut_ptr(),
};
let mut challenge = [0_u8; 32];
random_bytes(&mut challenge)?;
let client_bytes = client_data("create", &challenge);
let client = WEBAUTHN_CLIENT_DATA {
dwVersion: 1,
cbClientDataJSON: client_bytes.len() as u32,
pbClientDataJSON: client_bytes.as_ptr().cast_mut(),
pwszHashAlgId: hash_algorithm.as_ptr(),
};
let mut hmac_extension_value: i32 = 1;
let mut hmac_extension = WEBAUTHN_EXTENSION {
pwszExtensionIdentifier: WEBAUTHN_EXTENSIONS_IDENTIFIER_HMAC_SECRET,
cbExtension: 4,
pvExtension: (&mut hmac_extension_value as *mut i32).cast(),
};
let extensions = WEBAUTHN_EXTENSIONS {
cExtensions: 1,
pExtensions: &mut hmac_extension,
};
let mut attestation: *mut WEBAUTHN_CREDENTIAL_ATTESTATION = std::ptr::null_mut();
let status = if api_version() >= 9 {
let options = MakeCredentialOptionsV9 {
dwVersion: 9,
dwTimeoutMilliseconds: TIMEOUT_MS,
CredentialList: WEBAUTHN_CREDENTIALS {
cCredentials: 0,
pCredentials: std::ptr::null_mut(),
},
Extensions: extensions,
dwAuthenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM,
bRequireResidentKey: 1,
dwUserVerificationRequirement: WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED,
dwAttestationConveyancePreference: WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
dwFlags: 0,
pCancellationId: std::ptr::null_mut(),
pExcludeCredentialList: std::ptr::null_mut(),
dwEnterpriseAttestation: 0,
dwLargeBlobSupport: 0,
bPreferResidentKey: 0,
bBrowserInPrivateMode: 0,
bEnablePrf: 1,
pLinkedDevice: std::ptr::null_mut(),
cbJsonExt: 0,
pbJsonExt: std::ptr::null_mut(),
pPRFGlobalEval: std::ptr::null_mut(),
cCredentialHints: 0,
ppwszCredentialHints: std::ptr::null_mut(),
bThirdPartyPayment: 0,
pwszRemoteWebOrigin: std::ptr::null(),
cbPublicKeyCredentialCreationOptionsJSON: 0,
pbPublicKeyCredentialCreationOptionsJSON: std::ptr::null_mut(),
cbAuthenticatorId: 0,
pbAuthenticatorId: std::ptr::null_mut(),
};
unsafe {
WebAuthNAuthenticatorMakeCredential(
hwnd,
&relying_party,
&user,
¶meter_list,
&client,
(&options as *const MakeCredentialOptionsV9)
.cast::<WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS>(),
&mut attestation,
)
}
} else {
let options = WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS {
dwVersion: 7,
dwTimeoutMilliseconds: TIMEOUT_MS,
Extensions: extensions,
dwAuthenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM,
bRequireResidentKey: 0,
dwUserVerificationRequirement: WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED,
dwAttestationConveyancePreference: WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
bEnablePrf: 1,
..Default::default()
};
unsafe {
WebAuthNAuthenticatorMakeCredential(
hwnd,
&relying_party,
&user,
¶meter_list,
&client,
&options,
&mut attestation,
)
}
};
if status < 0 {
return Err(map_status("WebAuthNAuthenticatorMakeCredential", status));
}
if attestation.is_null() {
return Err(Error::Platform {
operation: "WebAuthNAuthenticatorMakeCredential returned no attestation",
code: 0,
});
}
let credential_id = {
let attestation = unsafe { &*attestation };
if attestation.pbCredentialId.is_null() {
Vec::new()
} else {
unsafe {
std::slice::from_raw_parts(
attestation.pbCredentialId,
attestation.cbCredentialId as usize,
)
}
.to_vec()
}
};
unsafe { WebAuthNFreeCredentialAttestation(attestation) };
if credential_id.is_empty() || credential_id.len() > payload::MAX_CREDENTIAL_ID_LEN {
return Err(Error::WrappedPayloadInvalid {
reason: "the platform returned an unusable credential id",
});
}
Ok(credential_id)
}
fn get_hmac_secret(
hwnd: HWND,
credential_id: &[u8],
salt: &[u8; payload::SALT_LEN],
) -> Result<[u8; 32]> {
let hash_algorithm = wide(HASH_ALGORITHM);
let mut challenge = [0_u8; 32];
random_bytes(&mut challenge)?;
let client_bytes = client_data("get", &challenge);
let client = WEBAUTHN_CLIENT_DATA {
dwVersion: 1,
cbClientDataJSON: client_bytes.len() as u32,
pbClientDataJSON: client_bytes.as_ptr().cast_mut(),
pwszHashAlgId: hash_algorithm.as_ptr(),
};
let mut salt_bytes = *salt;
let mut hmac_salt = WEBAUTHN_HMAC_SECRET_SALT {
cbFirst: payload::SALT_LEN as u32,
pbFirst: salt_bytes.as_mut_ptr(),
cbSecond: 0,
pbSecond: std::ptr::null_mut(),
};
let mut salt_values = WEBAUTHN_HMAC_SECRET_SALT_VALUES {
pGlobalHmacSalt: &mut hmac_salt,
cCredWithHmacSecretSaltList: 0,
pCredWithHmacSecretSaltList: std::ptr::null_mut(),
};
let mut credential_id_bytes = credential_id.to_vec();
let mut credential = WEBAUTHN_CREDENTIAL {
dwVersion: 1,
cbId: credential_id_bytes.len() as u32,
pbId: credential_id_bytes.as_mut_ptr(),
pwszCredentialType: WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY,
};
let rp_id = wide(RP_ID);
let mut assertion: *mut WEBAUTHN_ASSERTION = std::ptr::null_mut();
let status = if api_version() >= 9 {
let options = GetAssertionOptionsV9 {
dwVersion: 9,
dwTimeoutMilliseconds: TIMEOUT_MS,
CredentialList: WEBAUTHN_CREDENTIALS {
cCredentials: 1,
pCredentials: &mut credential,
},
Extensions: WEBAUTHN_EXTENSIONS {
cExtensions: 0,
pExtensions: std::ptr::null_mut(),
},
dwAuthenticatorAttachment: 0,
dwUserVerificationRequirement: WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED,
dwFlags: WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG,
pwszU2fAppId: std::ptr::null(),
pbU2fAppId: std::ptr::null_mut(),
pCancellationId: std::ptr::null_mut(),
pAllowCredentialList: std::ptr::null_mut(),
dwCredLargeBlobOperation: 0,
cbCredLargeBlob: 0,
pbCredLargeBlob: std::ptr::null_mut(),
pHmacSecretSaltValues: &mut salt_values,
bBrowserInPrivateMode: 0,
pLinkedDevice: std::ptr::null_mut(),
bAutoFill: 0,
cbJsonExt: 0,
pbJsonExt: std::ptr::null_mut(),
cCredentialHints: 0,
ppwszCredentialHints: std::ptr::null_mut(),
pwszRemoteWebOrigin: std::ptr::null(),
cbPublicKeyCredentialRequestOptionsJSON: 0,
pbPublicKeyCredentialRequestOptionsJSON: std::ptr::null_mut(),
cbAuthenticatorId: 0,
pbAuthenticatorId: std::ptr::null_mut(),
};
unsafe {
WebAuthNAuthenticatorGetAssertion(
hwnd,
rp_id.as_ptr(),
&client,
(&options as *const GetAssertionOptionsV9)
.cast::<WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS>(),
&mut assertion,
)
}
} else {
let options = WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS {
dwVersion: 7,
dwTimeoutMilliseconds: TIMEOUT_MS,
CredentialList: WEBAUTHN_CREDENTIALS {
cCredentials: 1,
pCredentials: &mut credential,
},
dwUserVerificationRequirement: WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED,
dwFlags: WEBAUTHN_AUTHENTICATOR_HMAC_SECRET_VALUES_FLAG,
pHmacSecretSaltValues: &mut salt_values,
..Default::default()
};
unsafe {
WebAuthNAuthenticatorGetAssertion(
hwnd,
rp_id.as_ptr(),
&client,
&options,
&mut assertion,
)
}
};
if status < 0 {
return Err(map_status("WebAuthNAuthenticatorGetAssertion", status));
}
if assertion.is_null() {
return Err(Error::Platform {
operation: "WebAuthNAuthenticatorGetAssertion returned no assertion",
code: 0,
});
}
let secret = {
let assertion = unsafe { &*assertion };
let salt = assertion.pHmacSecret;
if salt.is_null() {
None
} else {
let salt = unsafe { &*salt };
if salt.cbFirst != MASTER_KEY_LEN as u32 || salt.pbFirst.is_null() {
None
} else {
let mut out = [0_u8; MASTER_KEY_LEN];
unsafe {
std::ptr::copy_nonoverlapping(salt.pbFirst, out.as_mut_ptr(), MASTER_KEY_LEN)
};
Some(out)
}
}
};
unsafe { WebAuthNFreeAssertion(assertion) };
secret.ok_or(Error::Unavailable {
reason: "the platform assertion did not include a 32-byte hmac-secret",
})
}
fn delete_credential(credential_id: &[u8]) -> Result<()> {
let status = unsafe {
WebAuthNDeletePlatformCredential(credential_id.len() as u32, credential_id.as_ptr())
};
if status < 0 && status != NTE_NOT_FOUND {
return Err(map_status("WebAuthNDeletePlatformCredential", status));
}
Ok(())
}
fn map_status(operation: &'static str, status: i32) -> Error {
match status {
NTE_USER_CANCELLED | ERROR_CANCELLED | NTE_AUTHENTICATION_IGNORED => {
Error::AuthenticationFailed
}
NTE_NOT_SUPPORTED => Error::Unavailable {
reason: "the platform authenticator does not support the hmac-secret extension",
},
NTE_DEVICE_NOT_READY => Error::Unavailable {
reason: "Windows Hello is not ready for this user; enroll Hello or unlock the session",
},
_ => Error::Platform {
operation,
code: status,
},
}
}
fn wide(text: &str) -> Vec<u16> {
text.encode_utf16().chain(std::iter::once(0)).collect()
}
fn client_data(kind: &str, challenge: &[u8]) -> Vec<u8> {
format!(
"{{\"type\":\"webauthn.{kind}\",\"challenge\":\"{}\",\"origin\":\"{ORIGIN}\"}}",
base64url(challenge)
)
.into_bytes()
}
fn base64url(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let first = u32::from(chunk[0]);
let second = chunk.get(1).copied().map_or(0, u32::from);
let third = chunk.get(2).copied().map_or(0, u32::from);
let triple = (first << 16) | (second << 8) | third;
out.push(ALPHABET[(triple >> 18) as usize & 0x3F] as char);
out.push(ALPHABET[(triple >> 12) as usize & 0x3F] as char);
if chunk.len() > 1 {
out.push(ALPHABET[(triple >> 6) as usize & 0x3F] as char);
}
if chunk.len() > 2 {
out.push(ALPHABET[triple as usize & 0x3F] as char);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::MockKeyStore;
#[test]
fn availability_is_reported() {
let availability = availability();
if availability.is_available() {
assert_eq!(availability.backend(), Some(Backend::WindowsWebAuthn));
} else {
match availability {
Availability::Unavailable { reason } => assert!(!reason.is_empty()),
other => panic!("unexpected availability: {other:?}"),
}
}
}
#[test]
fn the_prompt_parent_is_never_null() {
assert!(!foreground_window().is_null());
}
#[test]
fn delete_with_no_credential_is_ok() {
let store = WindowsKeyStore::new();
store.delete().expect("delete is idempotent");
}
#[test]
fn mock_blob_is_rejected_as_the_wrong_backend() {
let mock = MockKeyStore::new();
let master = MasterKey::generate().expect("randomness");
let blob = mock.wrap(&master).expect("wrap");
let store = WindowsKeyStore::new();
assert!(matches!(
store.unwrap(&blob),
Err(Error::WrongBackend {
expected: Backend::WindowsWebAuthn,
actual: Backend::Mock,
})
));
assert!(matches!(
store.delete_blob(&blob),
Err(Error::WrongBackend { .. })
));
}
#[test]
fn base64url_uses_the_url_safe_alphabet_without_padding() {
assert_eq!(base64url(&[]), "");
assert_eq!(base64url(&[0xFB_u8]), "-w");
assert_eq!(base64url(&[0xFF_u8, 0xEF]), "_-8");
assert_eq!(base64url(b"foobar"), "Zm9vYmFy");
}
#[test]
fn client_data_has_the_expected_shape() {
let json = String::from_utf8(client_data("get", &[0x11_u8; 4])).expect("utf-8");
assert!(json.starts_with("{\"type\":\"webauthn.get\""));
assert!(json.contains("\"challenge\":\"EREREQ\""));
assert!(json.contains(ORIGIN));
}
#[test]
#[ignore = "requires an enrolled Windows Hello account and shows prompts"]
fn hello_roundtrip() {
let api_version = unsafe { WebAuthNGetApiVersionNumber() };
let store = WindowsKeyStore::new();
println!("WebAuthN API version: {api_version}");
println!("availability: {:?}", store.availability());
if !store.availability().is_available() {
println!("RESULT: SKIPPED (no user-verifying platform authenticator)");
return;
}
match store.create() {
Ok(()) => println!("create: ok"),
Err(Error::Unavailable { reason }) => {
println!("RESULT: PRF UNAVAILABLE: {reason}");
assert!(matches!(
store.availability(),
Availability::Unavailable { .. }
));
return;
}
Err(error) => panic!("create failed: {error}"),
}
let master = MasterKey::generate().expect("randomness");
let blob = match store.wrap(&master) {
Ok(blob) => blob,
Err(Error::Unavailable { reason }) => {
println!("RESULT: PRF UNAVAILABLE: {reason}");
assert!(matches!(
store.availability(),
Availability::Unavailable { .. }
));
return;
}
Err(error) => panic!("wrap failed: {error}"),
};
println!("wrap: ok");
let restored = store.unwrap(&blob).expect("unwrap (Hello prompt)");
assert_eq!(restored, master);
println!("unwrap: ok, the same master key came back");
store.delete_blob(&blob).expect("delete the credential");
assert!(
store.unwrap(&blob).is_err(),
"a deleted credential still unwraps"
);
println!("delete: ok, the old blob no longer unwraps");
println!("RESULT: roundtrip OK");
}
}