pub mod ceremony;
pub mod error;
pub mod session;
pub mod verify;
pub use ceremony::{
authentication_options_json, generate_challenge, registration_options_json,
verify_authentication, verify_registration, AuthenticationOutcome, RegistrationOutcome,
};
pub use error::PasskeyError;
pub use session::{open_challenge, seal_challenge, CeremonyPurpose, CHALLENGE_TTL};
use crate::sql::{Auto, Pool};
use crate::Model;
#[derive(Model, Debug, Clone)]
#[rustango(table = "rustango_webauthn_credentials", managed = false)]
#[allow(dead_code)]
pub struct WebauthnCredential {
#[rustango(primary_key)]
pub id: Auto<i64>,
#[rustango(index)]
pub user_id: i64,
#[rustango(max_length = 255, unique)]
pub credential_id: String,
pub public_key: Vec<u8>,
#[rustango(default = "0")]
pub sign_count: i64,
#[rustango(max_length = 64, default = "''")]
pub label: String,
#[rustango(default = "now()")]
pub created_at: chrono::DateTime<chrono::Utc>,
}
pub async fn ensure_table(pool: &Pool) -> Result<(), sqlx::Error> {
use crate::core::Model as _;
let snapshot =
crate::migrate::SchemaSnapshot::from_models_forced(&[WebauthnCredential::SCHEMA]);
crate::migrate::apply_idempotent(pool, &snapshot).await?;
Ok(())
}
pub async fn for_user(
pool: &Pool,
user_id: i64,
) -> Result<Vec<WebauthnCredential>, crate::sql::ExecError> {
use crate::sql::FetcherPool as _;
WebauthnCredential::objects()
.filter("user_id", user_id)
.fetch(pool)
.await
}
pub async fn by_credential_id(
pool: &Pool,
credential_id: &str,
) -> Result<Option<WebauthnCredential>, crate::sql::ExecError> {
use crate::sql::FetcherPool as _;
Ok(WebauthnCredential::objects()
.filter("credential_id", credential_id.to_owned())
.fetch(pool)
.await?
.into_iter()
.next())
}
pub async fn register(
pool: &Pool,
user_id: i64,
credential_id: &str,
public_key: Vec<u8>,
sign_count: i64,
label: &str,
) -> Result<(), crate::sql::ExecError> {
let mut row = WebauthnCredential {
id: Auto::Unset,
user_id,
credential_id: credential_id.to_owned(),
public_key,
sign_count,
label: label.to_owned(),
created_at: chrono::Utc::now(),
};
row.insert_pool(pool).await?;
Ok(())
}
#[must_use]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SignCountUpdate {
Advanced,
NoCounter,
Stale,
Unknown,
}
impl SignCountUpdate {
#[must_use]
pub fn is_accepted(self) -> bool {
matches!(self, Self::Advanced | Self::NoCounter)
}
}
pub async fn update_sign_count(
pool: &Pool,
credential_id: &str,
new_count: i64,
) -> Result<SignCountUpdate, crate::sql::ExecError> {
use crate::sql::UpdaterPool as _;
if new_count > 0 {
let moved = WebauthnCredential::objects()
.filter("credential_id", credential_id.to_owned())
.filter("sign_count__lt", new_count)
.update()
.set("sign_count", new_count)
.execute_pool(pool)
.await?;
if moved > 0 {
return Ok(SignCountUpdate::Advanced);
}
}
Ok(match by_credential_id(pool, credential_id).await? {
None => SignCountUpdate::Unknown,
Some(c) if c.sign_count == 0 && new_count == 0 => SignCountUpdate::NoCounter,
Some(_) => SignCountUpdate::Stale,
})
}