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::InvalidClientDataType(s.to_string())),
}
}
}
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)?;
let type_str = json["type"]
.as_str()
.ok_or_else(|| PasskiError::MissingClientDataField("type".to_string()))?;
let type_ = type_str.parse::<ClientDataType>()?;
let challenge = json["challenge"]
.as_str()
.ok_or_else(|| PasskiError::MissingClientDataField("challenge".to_string()))?
.to_string();
let origin = json["origin"]
.as_str()
.ok_or_else(|| PasskiError::MissingClientDataField("origin".to_string()))?
.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_origins: &[impl AsRef<str>],
) -> Result<()> {
if self.type_ != expected_type {
return Err(PasskiError::ClientDataTypeMismatch {
expected: expected_type,
got: self.type_,
});
}
let challenge = crate::Passki::base64_decode(&self.challenge)?;
if challenge != expected_challenge {
return Err(PasskiError::ChallengeMismatch);
}
if !expected_origins.iter().any(|o| o.as_ref() == self.origin) {
return Err(PasskiError::OriginMismatch {
expected: expected_origins
.iter()
.map(|o| o.as_ref().to_string())
.collect(),
got: self.origin.clone(),
});
}
if self.cross_origin {
return Err(PasskiError::CrossOriginNotAllowed);
}
Ok(())
}
}