use serde::{Deserialize, Serialize};
use uuid::Uuid;
use webauthn_rs::prelude::*;
use super::invite::Purpose;
use crate::server::error::AppError;
use crate::server::store::{KeyspaceHandle, WriteBatch};
#[derive(Debug, Serialize, Deserialize)]
pub struct CredentialMapping {
pub user_uuid: Uuid,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PasskeyUser {
pub user_uuid: Uuid,
pub did: String,
pub display_name: String,
pub credentials: Vec<Passkey>,
#[serde(default)]
pub labels: std::collections::BTreeMap<String, String>,
#[serde(default)]
pub registered_at: std::collections::BTreeMap<String, u64>,
}
impl PasskeyUser {
pub fn new(did: &str) -> Self {
Self {
user_uuid: Uuid::new_v4(),
did: did.to_string(),
display_name: did.to_string(),
credentials: Vec::new(),
labels: Default::default(),
registered_at: Default::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum CeremonyOrigin {
Invite {
token_hash: String,
invite_id: String,
role: Option<String>,
issued_by: String,
},
Subject { session_id: Option<String> },
}
#[derive(Serialize, Deserialize)]
pub struct Ceremony {
pub enrollment_id: String,
pub subject: String,
pub purpose: Purpose,
pub origin: CeremonyOrigin,
pub user_uuid: Uuid,
pub device_label: Option<String>,
pub registration: PasskeyRegistration,
pub user_verification: Option<PasskeyAuthentication>,
pub expires_at: u64,
}
impl std::fmt::Debug for Ceremony {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ceremony")
.field("enrollment_id", &self.enrollment_id)
.field("subject", &self.subject)
.field("purpose", &self.purpose)
.field("origin", &self.origin)
.field("uv", &self.user_verification.is_some())
.field("expires_at", &self.expires_at)
.finish_non_exhaustive()
}
}
fn auth_state_key(id: &str) -> String {
format!("pk_auth:{id}")
}
fn ceremony_key(id: &str) -> String {
format!("pk_enrol:{id}")
}
fn credential_mapping_key(cred_id_hex: &str) -> String {
format!("pk_cred:{cred_id_hex}")
}
fn passkey_user_key(uuid: &Uuid) -> String {
format!("pk_user:{uuid}")
}
fn passkey_did_key(did: &str) -> String {
format!("pk_did:{did}")
}
pub fn cred_id_hex(id: &CredentialID) -> String {
hex::encode(AsRef::<[u8]>::as_ref(id))
}
pub async fn store_ceremony(ks: &KeyspaceHandle, ceremony: &Ceremony) -> Result<(), AppError> {
ks.insert(ceremony_key(&ceremony.enrollment_id), ceremony)
.await
}
pub async fn take_ceremony(ks: &KeyspaceHandle, id: &str) -> Result<Option<Ceremony>, AppError> {
ks.take(ceremony_key(id)).await
}
pub async fn store_auth_state(
ks: &KeyspaceHandle,
id: &str,
state: &PasskeyAuthentication,
) -> Result<(), AppError> {
ks.insert(auth_state_key(id), state).await
}
pub async fn take_auth_state(
ks: &KeyspaceHandle,
id: &str,
) -> Result<Option<PasskeyAuthentication>, AppError> {
ks.take(auth_state_key(id)).await
}
pub async fn store_passkey_user(ks: &KeyspaceHandle, user: &PasskeyUser) -> Result<(), AppError> {
ks.insert(passkey_user_key(&user.user_uuid), user).await?;
ks.insert_raw(
passkey_did_key(&user.did),
user.user_uuid.to_string().into_bytes(),
)
.await
}
pub fn stage_new_credential(
batch: &mut WriteBatch,
ks: &KeyspaceHandle,
user: &PasskeyUser,
new_cred_id_hex: &str,
) -> Result<(), AppError> {
batch.insert(ks, passkey_user_key(&user.user_uuid), user)?;
batch.insert_raw(
ks,
passkey_did_key(&user.did),
user.user_uuid.to_string().into_bytes(),
);
batch.insert(
ks,
credential_mapping_key(new_cred_id_hex),
&CredentialMapping {
user_uuid: user.user_uuid,
},
)?;
Ok(())
}
pub async fn remove_credential(
ks: &KeyspaceHandle,
user: &mut PasskeyUser,
cred_id_hex: &str,
) -> Result<(), AppError> {
user.credentials
.retain(|c| self::cred_id_hex(c.cred_id()) != cred_id_hex);
user.labels.remove(cred_id_hex);
user.registered_at.remove(cred_id_hex);
ks.remove(credential_mapping_key(cred_id_hex)).await?;
store_passkey_user(ks, user).await
}
pub async fn get_passkey_user(
ks: &KeyspaceHandle,
uuid: &Uuid,
) -> Result<Option<PasskeyUser>, AppError> {
ks.get(passkey_user_key(uuid)).await
}
pub async fn get_passkey_user_by_cred(
ks: &KeyspaceHandle,
cred_id_hex: &str,
) -> Result<Option<PasskeyUser>, AppError> {
let mapping: Option<CredentialMapping> = ks.get(credential_mapping_key(cred_id_hex)).await?;
match mapping {
Some(m) => get_passkey_user(ks, &m.user_uuid).await,
None => Ok(None),
}
}
pub async fn get_passkey_user_by_did(
ks: &KeyspaceHandle,
did: &str,
) -> Result<Option<PasskeyUser>, AppError> {
let Some(bytes) = ks.get_raw(passkey_did_key(did)).await? else {
return Ok(None);
};
let uuid_str = String::from_utf8(bytes)
.map_err(|e| AppError::Internal(format!("invalid DID index UUID: {e}")))?;
let uuid = Uuid::parse_str(&uuid_str)
.map_err(|e| AppError::Internal(format!("invalid DID index UUID: {e}")))?;
get_passkey_user(ks, &uuid).await
}
pub async fn get_all_passkeys(ks: &KeyspaceHandle) -> Result<Vec<Passkey>, AppError> {
let entries = ks.prefix_iter_raw("pk_user:").await?;
let mut passkeys = Vec::new();
for (_key, value) in entries {
if let Ok(user) = serde_json::from_slice::<PasskeyUser>(&value) {
passkeys.extend(user.credentials);
}
}
Ok(passkeys)
}