Skip to main content

max / makenotwork

2.5 KB · 65 lines History Blame Raw
1 //! SyncKit security: per-user token revocation and the security audit log.
2
3 use sqlx::PgPool;
4
5 use crate::db::{SyncAppId, UserId};
6 use crate::error::Result;
7
8 /// Security audit event types. Kept as a small closed set so the column stays
9 /// queryable; the variable context goes in `detail` (JSONB).
10 pub mod sync_security_event {
11 /// A `/api/sync/auth` attempt was denied (wrong password, suspended,
12 /// locked, or 2FA-gated, all answered identically per the oracle fix).
13 pub const AUTH_FAILURE: &str = "auth_failure";
14 /// A sync device was removed; the user's sync tokens were invalidated.
15 pub const DEVICE_REMOVED: &str = "device_removed";
16 /// A key rotation completed (all devices re-encrypted to the new key).
17 pub const KEY_ROTATION_COMPLETED: &str = "key_rotation_completed";
18 }
19
20 /// Append one row to the SyncKit security audit log. Best-effort by contract:
21 /// callers should not fail the request if this fails, log and move on, so the
22 /// audit trail can never become a denial-of-service lever on the path it audits.
23 #[tracing::instrument(skip_all)]
24 pub async fn record_security_event(
25 pool: &PgPool,
26 app_id: SyncAppId,
27 user_id: Option<UserId>,
28 event_type: &str,
29 detail: Option<serde_json::Value>,
30 ip: Option<&str>,
31 ) -> Result<()> {
32 sqlx::query(
33 "INSERT INTO sync_security_events (app_id, user_id, event_type, detail, ip)
34 VALUES ($1, $2, $3, $4, $5)",
35 )
36 .bind(app_id)
37 .bind(user_id)
38 .bind(event_type)
39 .bind(detail)
40 .bind(ip)
41 .execute(pool)
42 .await?;
43 Ok(())
44 }
45
46 /// Invalidate every outstanding SyncKit JWT for a user by stamping
47 /// `sync_jwt_invalidated_at = NOW()`. The extractor rejects any sync token with
48 /// `iat <= sync_jwt_invalidated_at`, so all the user's sync sessions must
49 /// re-authenticate. Deliberately separate from `jwt_invalidated_at`: this does
50 /// NOT disturb the user's website sessions.
51 ///
52 /// Note on key rotation: in the zero-knowledge model the server never holds the
53 /// data-encryption key, so it cannot rotate it on the user's behalf, that is a
54 /// client action. Invalidating tokens (forcing re-auth) is the strongest
55 /// server-side response to a device removal; the audit log records the removal
56 /// so a client/operator can drive an actual key rotation if warranted.
57 #[tracing::instrument(skip_all)]
58 pub async fn invalidate_user_sync_tokens(pool: &PgPool, user_id: UserId) -> Result<()> {
59 sqlx::query("UPDATE users SET sync_jwt_invalidated_at = NOW() WHERE id = $1")
60 .bind(user_id)
61 .execute(pool)
62 .await?;
63 Ok(())
64 }
65