Repository navigation
23 Encryption and Private Storage
HealthBook AI (internal code name: BabyCare) encrypts sensitive data in the application before it reaches the database or the object store. One Fernet key (AES-128-CBC with HMAC-SHA256) protects clinical text columns, OCR transcriptions, original documents, stool photos, camera credentials, access tickets and migration snapshots. The blob store, whether a local directory or a private Supabase bucket, only ever holds ciphertext. Backups add a second, separate key.
- Encrypt clinical free text transparently through the
EncryptedTextSQLAlchemy type. - Encrypt every uploaded original before storage, and decrypt it server-side only for an authenticated, authorized request.
- Refuse to start with a missing key when encrypted data already exists, instead of generating a replacement key.
- Keep the object store private: no public bucket, no public URLs, and access only from the backend.
- Produce backups encrypted with their own key, and restore only into empty destinations.
| Data | Where it is encrypted | Where it is stored |
|---|---|---|
| Names, titles, instructions, doses, lab values, notes, JSON snapshots |
EncryptedText.process_bind_param writes enc:<fernet token>
|
Database TEXT columns |
| OCR or AI transcription |
encrypt(text), stored in documents.extracted_cipher
|
Database |
| Uploaded PDF, PNG, JPEG or HEIC |
encrypt(raw) in the upload handler |
storage.write_document(<48 hex>.enc) |
| Stool photos and intake images |
encrypt(raw) in care_api
|
Same blob store |
| Signed file ticket |
encrypt(json) with a 120 s TTL |
Returned to the client only |
| Pre-migration SQLite snapshot | encrypt(snapshot.serialize()) |
DATA_DIR/before-migration-*.sqlite.enc |
| Backups | Fernet(BABYCARE_BACKUP_KEY) |
Operator-chosen path |
crypto() resolves the application key in this order:
- If
BABYCARE_FERNET_KEYis set, it is used. Production requires it:api/config.pyraises at import time whenBABYCARE_ENV=productionand the variable is missing. - Otherwise the key comes from
DATA_DIR/file_crypt.key, created withO_CREAT | O_EXCLand mode0600. - If that file is missing, the function first checks for existing encrypted data: any
encrypted_files/*.enc, anybefore-migration-*.enc, or (on SQLite) anypatients.namestarting withenc:. If any exists, it raisesRuntimeError('Existing encrypted records have no application key. Restore the original key; no replacement key was generated.'). A replacement key would leave the old records permanently unreadable alongside new ones.
EncryptedText.process_result_value decrypts values that carry the enc: prefix and passes legacy plaintext through unchanged. Migration 0002 re-encrypts those legacy rows in place. The type's docstring states the limit: dates, ids, statuses and enum columns are not encrypted and depend on disk or database encryption. Database and Data Model lists which columns are encrypted.
| Backend | Selected by | Behaviour |
|---|---|---|
LocalStorage |
BABYCARE_STORAGE=local (default) |
DATA_DIR/encrypted_files, directory mode 0700. Writes use O_EXCL and 0600, so an existing original is never overwritten. |
SupabaseStorage |
BABYCARE_STORAGE=supabase |
Supabase Storage REST API at https://<ref>.supabase.co/storage/v1, using the service-role key, which stays on the server. Objects live under documents/ in the bucket (default medical-documents). Writes use x-upsert: false. Transport errors are retried three times. |
Shared rules:
- Object names must be under 70 characters with no
/,\or leading dot. - The module never decrypts, never logs object names next to patient identifiers, and never builds public URLs.
-
ensure_bucket()creates the bucket withpublic: falseand a 13 MB size limit. If an existing bucket is public, it raisesStorageError, and on startupprepare_storage()fails closed. - In production, local storage is refused unless
BABYCARE_ALLOW_LOCAL_STORAGE=true, because free hosts have ephemeral disks. -
MissingObjectmaps to 404 andStorageErrorto 503 through handlers that reveal no internals.
flowchart TD
U["Authenticated upload"] --> V["own_patient check"]
V --> S["Sniff magic bytes<br/>PDF PNG JPEG HEIC"]
S --> L["Limits<br/>12 MB, 35 pages, 25 MP"]
L --> HSH["SHA-256 duplicate check<br/>409 if already uploaded"]
HSH --> E["Fernet encrypt raw bytes"]
E --> W["write_document<br/>random name dot enc"]
W -->|"local"| LD[("encrypted_files dir")]
W -->|"supabase"| SB[("Private bucket<br/>service-role only")]
W --> ROW["documents row<br/>encrypted title and name"]
ROW -->|"DB failure"| DEL["delete_document rollback"]
R["Download, page, thumbnail<br/>or signed ticket"] --> OWN["own_resource check"]
OWN --> RD["read_document"]
RD --> DEC["Fernet decrypt on server"]
DEC --> REN["Render page PNG or<br/>thumbnail if requested"]
REN --> OUT["Response with no-store<br/>and nosniff"]
If storage fails during an upload, the handler returns 503 "Encrypted document store unavailable; nothing was saved". If the database commit fails afterwards, the blob it just wrote is deleted.
-
GET /api/documents/{did}/pages/{page}decrypts the original and renders one page to PNG (PyMuPDF for PDFs, Pillow for images, at most 1800 px). -
GET /api/documents/{did}/thumbnail?w=returns a PNG or JPEG preview between 80 and 800 px wide. - Clients never receive a storage URL or a key; they only receive the rendered bytes.
-
POST /api/documents/{did}/signed-accessreturns/api/files/{ticket}. The ticket is a Fernet token over{document_id, user_id, session_hash, expires}, valid for 120 seconds (decrypt(..., ttl=120)).GET /api/files/{ticket}checks that the session still exists and is unexpired and re-runsown_resource. Signing out or revoking caregiver access therefore invalidates outstanding tickets immediately. - Public doctor-share documents are decrypted per request only after the token, expiry, revocation and PIN checks pass. See Share With Doctor Architecture.
| Tool | Scope | Key handling |
|---|---|---|
python -m api.backup create PATH |
SQLite only: database snapshot, encrypted_files/*.enc, and the application key |
The whole archive is encrypted with BABYCARE_BACKUP_KEY, which is required and must be stored apart from the backups. Refuses PostgreSQL. |
api.backup restore SRC --destination DIR |
Restores into a directory that must not already exist | Rejects path traversal and entries over 1 GB, and writes with xb
|
scripts/cloud_backup.py create/verify/restore |
Any database and any store: every table as JSON (datasync), every stored object, and a manifest with row counts and SHA-256 hashes |
BABYCARE_BACKUP_KEY encrypts the archive. The application key is included only with --include-app-key. Without that key a stolen backup stays unreadable. |
datasync copies enc: values verbatim, so it never needs or decrypts the application key. verify checks row counts, object hashes, and that every documents.storage_name has a stored object. restore writes only into an empty database and never overwrites an existing object. Account deletion removes rows and blobs, but offline backups must be expired by the operator's own retention policy.
Before Alembic upgrades a populated SQLite database that is not at head, api/migrate.py copies it with the SQLite backup API into memory, serializes it, encrypts it with the application key, and writes before-migration-<timestamp>.sqlite.enc. These files also count as "existing encrypted data" for the no-replacement-key rule.
- Ciphertext at rest in both the database and the object store. A leaked bucket or database dump without the key reveals no clinical text.
- The key is held by the server process only; it never reaches the web or mobile clients.
- Rotating the key is not implemented. Changing
BABYCARE_FERNET_KEYmakes existing records unreadable.
api/crypto.py, api/storage.py, api/cloud.py (prepare_storage, install_handlers), api/main.py (upload, download), api/workflows.py (document_page, signed_access, signed_file), api/history_api.py (document_thumbnail), api/migrate.py, api/backup.py, api/datasync.py, scripts/cloud_backup.py
Security and Privacy Model · Medical Document Ingestion · Database and Data Model · Deployment Architecture · Stool Photo Pipeline
Overview
Platform
Identity and access
Medical records
- 10 Document Ingestion
- 11 OCR and Human Verification
- 13 Medicines and Administration
- 14 Vaccination Model
- 15 Labs, Growth, Longitudinal
- 16 Illness and Lifetime Health
Daily care
Output and sharing
Security
Engineering
- 26 Testing and Validation
- 27 Deployment and CI/CD
- 28 Technology Stack
- 29 Design Decisions
- 30 Limitations
- 31 Roadmap