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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user