critical priority medium complexity infrastructure pending backend specialist Tier 5

Acceptance Criteria

A MaskedFieldResult type is defined with fields: maskedValue (String), errorCode (DecryptionErrorCode), and isDecrypted (bool = false)
A top-level decryptOrMask() function wraps the decryption result: on success it returns a DisplayFieldResult with isDecrypted=true and the plaintext; on any DecryptionError it returns a MaskedFieldResult with maskedValue='••••••••' and the corresponding error code
The masked placeholder string is a named constant (kMaskedFieldPlaceholder) to allow UI-layer theming without changing business logic
No partial plaintext bytes, no base64 fragments, and no raw cipher bytes are present in MaskedFieldResult under any code path
MaskedFieldResult.maskedValue is always exactly the constant placeholder — it is never derived from the ciphertext or a partial decryption attempt
The function handles all three DecryptionErrorCode values (INTEGRITY_VIOLATION, DECRYPTION_FAILED, INVALID_INPUT, KEY_UNAVAILABLE) and maps each to MaskedFieldResult without rethrowing
Network errors that prevented key retrieval (task-007) result in MaskedFieldResult with KEY_UNAVAILABLE code, not an unhandled exception
The DisplayFieldResult / MaskedFieldResult union can be pattern-matched by the UI layer to conditionally show a lock icon and tooltip
Accessibility: callers can use MaskedFieldResult.errorCode to provide a meaningful screen reader announcement (e.g. 'Field unavailable — decryption error') rather than announcing the bullet characters

Technical Requirements

frameworks
Flutter
Dart 3 sealed classes for result union
apis
field-encryption-utils decryption API from task-008
data models
DisplayFieldResult (plaintext: String, isDecrypted: bool = true)
MaskedFieldResult (maskedValue: String, errorCode: DecryptionErrorCode, isDecrypted: bool = false)
FieldDisplayResult sealed supertype (DisplayFieldResult | MaskedFieldResult)
performance requirements
decryptOrMask() adds zero perceptible latency — it is a synchronous wrapper over the async decrypt result
No additional heap allocations beyond constructing the result object
security requirements
MaskedFieldResult must not contain any derivative of the ciphertext or key material
The function must catch all exceptions from the decryption layer and convert them to MaskedFieldResult — no raw exceptions may propagate to the UI
Placeholder string must not hint at actual field length (fixed-width ••••••••, not length-matched)

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Keep decryptOrMask() a thin adapter — its only responsibility is mapping the DecryptionResult sealed type to FieldDisplayResult. Do not duplicate any crypto logic here. Use a try/catch around the call to the task-008 decryption isolate to catch unexpected errors and degrade to MaskedFieldResult(errorCode: KEY_UNAVAILABLE). Define kMaskedFieldPlaceholder as a const String in a shared constants file so the UI and tests share the same value.

The fixed-width placeholder (not length-matched) is intentional security design — length-matched masks leak metadata about field content. Document this decision in a code comment so future developers do not 'fix' it. The UI layer (not this utility) is responsible for showing tooltips or icons; this layer only provides the errorCode signal.

Testing Requirements

Unit tests with flutter_test: (1) verify decryptOrMask() returns DisplayFieldResult on successful decryption from task-008, (2) verify each DecryptionErrorCode maps to MaskedFieldResult with correct errorCode, (3) assert MaskedFieldResult.maskedValue equals kMaskedFieldPlaceholder for all error paths, (4) assert no substring of the original ciphertext appears in MaskedFieldResult, (5) verify KEY_UNAVAILABLE is returned when key store throws a network exception. Use mock of the decrypt function from task-008. Verify the sealed type pattern-match is exhaustive by compiling switch statements — Dart will error if a case is missing.

Component
Field Encryption Utilities
infrastructure high
Epic Risks (3)
high impact medium prob security

Blindeforbundet's encryption key retrieval mechanism may not be finalised at implementation time, or session key availability via Supabase RLS may be inconsistent, causing decryption failures that expose masked placeholders to users and degrade the experience.

Mitigation & Contingency

Mitigation: Agree with Blindeforbundet on key storage and retrieval contract before implementation starts. Prototype key retrieval in a spike against the staging Supabase instance and validate the full decrypt/verify cycle with real test data before committing to the implementation.

Contingency: Implement a fallback that shows a 'field temporarily unavailable' state with a retry affordance. Log decryption failures server-side for audit. Escalate to Blindeforbundet stakeholders to unblock key management before the service tier epic begins.

medium impact medium prob technical

NHF contacts may belong to up to 5 chapters, each governed by separate RLS policies. A coordinator's chapter scope may not cover all affiliations, causing partial profile reads or silent data omissions that are difficult to detect in tests.

Mitigation & Contingency

Mitigation: Map all RLS policy combinations for multi-chapter contacts early. Write integration tests that create contacts with 5 affiliations and query them from coordinators with varying chapter scopes. Use Supabase's RLS test utilities to verify row visibility per role.

Contingency: Add an explicit 'affiliation partially visible' state in the repository response model so the UI can communicate scope limitations to the coordinator rather than silently showing incomplete data.

low impact medium prob scope

Organisation-specific validation rules (e.g., NHF chapter limit, Blindeforbundet encrypted field edit flow) may expand in scope during implementation as edge cases are discovered, causing the validator to grow beyond the planned complexity.

Mitigation & Contingency

Mitigation: Define the complete validation rule set with product and org stakeholders before coding begins. Document each rule with its source organisation and acceptance test. Use a rule registry pattern so new rules can be added without modifying core validator logic.

Contingency: Timebox validator enhancements to 2 hours per additional rule. Defer non-blocking rules to a follow-on maintenance task rather than blocking the epic delivery.