feat(sp12): SQLCipher encryption at rest (optional)

- New cyclone.db_crypto module:
  * is_sqlcipher_available() — capability check
  * is_encryption_enabled() — Keychain key + sqlcipher3 present
  * get_db_key() — reads 'cyclone.db.key' from Keychain
  * make_sqlcipher_connect_creator(url, key) — SQLAlchemy creator
- db._make_engine() now switches to SQLCipher when key is present
- pyproject.toml: optional 'sqlcipher' extra (sqlcipher3>=0.6,<1)
- Fallback: without Keychain key, DB stays plain SQLite (no surprise
  behavior for operators who haven't set up encryption yet)
- Verified: encrypted file is unreadable as plain SQLite, wrong key
  raises on first query, migrations + ORM work transparently
- HIPAA §164.312(a)(2)(iv) compliance note in docs

Tests: 705 -> 717 (12 new for SQLCipher). All 717 backend tests pass.
This commit is contained in:
Tyler
2026-06-20 23:52:41 -06:00
parent 84d2f39760
commit d54c44f04a
5 changed files with 406 additions and 1 deletions
+44
View File
@@ -147,6 +147,50 @@ All CMS POS codes `01``99` are accepted. The canonical list lives in
`cyclone/parsers/payer.py` as `CMS_PLACE_OF_SERVICE_CODES` and is the
source of truth for validation and any UI dropdowns.
## Database encryption at rest (SP12)
Cyclone optionally encrypts the SQLite database with SQLCipher
(AES-256). The encryption key is stored in macOS Keychain — never on
disk in plaintext. Without the key, the DB falls back to plain SQLite.
### One-time operator setup
```bash
# 1. Install SQLCipher (C library) and the Python binding.
brew install sqlcipher
pip install -e backend[sqlcipher]
# 2. Generate a random 32-byte key and store it in Keychain.
python3 -c "import secrets; print(secrets.token_urlsafe(32))" \
| xargs -I {} security add-generic-password \
-s cyclone -a cyclone.db.key -w "{}"
# 3. Restart Cyclone. The DB is now encrypted.
```
### Verification
```bash
# The DB file should be unreadable as plain SQLite.
sqlite3 ~/.local/share/cyclone/cyclone.db "SELECT count(*) FROM sqlite_master"
# → file is not a database
# But readable through Cyclone.
curl http://localhost:8000/api/claims
# → 200 OK
```
### Key rotation (future)
To rotate the key:
1. Decrypt with old key, dump to SQL
2. Re-encrypt with new key, import SQL
3. Update Keychain
A first-class rotation endpoint is out of scope for SP12 (planned for
SP14+).
## Audit log (SP11)
Cyclone persists every state-changing event to a tamper-evident