critical priority high complexity infrastructure pending backend specialist Tier 0

Acceptance Criteria

encrypt(String plaintext, String keyId) returns a Future<EncryptedFieldResult> containing the ciphertext, IV, and key version identifier
decrypt(EncryptedFieldResult ciphertext, String keyId) returns Future<Either<DecryptionFailure, String>> with the plaintext value
AES-256-GCM is used with a random 96-bit IV generated per encryption call — never reuse IVs
Envelope encryption pattern is implemented: data encrypted with a DEK (data encryption key), DEK encrypted by Supabase Vault master key
On successful decryption, a read-receipt record is written to Supabase (field_name, contact_id, decrypted_by_user_id, decrypted_at) via a non-blocking fire-and-forget call
Read receipt failures do not block or throw — they are logged silently and the decrypted value is still returned
Key rotation is handled by storing the key_version alongside encrypted data — old versions can still decrypt existing records while new records use the latest key
Attempting to decrypt with an unknown or revoked key_version returns KeyVersionNotFoundFailure
The utility exposes no raw key material in any public API — keys are referenced by ID only
All encryption/decryption operations complete within 200ms under normal conditions
The utility is tested against a Supabase Vault staging environment confirming end-to-end encryption/decryption round-trip

Technical Requirements

frameworks
Flutter
pointycastle (AES-GCM)
Riverpod
apis
Supabase Vault API
Supabase RPC (for write-protected read receipt inserts)
Supabase PostgREST
data models
EncryptedFieldResult
ReadReceiptRecord
DecryptionFailure
KeyVersionMetadata
performance requirements
Encryption/decryption under 200ms including Vault key fetch
DEK should be cached in secure memory for the session duration to avoid repeated Vault round-trips
Read receipt write must be non-blocking (fire-and-forget with timeout)
security requirements
Never store plaintext sensitive values in SharedPreferences, local DB, or logs
DEK cache must be cleared on app background/lock
GCM authentication tag must be verified on decryption — reject unauthenticated ciphertexts
Supabase Vault access must be scoped to Blindeforbundet org only via RLS
Key IDs must not be guessable — use Vault-assigned UUIDs only
Comply with GDPR Article 25 (data protection by design) and Personopplysningsloven

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use the envelope encryption pattern: generate a random DEK per contact record (or per field group), encrypt the DEK with the Vault master key (Vault stores and manages the master key — you never see it), store the encrypted DEK alongside the ciphertext. On decryption, send the encrypted DEK to Vault for unwrapping, then use the unwrapped DEK locally for AES-GCM decryption. Use pointycastle's GCMBlockCipher for AES-256-GCM. Cache the unwrapped DEK in a private in-memory map keyed by contact_id, invalidated on Flutter AppLifecycleState.paused.

The read-receipt hook should be a separate FieldReadReceiptService that is called from the decryption utility via a callback or post-decryption hook — keep it decoupled so it can be mocked independently. For key rotation, add a key_version field to the encrypted payload DTO; the decrypt path reads this version and fetches the appropriate Vault key by version ID. Consider a Supabase RPC function for read-receipt writes to enforce server-side validation (prevents clients from spoofing user IDs).

Testing Requirements

Unit tests for the AES-GCM encrypt/decrypt cycle using pointycastle directly (no Vault dependency). Integration tests against Supabase Vault staging: verify encrypt → store → retrieve → decrypt round-trip, verify tampered ciphertext is rejected (GCM tag failure), verify key rotation (encrypt with v1, rotate to v2, verify v1 ciphertext still decrypts, new encryptions use v2). Test read-receipt writes: mock Supabase client and verify fire-and-forget call is made after successful decryption. Test that read-receipt failure does NOT throw.

Test DEK cache invalidation on simulated app background event. Run all sensitive data tests in an isolated test environment with no real PII.

Component
Contact Detail Screen
ui medium
Epic Risks (2)
low impact medium prob dependency

The Peer Mentor Profile tab on the contact detail screen depends on the peer-mentor-detail-screen-widget being delivered by the separate Peer Mentor Detail feature. If that feature is delayed, the navigation affordance will be present but lead to a stub screen, which may confuse coordinators in the TestFlight pilot.

Mitigation & Contingency

Mitigation: Implement the peer mentor tab with a feature flag guard. When the Peer Mentor Detail feature is incomplete, the flag disables the tab. Coordinate delivery timelines with the team responsible for Peer Mentor Detail to align TestFlight releases.

Contingency: If the Peer Mentor Detail feature is significantly delayed, ship the contact detail screen without the peer mentor tab in the first TestFlight build and add it as an incremental update once the dependent screen is ready.

medium impact medium prob technical

The contact detail screen must adapt its layout significantly based on organisation context: NHF shows affiliation chips, Blindeforbundet shows encrypted fields and assignment status, standard contacts show neither. Managing this conditional rendering without introducing bugs in each variant is complex and increases the risk of organisation-specific regressions.

Mitigation & Contingency

Mitigation: Define a ContactDetailViewModel that resolves all org-specific flags (showEncryptedFields, showAssignmentStatus, showMultiChapterChips) from the organisation config before the widget tree renders. Widget tests must cover all three organisation variants as separate test cases to catch regressions.

Contingency: If conditional rendering logic grows unwieldy, refactor into separate composable section widgets (ProfileHeaderSection, AffiliationSection, EncryptedFieldsSection) that are conditionally included by the parent screen, isolating org-specific logic to individual components.