use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::errors::PasskeyError;
use crate::session::UserId;
use crate::storage::CacheData;
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct PublicKeyCredentialUserEntity {
pub user_handle: String,
pub name: String,
#[serde(rename = "displayName")]
pub display_name: String,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub(super) struct StoredOptions {
pub(super) challenge: String,
pub(super) user: PublicKeyCredentialUserEntity,
pub(super) timestamp: u64,
pub(super) ttl: u64,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct PasskeyCredential {
#[serde(skip_serializing)]
pub sequence_number: Option<i64>,
pub credential_id: String,
pub user_id: String,
pub public_key: String,
pub aaguid: String,
pub rp_id: String,
pub counter: u32,
pub user: PublicKeyCredentialUserEntity,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_used_at: DateTime<Utc>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub(super) struct UserIdCredentialIdStr {
pub(super) user_id: String,
pub(super) credential_id: String,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub(super) struct SessionInfo {
pub(super) user: crate::session::User,
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum CredentialSearchField {
CredentialId(CredentialId),
UserId(UserId),
UserHandle(UserHandle),
UserName(UserName),
}
impl From<SessionInfo> for CacheData {
fn from(data: SessionInfo) -> Self {
Self {
value: serde_json::to_string(&data).expect("Failed to serialize SessionInfo"),
}
}
}
impl TryFrom<CacheData> for SessionInfo {
type Error = PasskeyError;
fn try_from(data: CacheData) -> Result<Self, Self::Error> {
serde_json::from_str(&data.value).map_err(|e| PasskeyError::Storage(e.to_string()))
}
}
impl From<StoredOptions> for CacheData {
fn from(data: StoredOptions) -> Self {
Self {
value: serde_json::to_string(&data).expect("Failed to serialize StoredOptions"),
}
}
}
impl TryFrom<CacheData> for StoredOptions {
type Error = PasskeyError;
fn try_from(data: CacheData) -> Result<Self, Self::Error> {
serde_json::from_str(&data.value).map_err(|e| PasskeyError::Storage(e.to_string()))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CredentialId(String);
impl CredentialId {
pub fn new(id: String) -> Result<Self, crate::passkey::PasskeyError> {
use crate::passkey::PasskeyError;
if id.is_empty() {
return Err(PasskeyError::Validation(
"Credential ID cannot be empty".to_string(),
));
}
if id.len() < 10 {
return Err(PasskeyError::Validation(
"Credential ID too short".to_string(),
));
}
if id.len() > 1024 {
return Err(PasskeyError::Validation(
"Credential ID too long".to_string(),
));
}
if !id.chars().all(|c| {
c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~' | '=' | '+' | '/')
}) {
return Err(PasskeyError::Validation(
"Credential ID contains invalid characters".to_string(),
));
}
Ok(CredentialId(id))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct UserHandle(String);
impl UserHandle {
#[cfg(test)]
pub fn new(handle: String) -> Self {
Self(handle)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct UserName(String);
impl UserName {
pub fn new(name: String) -> Result<Self, crate::passkey::PasskeyError> {
use crate::passkey::PasskeyError;
if name.is_empty() {
return Err(PasskeyError::Validation(
"Username cannot be empty".to_string(),
));
}
if name.len() > 64 {
return Err(PasskeyError::Validation("Username too long".to_string()));
}
if name.trim().is_empty() {
return Err(PasskeyError::Validation(
"Username cannot consist only of whitespace".to_string(),
));
}
if name.contains("..") || name.contains("--") || name.contains("__") {
return Err(PasskeyError::Validation(
"Username contains dangerous character sequences".to_string(),
));
}
Ok(UserName(name))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChallengeType(String);
impl ChallengeType {
pub fn new(challenge_type: String) -> Result<Self, super::errors::PasskeyError> {
use super::errors::PasskeyError;
if challenge_type.is_empty() {
return Err(PasskeyError::Challenge(
"Challenge type cannot be empty".to_string(),
));
}
if challenge_type.len() > 64 {
return Err(PasskeyError::Challenge(
"Challenge type too long".to_string(),
));
}
if !challenge_type
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return Err(PasskeyError::Challenge(
"Challenge type contains invalid characters".to_string(),
));
}
Ok(ChallengeType(challenge_type))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn registration() -> Self {
ChallengeType("registration".to_string())
}
pub fn authentication() -> Self {
ChallengeType("authentication".to_string())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChallengeId(String);
impl ChallengeId {
pub fn new(id: String) -> Result<Self, super::errors::PasskeyError> {
use super::errors::PasskeyError;
if id.is_empty() {
return Err(PasskeyError::Challenge(
"Challenge ID cannot be empty".to_string(),
));
}
if id.len() < 8 {
return Err(PasskeyError::Challenge(
"Challenge ID too short".to_string(),
));
}
if id.len() > 256 {
return Err(PasskeyError::Challenge("Challenge ID too long".to_string()));
}
if !id
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '+'))
{
return Err(PasskeyError::Challenge(
"Challenge ID contains invalid characters".to_string(),
));
}
Ok(ChallengeId(id))
}
pub fn as_str(&self) -> &str {
&self.0
}
}