Files
HealthLog/prisma/migrations/0208_v125_med_notes_encryption/migration.sql
T
Marc-André Bombeck 5ea17398bc feat(security): encrypt medication free-text notes at rest + password-change MFA step-up
Encrypt MedicationSideEffect.notes, MedicationDoseChange.note and
MedicationInventoryItem.notes with AES-256-GCM, closing the last plaintext
PHI columns left after the v1.23 note-encryption rollout. Add the three
*Encrypted Bytes columns (migration 0208), register them in the encrypted-
column rotation set, route every read/write through the note cipher, and add
a transactional fail-closed boot-time backfill for legacy rows. Plaintext
columns drop in a later release once the backfill reports zero remaining rows.

Require a fresh second factor on password change for MFA-enrolled accounts,
matching the existing step-up on MFA-disable and recovery-code regen. Redact
free-text note values from the Zod validation excerpt that can reach a wide
event.
2026-06-28 03:05:10 +02:00

36 lines
1.9 KiB
SQL

-- v1.25 — encrypt medication free-text notes at rest.
--
-- Adds the AES-256-GCM ciphertext columns for the three medication
-- free-text fields that were still plaintext: the side-effect note
-- (`medication_side_effects.notes`), the dose-change titration note
-- (`medication_dose_changes.note`), and the inventory-item note
-- (`medication_inventory_items.notes`). These were the last plaintext
-- PHI columns left after the v1.23 note-encryption rollout.
--
-- STORAGE CONVENTION: Bytes (`bytea`), matching the dominant free-text
-- encrypted-note convention already in the schema (mood_entries.note_
-- encrypted, measurements.notes_encrypted, illness_day_logs.note_encrypted,
-- labs.note_encrypted, coach_*.{...}_encrypted) — the `encrypt()`
-- ciphertext string stored UTF-8 as `bytea`, the shape the rotation
-- script's `rotateBytesColumn` already covers. All three columns are
-- registered in ENCRYPTED_COLUMNS + the rotation script in the same change.
--
-- This migration is ADDITIVE ONLY. The legacy plaintext columns
-- (`medication_side_effects.notes`, `medication_dose_changes.note`,
-- `medication_inventory_items.notes`) are intentionally NOT dropped here:
-- an idempotent, transactional, fail-closed boot-time job encrypts the
-- existing rows (plaintext -> ciphertext, then nulls the plaintext) so the
-- read path can fall back to plaintext for not-yet-migrated rows. Dropping
-- the plaintext columns is a deliberate FOLLOW-UP release, once the backfill
-- reports zero remaining plaintext-but-no-ciphertext rows — the same
-- boundary the encryption-key-rotation playbook uses before dropping a
-- legacy key.
ALTER TABLE "medication_side_effects"
ADD COLUMN IF NOT EXISTS "notes_encrypted" BYTEA;
ALTER TABLE "medication_dose_changes"
ADD COLUMN IF NOT EXISTS "note_encrypted" BYTEA;
ALTER TABLE "medication_inventory_items"
ADD COLUMN IF NOT EXISTS "notes_encrypted" BYTEA;