225 lines
8.1 KiB
Markdown
225 lines
8.1 KiB
Markdown
# age-verification
|
||
|
||
Implementation of `org.freedesktop.AgeVerification1` — a D-Bus interface for
|
||
California SB-976-style age bracket compliance on Linux, with an optional
|
||
privacy-preserving account sync backend.
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ Linux OS (Kicksecure / Whonix / Debian / etc.) │
|
||
│ │
|
||
│ ┌──────────────────────────────────┐ │
|
||
│ │ age-verification-daemon (C) │ system D-Bus │
|
||
│ │ /var/lib/age-verification/ │ org.freedesktop. │
|
||
│ │ ages.conf (root:root 0600) │ AgeVerification1 │
|
||
│ └────────────────┬─────────────────┘ │
|
||
│ │ D-Bus GetAgeBracket │
|
||
│ ┌────────────────▼─────────────────┐ │
|
||
│ │ age-verification-sync (Rust) │──────────────────────► │
|
||
│ │ Layered encryption: │ HTTPS (TLS) │
|
||
│ │ 1. TLS transport │ POST /api/v1/ │
|
||
│ │ 2. RSA-OAEP server pubkey │ age-bracket │
|
||
│ │ 3. Ed25519 device sig │ │
|
||
│ │ 4. AES-256-GCM local keystore │ │
|
||
│ └──────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────┐
|
||
│ age-verification-server (Node) │
|
||
│ SQLite: stores bracket (1-4) │
|
||
│ ONLY — no raw age/dob stored │
|
||
│ │
|
||
│ GET /api/v1/query/bracket │◄── OS services /
|
||
│ (returns only bracket int) │ app stores
|
||
└─────────────────────────────────┘
|
||
```
|
||
|
||
### Age brackets
|
||
|
||
| Bracket | Meaning |
|
||
|---------|----------------------|
|
||
| 1 | Under 13 |
|
||
| 2 | 13 – 15 (inclusive) |
|
||
| 3 | 16 – 17 (inclusive) |
|
||
| 4 | 18 or older |
|
||
|
||
---
|
||
|
||
## Components
|
||
|
||
### 1. `daemon/` — C D-Bus daemon
|
||
|
||
Implements `org.freedesktop.AgeVerification1` on the **system bus**.
|
||
Runs as root. Stores age brackets (not raw ages) in `/var/lib/age-verification/ages.conf`.
|
||
|
||
**Build:**
|
||
```bash
|
||
cd daemon
|
||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||
cmake --build build
|
||
sudo cmake --install build
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now age-verification-daemon
|
||
```
|
||
|
||
**Test via dbus-send:**
|
||
```bash
|
||
# Set age
|
||
sudo dbus-send --system --print-reply \
|
||
--dest=org.freedesktop.AgeVerification1 \
|
||
/org/freedesktop/AgeVerification1 \
|
||
org.freedesktop.AgeVerification1.SetAge \
|
||
string:youruser uint32:25
|
||
|
||
# Set date of birth
|
||
sudo dbus-send --system --print-reply \
|
||
--dest=org.freedesktop.AgeVerification1 \
|
||
/org/freedesktop/AgeVerification1 \
|
||
org.freedesktop.AgeVerification1.SetDateOfBirth \
|
||
string:youruser string:2000-06-15
|
||
|
||
# Get bracket
|
||
dbus-send --system --print-reply \
|
||
--dest=org.freedesktop.AgeVerification1 \
|
||
/org/freedesktop/AgeVerification1 \
|
||
org.freedesktop.AgeVerification1.GetAgeBracket \
|
||
string:youruser
|
||
```
|
||
|
||
---
|
||
|
||
### 2. `sync-client/` — Rust sync client
|
||
|
||
Reads the bracket from the local daemon and uploads it to your account server
|
||
using layered encryption:
|
||
|
||
1. **TLS** — all traffic over HTTPS via rustls
|
||
2. **RSA-4096 OAEP-SHA256** — bracket payload encrypted with the server's public key
|
||
3. **Ed25519 device keypair** — payload signed to prove device identity
|
||
4. **AES-256-GCM + PBKDF2** — local keystore encrypted with account passphrase
|
||
|
||
**Build:**
|
||
```bash
|
||
cd sync-client
|
||
cargo build --release
|
||
# Binary at target/release/age-verification-sync
|
||
```
|
||
|
||
**First-time device registration** (do this once after account creation):
|
||
```bash
|
||
# Register device with server — you'll need to extract the Ed25519 pubkey
|
||
# from the keystore or add a `--print-pubkey` subcommand for this.
|
||
# See TODO in src/main.rs for a planned 'register-device' subcommand.
|
||
```
|
||
|
||
**Sync:**
|
||
```bash
|
||
age-verification-sync \
|
||
--user alice \
|
||
--server https://accounts.example.com \
|
||
--token "$YOUR_ACCOUNT_JWT"
|
||
```
|
||
|
||
**Deploy as a systemd timer** (syncs on login or periodically):
|
||
```ini
|
||
# /etc/systemd/user/age-verification-sync.timer
|
||
[Unit]
|
||
Description=Sync age bracket
|
||
|
||
[Timer]
|
||
OnBootSec=30s
|
||
OnUnitActiveSec=24h
|
||
|
||
[Install]
|
||
WantedBy=timers.target
|
||
```
|
||
|
||
---
|
||
|
||
### 3. `server/` — Node.js account server
|
||
|
||
Express + SQLite backend. Stores **only the bracket integer** per account.
|
||
|
||
**Setup:**
|
||
```bash
|
||
cd server
|
||
npm install
|
||
npm run keygen # generates keys/server-private.pem + server-public.pem
|
||
cp .env.example .env # fill in JWT_SECRET
|
||
mkdir -p data
|
||
npm start
|
||
```
|
||
|
||
**Distribute the public key to OS installs:**
|
||
```bash
|
||
scp keys/server-public.pem \
|
||
root@client:/etc/age-verification/server-pubkey.pem
|
||
```
|
||
|
||
**API endpoints:**
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/v1/auth/register` | — | Create account |
|
||
| POST | `/api/v1/auth/login` | — | Get JWT |
|
||
| POST | `/api/v1/auth/register-device` | JWT | Register device Ed25519 pubkey |
|
||
| POST | `/api/v1/age-bracket` | JWT | Upload encrypted bracket from sync client |
|
||
| GET | `/api/v1/query/bracket` | JWT | Get bracket for this account (returns `{bracket: N}` only) |
|
||
|
||
**Production deployment:**
|
||
- Put nginx in front (TLS termination, `proxy_pass http://127.0.0.1:3000`)
|
||
- Use a process manager: `pm2 start src/index.js --name age-verif`
|
||
- Back up `data/ageverif.db` and `keys/server-private.pem` (separately, securely)
|
||
|
||
---
|
||
|
||
## Privacy properties
|
||
|
||
| Property | How it's achieved |
|
||
|----------|------------------|
|
||
| Minimum data on disk | Daemon stores bracket (1–4), not raw age |
|
||
| Minimum data on server | Server stores bracket (1–4), not raw age |
|
||
| Local file not world-readable | `/var/lib/age-verification/ages.conf` is root:root 0600 |
|
||
| Local keystore encrypted | AES-256-GCM keyed from PBKDF2(account passphrase, salt) |
|
||
| Transport encrypted | TLS (reqwest/rustls, HTTPS only) |
|
||
| Server can't be MITM'd | Payload also RSA-OAEP encrypted with server pubkey |
|
||
| Device identity verified | Ed25519 signature on every sync payload |
|
||
| Server returns minimum | `/query/bracket` returns `{bracket: N}` only |
|
||
|
||
---
|
||
|
||
## D-Bus interface reference
|
||
|
||
```xml
|
||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||
<node name="/org/freedesktop/AgeVerification1">
|
||
<interface name="org.freedesktop.AgeVerification1">
|
||
<method name="SetAge">
|
||
<arg type="s" name="User" direction="in"/>
|
||
<arg type="u" name="YearsOfAge" direction="in"/>
|
||
</method>
|
||
<method name="SetDateOfBirth">
|
||
<arg type="s" name="User" direction="in"/>
|
||
<arg type="s" name="Date" direction="in"/>
|
||
</method>
|
||
<method name="GetAgeBracket">
|
||
<arg type="s" name="User" direction="in"/>
|
||
<arg type="u" name="AgeBracket" direction="out"/>
|
||
</method>
|
||
</interface>
|
||
</node>
|
||
```
|
||
|
||
**Errors:**
|
||
- `org.freedesktop.AgeVerification1.Error.NoSuchUser`
|
||
- `org.freedesktop.AgeVerification1.Error.PermissionDenied`
|
||
- `org.freedesktop.AgeVerification1.Error.InvalidDate`
|
||
- `org.freedesktop.AgeVerification1.Error.AgeUndefined`
|
||
- `org.freedesktop.AgeVerification1.Error.Internal`
|