use std::io::ErrorKind; use std::sync::Arc; use actix_web::web; use uuid::Uuid; use webauthn_rs::{Webauthn, WebauthnBuilder}; use webauthn_rs::prelude::{CreationChallengeResponse, Passkey, PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse}; use crate::constants::{WEBAUTHN_LOGIN_CHALLENGE_EXPIRE, WEBAUTHN_REGISTER_CHALLENGE_EXPIRE}; use crate::data::app_config::AppConfig; use crate::data::crypto_wrapper::CryptoWrapper; 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)] struct RegisterKeyOpaqueData { registration_state: String, user_id: UserID, expire: u64, } pub struct AuthRequest { pub opaque_state: String, pub login_challenge: RequestChallengeResponse, } #[derive(Debug, serde::Serialize, serde::Deserialize)] 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 { Self { core: WebauthnBuilder::new( conf.domain_name().split_once(':') .map(|s| s.0) .unwrap_or_else(|| conf.domain_name()), &url::Url::parse(&conf.website_origin) .expect("Failed to parse configuration origin!")) .expect("Invalid Webauthn configuration") .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, key: &WebauthnPubKey) -> Res { let (login_challenge, authentication_state) = self.core.start_passkey_authentication(&vec![ key.creds.clone() ])?; 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(()) } }