//! SyncKit security: per-user token revocation and the security audit log. use sqlx::PgPool; use crate::db::{SyncAppId, UserId}; use crate::error::Result; /// Security audit event types. Kept as a small closed set so the column stays /// queryable; the variable context goes in `detail` (JSONB). pub mod sync_security_event { /// A `/api/sync/auth` attempt was denied (wrong password, suspended, /// locked, or 2FA-gated, all answered identically per the oracle fix). pub const AUTH_FAILURE: &str = "auth_failure"; /// A sync device was removed; the user's sync tokens were invalidated. pub const DEVICE_REMOVED: &str = "device_removed"; /// A key rotation completed (all devices re-encrypted to the new key). pub const KEY_ROTATION_COMPLETED: &str = "key_rotation_completed"; } /// Append one row to the SyncKit security audit log. Best-effort by contract: /// callers should not fail the request if this fails, log and move on, so the /// audit trail can never become a denial-of-service lever on the path it audits. #[tracing::instrument(skip_all)] pub async fn record_security_event( pool: &PgPool, app_id: SyncAppId, user_id: Option, event_type: &str, detail: Option, ip: Option<&str>, ) -> Result<()> { sqlx::query( "INSERT INTO sync_security_events (app_id, user_id, event_type, detail, ip) VALUES ($1, $2, $3, $4, $5)", ) .bind(app_id) .bind(user_id) .bind(event_type) .bind(detail) .bind(ip) .execute(pool) .await?; Ok(()) } /// Invalidate every outstanding SyncKit JWT for a user by stamping /// `sync_jwt_invalidated_at = NOW()`. The extractor rejects any sync token with /// `iat <= sync_jwt_invalidated_at`, so all the user's sync sessions must /// re-authenticate. Deliberately separate from `jwt_invalidated_at`: this does /// NOT disturb the user's website sessions. /// /// Note on key rotation: in the zero-knowledge model the server never holds the /// data-encryption key, so it cannot rotate it on the user's behalf, that is a /// client action. Invalidating tokens (forcing re-auth) is the strongest /// server-side response to a device removal; the audit log records the removal /// so a client/operator can drive an actual key rotation if warranted. #[tracing::instrument(skip_all)] pub async fn invalidate_user_sync_tokens(pool: &PgPool, user_id: UserId) -> Result<()> { sqlx::query("UPDATE users SET sync_jwt_invalidated_at = NOW() WHERE id = $1") .bind(user_id) .execute(pool) .await?; Ok(()) }