use std::fmt;
use std::str::FromStr;
use crate::types::{PasskiError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientDataType {
Create,
Get,
}
impl ClientDataType {
pub fn as_str(&self) -> &'static str {
match self {
ClientDataType::Create => "webauthn.create",
ClientDataType::Get => "webauthn.get",
}
}
}
impl FromStr for ClientDataType {
type Err = PasskiError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"webauthn.create" => Ok(ClientDataType::Create),
"webauthn.get" => Ok(ClientDataType::Get),
_ => Err(PasskiError::new(format!("Invalid type in client data: {}", s))),
}
}
}
impl fmt::Display for ClientDataType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug)]
pub struct ClientData {
pub type_: ClientDataType,
pub challenge: String,
pub origin: String,
pub cross_origin: bool,
}
impl ClientData {
pub fn from_bytes(bytes: &[u8]) -> Result<ClientData> {
let json: serde_json::Value = serde_json::from_slice(bytes)
.map_err(|e| PasskiError::new(format!("Invalid client data JSON: {}", e)))?;
let type_str = json["type"]
.as_str()
.ok_or_else(|| PasskiError::new("Missing type in client data"))?;
let type_ = type_str.parse::<ClientDataType>()?;
let challenge = json["challenge"]
.as_str()
.ok_or_else(|| PasskiError::new("Missing challenge in client data"))?
.to_string();
let origin = json["origin"]
.as_str()
.ok_or_else(|| PasskiError::new("Missing origin in client data"))?
.to_string();
let cross_origin = json["crossOrigin"].as_bool().unwrap_or(false);
Ok(ClientData {
type_,
challenge,
origin,
cross_origin,
})
}
#[inline] pub fn from_base64(client_data_json: &str) -> Result<ClientData> {
let bytes = crate::Passki::base64_decode(client_data_json)?;
Self::from_bytes(&bytes)
}
#[allow(rustdoc::bare_urls)]
pub fn verify(
&self,
expected_type: ClientDataType,
expected_challenge: &[u8],
expected_origin: &str,
) -> Result<()> {
if self.type_ != expected_type {
return Err(Box::new(PasskiError::new(format!(
"Invalid type: expected {}, got {}",
expected_type, self.type_
))));
}
let challenge = crate::Passki::base64_decode(&self.challenge)?;
if challenge != expected_challenge {
return Err(Box::new(PasskiError::new("Challenge mismatch")));
}
if self.origin != expected_origin {
return Err(Box::new(PasskiError::new(format!(
"Invalid origin: expected {}, got {}",
expected_origin, self.origin
))));
}
Ok(())
}
}