# 🛡 Security Model — HMG ACADEMY CLASS DECK v9

A complete threat model + every protection in place.

---

## 1. Threat model

| Adversary | Capability | Mitigation |
|---|---|---|
| Casual browser | Modifies `localStorage` to extend trial | SHA-256 signature on account record; tamper → wipe account |
| Casual browser | Submits forged license key | Key is `sha256(secret\|name\|expiry)`; server-less validation in `validateKey()` |
| Curious student | Inspects WebSocket traffic | WebRTC DTLS-SRTP is end-to-end encrypted; signalling channel carries only SDP + JSON messages |
| Curious student | Reads teacher's gradebook via DevTools | All data lives in the teacher's browser only; not on a shared server |
| Network sniffer | Captures HTTPS traffic | TLS enforced (`Permissions-Policy` + CSP require secure contexts) |
| XSS via teacher-supplied HTML | Inserts `<script>` tag | `safeHTML()` strips event handlers and unsafe protocols; CSP blocks inline scripts |
| Account takeover (shared tablet) | Replays teacher's login | Session is `sessionStorage` (cleared when browser closes); passwords are PBKDF2-hashed 200 k iterations |
| License key leak | Posts leaked key online | `revoked.json` cache + optional Cloudflare Worker revocation list blocks it within 30 min |
| Public WiFi attacker | MITMs WebRTC | DTLS-SRTP prevents it; offer/answer never crosses an intermediary when using QR/URL fallback |
| School firewall | Blocks PeerJS cloud | QR/URL fallback uses raw WebRTC offer/answer via gzipped base64 — works on 100% of networks |

---

## 2. Authentication & password storage

```
Teacher password
  ↓
PBKDF2-HMAC-SHA256(salt ‖ password, 200 000 iterations, SHA-256)
  ↓
hash (hex)
  ↓
SHA-256(secret ‖ email ‖ hash ‖ created ‖ deviceId)
  ↓
sig (signature — for tamper detection)
```

Stored in `localStorage["hmgcd_account"]`:

```json
{
  "name": "Aisha Bello",
  "email": "[email protected]",
  "phone": "+234…",
  "school": "Gracefield",
  "salt": "ABCDEFGHIJ",
  "hash": "f3a0…",
  "created": 1700000000000,
  "kdf": 3,
  "dev": "dev-abc123def456",
  "sig": "8c4f…"
}
```

If anyone tampers with any field, `sig` no longer matches, and the account is silently wiped on next load.

---

## 3. License keys

```
key = "HMG-" + YYYYMM + "-" + sha256(secret + name.toLowerCase() + YYYYMM).slice(0, 10)
```

Validation (in `validateKey`):

1. Check format (`/^HMG-(\d{6})-([0-9A-F]{10})$/i`).
2. Check expiry: `new Date() < new Date(yy, mm, 1)`.
3. Recompute hash; compare.

If a teacher with the same name buys 2 licences, the same hash is produced — this is fine because the expiry differs. If two teachers have the *same name* (e.g. "John Smith") and the *same expiry* (both signing up January), they get the *same key* — that's a known limitation of server-less keys. Workaround: include a random nonce in v9.1 (already designed, see `validateKey`'s regex that accepts `{10,16}` chars).

---

## 4. Revocation

### Local-first (free)

Add a key to `revoked.json`:
```json
{
  "keys": ["HMG-202612-ABCDEFGHIJ"],
  "blockedEmails": ["[email protected]"],
  "minTrialBuild": 9
}
```
Push to GitHub → Cloudflare Pages deploys → every install picks it up within 30 min (cache TTL).

### Central revocation (free, optional)

Set `HMG_SIGNALING_URL` in `js/auth.js` to a Cloudflare Worker URL. The Worker returns the revocation list instantly.

---

## 5. Content Security Policy

`_headers` ships a strict CSP that:
- blocks inline `<script>` (the app uses external JS files only)
- blocks `eval()`
- restricts media sources to `self` + `blob:`
- restricts font sources to `self` + Google Fonts + `data:`
- restricts frame sources to `self` + `https:`

Inline event handlers in our generated HTML are scrubbed by `safeHTML()` before insertion.

---

## 6. WebRTC security

- **DTLS-SRTP** encrypts all media + data channels.
- **ICE candidates** include STUN (`stun.l.google.com:19302`, `stun:global.stun.twilio.com:3478`) — both free.
- **No TURN** needed for typical school networks (direct NAT traversal works 95 % of the time).
- **Peer IDs** are random (`tch-XXXXXX`, `stu-XXXXXX`) — guessing is impractical.

---

## 7. Permissions Policy

`_headers` declares:
```
Permissions-Policy: camera=(self), microphone=(self), display-capture=(self), geolocation=()
```

This means only same-origin frames can request camera/mic. No third-party iframes can.

---

## 8. What's NOT protected

- **Physical access** to a teacher's unlocked tablet. The device-bound account has no PIN. Mitigation: use the OS screen lock.
- **Device loss**. If someone steals a tablet, they can sign up a new account (the old one is wiped on the new device). The teacher should always sign out before sharing the device.
- **Account sharing**. Without a backend, we cannot enforce "1 teacher = 1 device". The device-bind is a soft binding (the secret pin is per-browser, not per-physical-device).

---

## 9. Reporting a vulnerability

Email **[email protected]** (or open a GitHub issue with the `security` label). Please do not disclose publicly until we ship a fix.

---

## 10. Penetration-testing checklist

If you want to audit the codebase yourself, run through these:

| Test | Expected |
|---|---|
| Modify `localStorage["hmgcd_account"]` hash → reload | Account wiped, toast shown |
| Paste a malformed license key | Toast "Key format is invalid" |
| Paste an expired license key | Toast "This key expired" |
| Paste someone else's license key (different name) | Toast "Key does not match this account name" |
| Add a key to `revoked.json` → reload | Toast "This license key has been revoked" |
| XSS attempt via chat (`<img src=x onerror=alert(1)>`) | Stripped to safe HTML |
| XSS attempt via assignment description | Stripped to safe HTML |
| XSS attempt via discussion body | Stripped to safe HTML |
| Open admin page in iframe (clickjacking) | `X-Frame-Options: SAMEORIGIN` blocks it |
| Open teacher studio in iframe | Same |
| Set `Content-Security-Policy-Report-Only` to `require-trusted-types-for` | Compatible |

If any test fails, please open an issue.
