use std::io::ErrorKind; use std::sync::Arc; use actix_web::web; use bincode::{Decode, Encode}; use light_openid::crypto_wrapper::CryptoWrapper; use uuid::Uuid; use webauthn_rs::prelude::{ CreationChallengeResponse, Passkey, PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse, }; use webauthn_rs::{Webauthn, WebauthnBuilder}; use crate::constants::{ APP_NAME, WEBAUTHN_LOGIN_CHALLENGE_EXPIRE, WEBAUTHN_REGISTER_CHALLENGE_EXPIRE, }; use crate::data::app_config::AppConfig; use crate::data::user::{User, UserID}; use crate::utils::err::Res; use crate::utils::time::time; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct WebauthnPubKey { creds: Passkey, } pub struct RegisterKeyRequest { pub opaque_state: String, pub creation_challenge: CreationChallengeResponse, } #[derive(Debug, serde::Serialize, serde::Deserialize, Encode, Decode)] struct RegisterKeyOpaqueData { registration_state: String, user_id: UserID, expire: u64, } pub struct AuthRequest { pub opaque_state: String, pub login_challenge: RequestChallengeResponse, } #[derive(Debug, Encode, Decode)] struct AuthStateOpaqueData { authentication_state: String, user_id: UserID, expire: u64, } pub type WebAuthManagerReq = web::Data>; pub struct WebAuthManager { core: Webauthn, crypto_wrapper: CryptoWrapper, } impl WebAuthManager { pub fn init(conf: &AppConfig) -> Self { let rp_id = conf .domain_name() .split_once(':') .map(|s| s.0) .unwrap_or_else(|| conf.domain_name()); let rp_origin = url::Url::parse(&conf.website_origin).expect("Failed to parse configuration origin!"); log::debug!( "rp_id={} rp_origin={} rp_origin_domain={:?}", rp_id, rp_origin, rp_origin.domain() ); Self { core: WebauthnBuilder::new(rp_id, &rp_origin) .expect("Invalid Webauthn configuration") .rp_name(APP_NAME) .build() .expect("Failed to build webauthn"), crypto_wrapper: CryptoWrapper::new_random(), } } pub fn start_register(&self, user: &User) -> Res { let (creation_challenge, registration_state) = self.core.start_passkey_registration( Uuid::parse_str(&user.uid.0).expect("Failed to parse user id"), &user.username, &user.full_name(), None, )?; Ok(RegisterKeyRequest { opaque_state: self.crypto_wrapper.encrypt(&RegisterKeyOpaqueData { registration_state: serde_json::to_string(®istration_state)?, user_id: user.uid.clone(), expire: time() + WEBAUTHN_REGISTER_CHALLENGE_EXPIRE, })?, creation_challenge, }) } pub fn finish_registration( &self, user: &User, opaque_state: &str, pub_cred: RegisterPublicKeyCredential, ) -> Res { let state: RegisterKeyOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?; if state.user_id != user.uid { return Err(Box::new(std::io::Error::new( ErrorKind::Other, "Invalid user for pubkey!", ))); } if state.expire < time() { return Err(Box::new(std::io::Error::new( ErrorKind::Other, "Challenge has expired!", ))); } let res = self.core.finish_passkey_registration( &pub_cred, &serde_json::from_str(&state.registration_state)?, )?; Ok(WebauthnPubKey { creds: res }) } pub fn start_authentication( &self, user_id: &UserID, keys: &[WebauthnPubKey], ) -> Res { let (login_challenge, authentication_state) = self.core.start_passkey_authentication( &keys.iter().map(|k| k.creds.clone()).collect::>(), )?; Ok(AuthRequest { opaque_state: self.crypto_wrapper.encrypt(&AuthStateOpaqueData { authentication_state: serde_json::to_string(&authentication_state)?, user_id: user_id.clone(), expire: time() + WEBAUTHN_LOGIN_CHALLENGE_EXPIRE, })?, login_challenge, }) } pub fn finish_authentication( &self, user_id: &UserID, opaque_state: &str, pub_cred: &PublicKeyCredential, ) -> Res { let state: AuthStateOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?; if &state.user_id != user_id { return Err(Box::new(std::io::Error::new( ErrorKind::Other, "Invalid user for pubkey!", ))); } if state.expire < time() { return Err(Box::new(std::io::Error::new( ErrorKind::Other, "Challenge has expired!", ))); } self.core.finish_passkey_authentication( pub_cred, &serde_json::from_str(&state.authentication_state)?, )?; Ok(()) } }