Skip to main content

ADR-004: PII Encryption at the Service Layer

Status: Accepted | Date: 2026-03-13

Context

The Student model contains two PII fields subject to Ghana Data Protection Act 2012 (Act 843):

  • passportNumber -- government-issued identity document number
  • guardianPhone -- guardian contact phone number

RDS at-rest encryption (via KMS) protects against physical media theft but not against a compromised database connection. Application-layer field-level encryption provides a second layer: even with full DB read access, an attacker sees only ciphertext.

Decision

encryptField and decryptField from src/lib/encryption.ts are called explicitly in service functions -- before Prisma writes and after Prisma reads. AES-256-GCM encryption uses KMS-derived data keys (sa3-student-pii-key). Encryption is never performed in Prisma middleware.

Rationale

  1. Explicitness prevents silent gaps -- raw queries or Prisma version changes cannot bypass it.
  2. Testable -- service functions with encryption can be unit-tested with mock KMS.
  3. Auditable -- CloudTrail logs every KMS GenerateDataKey and Decrypt call.
  4. Separate KMS key -- sa3-student-pii-key key policy restricts decryption to App Runner and Lambda roles only.
  5. Base64 ciphertext fits the existing String Prisma type -- no schema change needed.

Consequences

Positive: Visible encryption in code, testable, CloudTrail audit trail, KMS key rotation handled by AWS, RDS credential compromise does not expose plaintext PII.

Negative: Engineers must remember to call encrypt/decrypt (mitigated by code review and Critical Rule 3). KMS API round-trip per field adds latency for bulk queries (mitigated by data key caching).