Huge refactor, submission system addition & security improvements. +Implementation of moderation cmds.

This commit is contained in:
unknown
2026-05-22 21:46:06 +02:00
parent 12a0035699
commit 2129081599
32 changed files with 3426 additions and 106 deletions

194
docs/AUTH_FLOW.md Normal file
View File

@@ -0,0 +1,194 @@
# Authentication Flow
cg.cx uses a simple password-and-cookie authentication model. There is no user account system; access is granted per-content-item by knowing its password.
---
## AuthSource Enum
The server tracks *how* a request was authenticated using the `AuthSource` enum:
```rust
enum AuthSource {
Cookie, // Request presented a valid cgcx_pw cookie
QueryParam, // Request presented a correct ?sc=PASSWORD query param
}
```
This distinction matters because the server only issues a **new** cookie when auth succeeds via `QueryParam`. Cookie-based auth does not re-issue the cookie (the browser already has it).
---
## How Direct Links with `?sc=PASSWORD` Work
1. A client requests a protected endpoint (e.g., `GET /api/content/:cxid` or `GET /api/content/:cxid/file/:file_idx`) with the password in the query string:
```
GET /api/content/abc123?sc=MySecretPassword
```
2. The handler calls `password_from_request(...)`.
3. Inside `password_from_request`, the `sc` query parameter is checked **first**:
- If `sc` is present and the content has a `password_hash`, the server verifies the supplied password against the stored Argon2 hash.
- If verification succeeds, `password_from_request` returns `Some(AuthSource::QueryParam)`.
4. The handler proceeds with the request.
5. On the response path, the handler calls `add_auth_cookie(...)`.
6. Because the auth source is `QueryParam`, `add_auth_cookie` generates a new HMAC-signed `cgcx_pw` cookie and attaches it via a `Set-Cookie` header.
7. The clients browser stores the cookie. Subsequent requests to the same domain automatically include it, so the `?sc=...` parameter is no longer needed.
---
## How the Server Validates Passwords (Argon2)
Password verification happens in two places: `password_from_request` and the `verify_password` endpoint.
### Stored Hash Format
Password hashes are stored in the database as Argon2id hashes in the standard PHC string format (e.g., `$argon2id$v=19$m=65536,t=3,p=4$...`).
### Verification
```rust
use argon2::{Argon2, PasswordHash, PasswordVerifier};
let parsed_hash = PasswordHash::new(&hash)?;
let valid = Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok();
```
- Uses the default Argon2 parameters.
- If parsing the stored hash fails, verification is treated as failed.
- If verification fails, the server returns `401 Unauthorized`.
---
## How Cookies Are Set After Successful Query-Param Auth
After a successful `?sc=...` authentication, the response includes:
```http
Set-Cookie: cgcx_pw=<value>; Max-Age=3600; SameSite=Strict; HttpOnly; Path=/
```
- **Name:** `cgcx_pw`
- **Max-Age:** 3600 seconds (1 hour)
- **SameSite:** `Strict`
- **HttpOnly:** `true` (not accessible to JavaScript)
- **Path:** `/`
The cookie value is generated by `make_cookie_value(cxid, cookie_secret)` (see [Cookie Format](#cookie-format-hmac-signed) below).
The same cookie is also set by the `POST /api/content/:cxid/verify-password` endpoint after a successful JSON password verification.
---
## How Subsequent Requests Use the Cookie
1. The browser automatically sends the `cgcx_pw` cookie on every request to the same origin.
2. When `password_from_request` is called:
- It first checks for `sc` query param auth.
- If that fails or is absent, it scans **all** `Cookie` headers (there may be multiple).
- For each cookie header, it splits on `;` and looks for a part starting with `cgcx_pw=`.
- When found, it calls `verify_cookie(cxid, &part[8..], cookie_secret)`.
- If `verify_cookie` returns `true`, `password_from_request` returns `Some(AuthSource::Cookie)`.
3. Because the auth source is `Cookie`, `add_auth_cookie` does **not** add a new `Set-Cookie` header.
---
## Cookie Format (HMAC-Signed)
The cookie value is a Base64-encoded string of the form:
```
base64(cxid + ":" + hmac_sha256(cxid, secret))
```
### Building the Cookie (`make_cookie_value`)
```rust
fn make_cookie_value(cxid: &str, secret: &[u8]) -> String {
let mac = hmac_cookie(cxid, secret); // HMAC-SHA256 of cxid
let mut raw = Vec::with_capacity(cxid.len() + 1 + mac.len());
raw.extend_from_slice(cxid.as_bytes());
raw.push(b':');
raw.extend_from_slice(&mac);
base64::engine::general_purpose::STANDARD.encode(&raw)
}
```
### Verifying the Cookie (`verify_cookie`)
```rust
fn verify_cookie(cxid: &str, cookie_value: &str, secret: &[u8]) -> bool {
let decoded = base64_decode(cookie_value)?;
let mut parts = decoded.splitn(2, |&b| b == b':');
let decoded_cxid = std::str::from_utf8(parts.next()?)?;
let mac_bytes = parts.next()?;
if decoded_cxid != cxid {
return false;
}
let expected = hmac_cookie(cxid, secret);
if mac_bytes.len() != expected.len() {
return false;
}
// Constant-time comparison to prevent timing attacks
mac_bytes.ct_eq(&expected).into()
}
```
### Cookie Secret
The HMAC key (`cookie_secret`) is derived from the master encryption key at startup:
```rust
let cookie_secret = blake3::hash(master_key.as_bytes()).as_bytes().to_vec();
```
This means:
- The cookie secret is deterministic for a given master key.
- Cookies are bound to a specific server instance (or cluster sharing the same master key).
- Changing the master key invalidates all existing auth cookies.
### Security Properties
- **Binding:** The HMAC includes the `cxid`, so a cookie for one piece of content cannot be replayed against another.
- **Tamper resistance:** Without the `cookie_secret`, an attacker cannot forge a valid `cgcx_pw` cookie.
- **Timing safety:** Verification uses `subtle::ConstantTimeEq` to avoid leaking information via timing side channels.
---
## Summary Flow Diagram
```
Client Request
┌─────────────────────────────────────────┐
│ Does the content have a password? │
└─────────────────────────────────────────┘
├─ No ──► Proceed (no auth needed)
└─ Yes
┌─────────────────────────────────────────┐
│ Is ?sc=PASSWORD present and correct? │
│ └─ Argon2 verify against stored hash │
└─────────────────────────────────────────┘
├─ Yes ──► AuthSource::QueryParam
│ └─ Set cgcx_pw cookie on response
└─ No
┌─────────────────────────────────────────┐
│ Is cgcx_pw cookie present and valid? │
│ └─ HMAC-SHA256 verify against secret │
└─────────────────────────────────────────┘
├─ Yes ──► AuthSource::Cookie
│ └─ Proceed without setting new cookie
└─ No ──► 401 Unauthorized
```