# BUILD NOTES — SENTINEL Air-Gapped Encrypted Tokenized Vault

**Component:** `sentinel/vault/`
**Built:** 2026-08-29
**Status:** Working, runnable prototype. All self-tests passing.

---

## What was built

A complete, genuinely runnable encrypted vault with three interfaces on top of
one core library, plus a demo seeder and an automated test suite.

| File | Role |
| ---- | ---- |
| `vault.py` | Core crypto library. AES-256-GCM per item; PBKDF2/scrypt KDF; encrypted index; tokenization. |
| `tiers.py` | Tier 1/2/3 profiles with escalating KDF strength + controls; Tier-3→physical-air-gap note. |
| `cli.py` | `init` / `add` / `list` / `get` / `delete`, masked passphrase entry (getpass). |
| `app.py` + `templates/` | Flask web UI (loopback only) in the dark security-console style, with air-gap banner. |
| `demo_seed.py` | Seeds a demo vault with 5 synthetic files (no real PII). |
| `selftest.py` | 23 automated checks: crypto roundtrip, tamper detection, listing masking, tier escalation, full UI flow. |
| `README.md` | Setup, run, tiers, air-gap guarantees, security notice. |
| `requirements.txt` | `flask`, `cryptography`. |

---

## Key design decisions

- **AES-256-GCM (AEAD)** for every item and for the index. GCM provides both
  confidentiality and integrity, so any tampering with a stored blob or the
  index is detected on decryption (surfaced as `VaultLockedError`).
- **Per-item encryption** — each item is a separate `vault_store/<token>.enc`
  blob with its own random 96-bit nonce. The item's token is bound to its
  ciphertext via AES-GCM **AAD**, so a blob can't be silently swapped for
  another item's blob.
- **KDF from passphrase + salt** — PBKDF2-HMAC-SHA256 (Tier 1) or scrypt
  (Tiers 2/3). A random 128-bit salt is stored in the non-secret `vault.json`;
  the KDF parameters are persisted so the vault reopens correctly even if tier
  defaults change later. The passphrase is **never** stored.
- **Tokenization** — items are addressed by opaque `tok_<urlsafe-random>`
  handles. The real filename lives only inside the encrypted index; listings
  expose only the token, a **masked** name (first char + `*` + extension + a
  short hash fragment), size, and timestamp.
- **Air-gapped by construction** — core files import no networking libraries
  (audited: zero matches for socket/requests/urllib/http). The Flask server
  binds to `127.0.0.1` only and serves inlined CSS (no CDN/remote fonts), so it
  renders offline. The UI shows a persistent "Offline / Air-Gapped Mode" banner.
- **Ephemeral session key** — the Flask secret key is generated per process, so
  sessions don't survive a restart and no long-lived server secret is on disk.
- **Best-effort secure delete** — on `delete_item`, the ciphertext blob is
  overwritten with random bytes and fsync'd before unlink (true erasure depends
  on the underlying storage medium; noted as a prototype limitation).

## Tier model

Higher tier ⇒ stronger, slower KDF + more controls:

- Tier 1: PBKDF2, 600k iterations (OWASP floor), single passphrase.
- Tier 2: scrypt N=2¹⁷ (memory-hard), multi-user, 7-year retention.
- Tier 3: scrypt N=2²⁰, hardware key required, two-person delete, data-diode I/O.

`tiers.py` includes an extended comment explaining that software alone cannot
make a system air-gapped — Tier 3 assumes physical isolation (no NICs/radios,
data diode / sneakernet, hardware root of trust, TEMPEST/SCIF).

---

## Self-test results

Run: `python selftest.py` → **exit 0, ALL 23 TESTS PASSED**, including:

- token has opaque prefix; listing hides real name; size correct
- **roundtrip bytes identical** (5 KB random payload encrypt→decrypt)
- real name recovered only via `get_item`
- wrong passphrase rejected / right passphrase accepted
- **GCM detects tampered ciphertext**
- delete removes item + blob
- **no plaintext or real filename found anywhere on disk**
- tier KDF escalation (tier2 > tier1; tier3 strongest + hardware key)
- full Flask flow via test client: setup → unlock → add → list (real name hidden)
  → download (bytes identical) → delete → lock → unlock-required

Additional manual verification:
- CLI end-to-end (`init`/`add`/`list`/`get`/`delete`) roundtrip confirmed.
- `demo_seed.py` populated 5 synthetic items; live server on `127.0.0.1`
  returned the list with 5 tokens, masked names, air-gap banner, and tier pill,
  with **no real filename leaked** in the rendered HTML.

---

## Legal-guardrail compliance (per `shared/BRIEF.md`)

- Purely defensive: stores the user's OWN data on the user's OWN machine.
- No outbound access to any system; no hack-back, no exfiltration, no retaliation.
- All demo data is SYNTHETIC — no real PII, credentials, or IPs.

---

## Known limitations (prototype)

- Not independently security-audited.
- Secure delete is best-effort (medium-dependent); no anti-forensic guarantees.
- Multi-user (Tier 2/3) and hardware-key enforcement are modeled in config but
  not wired to real HSM/smartcard hardware in this prototype.
- The Flask dev server is for local use only; a production deployment would use
  a hardened WSGI server — though for a true air-gap the intended use is
  single-operator on localhost.

## How to run

```bash
pip install -r requirements.txt
python demo_seed.py      # optional synthetic data (passphrase: demo-pass-123)
python app.py            # http://127.0.0.1:5000
# or
python cli.py init && python cli.py add <file> && python cli.py list
python selftest.py       # re-run the automated checks
```
