tauri-plugin-webauthn 0.2.0

a Tauri plugin for WebAuthn
Documentation
use std::{
  sync::mpsc::{channel, Sender},
  thread,
};

use authenticator::{
  authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs},
  crypto::COSEAlgorithm,
  ctap2::server::{
    AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs,
    CredentialProtectionPolicy, HMACGetSecretInput, PublicKeyCredentialParameters,
    PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement,
    UserVerificationRequirement,
  },
  statecallback::StateCallback,
  Pin, StatusPinUv, StatusUpdate,
};
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
use base64urlsafedata::Base64UrlSafeData;
use openssl::sha::Sha256;
use tauri::{async_runtime::block_on, AppHandle, Emitter, Runtime, Url};
use tokio::sync::mpsc;
use webauthn_rs_proto::{
  AuthenticatorTransport, CollectedClientData, PublicKeyCredential,
  PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions,
  RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs,
  RequestAuthenticationExtensions, RequestRegistrationExtensions,
};

use crate::authenticators::ctap2::event::WebauthnEvent;

use super::EVENT_NAME;

pub fn init_manager() -> crate::Result<AuthenticatorService> {
  let mut manager = AuthenticatorService::new()?;
  manager.add_u2f_usb_hid_platform_transports();
  Ok(manager)
}

pub trait AuthenticatorExt {
  fn perform_register(
    &mut self,
    status_tx: Sender<StatusUpdate>,
    url: Url,
    options: PublicKeyCredentialCreationOptions,
    timeout: u64,
  ) -> crate::Result<RegisterPublicKeyCredential>;

  fn perform_authentication(
    &mut self,
    status_tx: Sender<StatusUpdate>,
    url: Url,
    options: PublicKeyCredentialRequestOptions,
    timeout: u64,
  ) -> crate::Result<PublicKeyCredential>;
}

impl AuthenticatorExt for AuthenticatorService {
  fn perform_register(
    &mut self,
    status_tx: Sender<StatusUpdate>,
    url: Url,
    options: PublicKeyCredentialCreationOptions,
    timeout: u64,
  ) -> crate::Result<RegisterPublicKeyCredential> {
    let client_data: Vec<u8> = serde_json::to_vec(&CollectedClientData {
      type_: "webauthn.create".to_string(),
      challenge: options.challenge,
      origin: url.clone(),
      cross_origin: None,
      token_binding: None,
      unknown_keys: Default::default(),
    })?;

    let mut hasher = Sha256::new();
    hasher.update(&client_data);
    let client_data_hash = hasher.finish();

    let args = RegisterArgs {
      pin: None,
      client_data_hash,
      origin: url.to_string(),
      user_verification_req: UserVerificationRequirement::Required,
      use_ctap1_fallback: false,
      relying_party: RelyingParty {
        id: options.rp.id,
        name: Some(options.rp.name),
      },
      user: PublicKeyCredentialUserEntity {
        id: options.user.id.to_vec(),
        name: Some(options.user.name),
        display_name: Some(options.user.display_name),
      },
      exclude_list: Vec::new(),
      resident_key_req: ResidentKeyRequirement::Required,
      extensions: convert_request_registration_extensions(options.extensions),
      pub_cred_params: convert_algorithms(options.pub_key_cred_params),
    };

    let (register_tx, register_rx) = channel();
    let callback = StateCallback::new(Box::new(move |rv| register_tx.send(rv).unwrap()));

    #[cfg(feature = "log")]
    log::debug!("Registering with args: {:?}", args);

    self.register(timeout, args, status_tx, callback)?;
    let result = register_rx.recv().unwrap()?;

    #[cfg(feature = "log")]
    log::debug!("Register result: {:?}", result);

    Ok(webauthn_rs_proto::RegisterPublicKeyCredential {
      extensions: convert_response_registration_extensions(result.extensions),
      response: webauthn_rs_proto::AuthenticatorAttestationResponseRaw {
        attestation_object: serde_cbor_2::to_vec(&result.att_obj)?.into(),
        client_data_json: Base64UrlSafeData::from(client_data),
        transports: Some(vec![
          AuthenticatorTransport::Usb,
          AuthenticatorTransport::Nfc,
          AuthenticatorTransport::Internal,
          AuthenticatorTransport::Ble,
        ]),
      },
      id: String::new(),
      raw_id: Vec::new().into(),
      type_: "public-key".to_string(),
    })
  }

  fn perform_authentication(
    &mut self,
    status_tx: Sender<StatusUpdate>,
    url: Url,
    options: PublicKeyCredentialRequestOptions,
    timeout: u64,
  ) -> crate::Result<PublicKeyCredential> {
    let client_data: Vec<u8> = serde_json::to_vec(&CollectedClientData {
      type_: "webauthn.get".to_string(),
      challenge: options.challenge,
      origin: url.clone(),
      cross_origin: None,
      token_binding: None,
      unknown_keys: Default::default(),
    })?;

    let mut hasher = Sha256::new();
    hasher.update(&client_data);
    let client_data_hash = hasher.finish();

    let args = SignArgs {
      pin: None,
      relying_party_id: options.rp_id.clone(),
      client_data_hash,
      origin: url.to_string(),
      user_presence_req: true,
      user_verification_req: UserVerificationRequirement::Required,
      use_ctap1_fallback: false,
      allow_list: Vec::new(),
      extensions: convert_request_authentication_extensions(options.extensions),
    };

    let (sign_tx, sign_rx) = channel();
    let callback = StateCallback::new(Box::new(move |rv| {
      sign_tx.send(rv).unwrap();
    }));

    #[cfg(feature = "log")]
    log::debug!("Signing with args: {:?}", args);

    self.sign(timeout, args, status_tx, callback)?;
    let result = sign_rx.recv().unwrap()?;

    #[cfg(feature = "log")]
    log::debug!("Sign result: {:?}", result);

    let raw_id = result.assertion.credentials.unwrap().id;
    let data = serde_cbor_2::to_vec(&result.assertion.auth_data)?;

    Ok(PublicKeyCredential {
      id: BASE64_URL_SAFE_NO_PAD.encode(&raw_id),
      raw_id: raw_id.into(),
      type_: "public-key".to_string(),
      response: webauthn_rs_proto::AuthenticatorAssertionResponseRaw {
        client_data_json: Base64UrlSafeData::from(client_data),
        authenticator_data: data[2..].into(),
        signature: result.assertion.signature.into(),
        user_handle: result.assertion.user.map(|h| h.id.into()),
      },
      extensions: convert_response_authentication_extensions(result.extensions),
    })
  }
}

pub fn status<R: Runtime>(
  app_handle: AppHandle<R>,
  pin_sender: mpsc::Sender<Sender<Pin>>,
  select_sender: mpsc::Sender<Sender<Option<usize>>>,
) -> Sender<StatusUpdate> {
  let (status_tx, status_rx) = channel::<StatusUpdate>();
  thread::spawn(move || loop {
    let Ok(status) = status_rx.recv() else {
      return;
    };

    #[cfg(feature = "log")]
    log::debug!("Status: {:?}", status);

    match &status {
      StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))
      | StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, ..)) => {
        block_on(async {
          let _ = pin_sender.send(sender.clone()).await;
        });
      }
      StatusUpdate::SelectResultNotice(sender, ..) => {
        block_on(async {
          let _ = select_sender.send(sender.clone()).await;
        });
      }
      _ => (),
    }

    let event: WebauthnEvent = status.into();
    let _ = app_handle.emit(EVENT_NAME, event);
  });
  status_tx
}

fn convert_response_authentication_extensions(
  extensions: AuthenticationExtensionsClientOutputs,
) -> webauthn_rs_proto::AuthenticationExtensionsClientOutputs {
  webauthn_rs_proto::AuthenticationExtensionsClientOutputs {
    appid: extensions.app_id,
    hmac_get_secret: extensions
      .hmac_get_secret
      .map(|h| webauthn_rs_proto::HmacGetSecretOutput {
        output1: h.output1.to_vec().into(),
        output2: h.output2.map(|s| s.to_vec().into()),
      }),
  }
}

fn convert_request_authentication_extensions(
  extensions: Option<RequestAuthenticationExtensions>,
) -> AuthenticationExtensionsClientInputs {
  extensions
    .map(|e| AuthenticationExtensionsClientInputs {
      app_id: e.appid,
      hmac_get_secret: e.hmac_get_secret.map(|h| HMACGetSecretInput {
        salt1: h.output1.to_vec().try_into().unwrap(),
        salt2: h.output2.map(|s| s.to_vec().try_into().unwrap()),
      }),
      ..Default::default()
    })
    .unwrap_or_default()
}

fn convert_request_registration_extensions(
  extensions: Option<RequestRegistrationExtensions>,
) -> AuthenticationExtensionsClientInputs {
  extensions
    .map(|e| AuthenticationExtensionsClientInputs {
      cred_props: e.cred_props,
      min_pin_length: e.min_pin_length,
      hmac_create_secret: e.hmac_create_secret,
      credential_protection_policy: e
        .cred_protect
        .clone()
        .map(|c| convert_credential_protection_policy(c.credential_protection_policy)),
      enforce_credential_protection_policy: e
        .cred_protect
        .and_then(|c| c.enforce_credential_protection_policy),
      ..Default::default()
    })
    .unwrap_or_default()
}

fn convert_response_registration_extensions(
  extensions: AuthenticationExtensionsClientOutputs,
) -> RegistrationExtensionsClientOutputs {
  RegistrationExtensionsClientOutputs {
    appid: extensions.app_id,
    hmac_secret: extensions.hmac_create_secret,
    cred_props: extensions
      .cred_props
      .map(|c| webauthn_rs_proto::CredProps { rk: c.rk }),
    ..Default::default()
  }
}

fn convert_credential_protection_policy(
  cred_protect: webauthn_rs_proto::CredentialProtectionPolicy,
) -> CredentialProtectionPolicy {
  match cred_protect {
    webauthn_rs_proto::CredentialProtectionPolicy::UserVerificationOptional => {
      CredentialProtectionPolicy::UserVerificationOptional
    }
    webauthn_rs_proto::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList => {
      CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList
    }
    webauthn_rs_proto::CredentialProtectionPolicy::UserVerificationRequired => {
      CredentialProtectionPolicy::UserVerificationRequired
    }
  }
}

fn convert_algorithms(
  algorithms: Vec<webauthn_rs_proto::PubKeyCredParams>,
) -> Vec<PublicKeyCredentialParameters> {
  algorithms
    .into_iter()
    .filter_map(|a| {
      Some(PublicKeyCredentialParameters {
        alg: COSEAlgorithm::try_from(a.alg).ok()?,
      })
    })
    .collect()
}