OverviewSignetSemaForeCuriousLee
Assurance record Published engineering decision

Explicit security UI state

The accepted engineering decision governing security-sensitive UI and vault state in Attomus Signet.

AUTH-ADR-0031 — Explicit Domain State for Security-Sensitive UI

Date: 2026-05-16 Status: Accepted Decision authority: Attomus Security Architecture Triggered by: Security post-mortem (stale OTP display after session lock; orphaned secret state)


Context

Two separate bugs — one in Android, one discovered during iOS CREST review — shared the same root cause: nullable or optional types were used to represent multiple distinct security-relevant states simultaneously, and display/action code could not reliably distinguish between them.

Android (stale OTP, a9cb76a7): AccountCard derived displayedCode from codeState?.code. A null codeState meant either “session locked” or “account provisioning failed” — two very different states. When the session lock path cleared sessionUnlocked but a background timer had not yet cleared codeStateByAccountId, the card displayed and allowed copying of cached OTP codes in a nominally locked state.

iOS (vault boundary regression, c571c88a): SecretVault.exportForBackup() returned a BackupExportMaterial struct with public var plaintext: Data. The call site in BackupExportView treated the presence of this struct as confirmation that export was authorised and safe to hand to the encryption layer. In reality the plaintext had already left the vault boundary — the struct itself was the security failure, not a missing nil check.

In both cases, nullable or loosely-typed values were used as a proxy for security state, and code downstream of that proxy made unsafe assumptions.


Decision

Security-sensitive UI and domain code must use explicit sealed state models, not nullable or boolean proxies.

Rule 1 — Account display state is an explicit enum

The authoritative state type for an account’s code display is a sealed class / enum with exactly three cases:

Locked                          // Session is not active; no code may be shown or copied
Unavailable(reason: String)     // Session is active but key retrieval failed
Available(codeState: CodeState) // Session is active and code is ready

Locked and Unavailable are separate states. Displaying “Unavailable” when the session is locked is a security failure, not just a UX error.

Display and copy actions are gated on pattern-matching this enum at the final render point, not on upstream boolean flags or null checks alone.

Rule 2 — Vault outputs are sealed; plaintext never crosses the vault boundary

Functions inside SecretVault (iOS) / AndroidSecretVault (Android) that produce backup export data must return only encrypted bytes, never plaintext structs. Any struct that contains plaintext: Data or secretBytes: ByteArray as a public field is a vault boundary violation regardless of whether the caller intends to encrypt it next.

The correct pattern is closure-based: the vault calls the encryption closure while holding its state lock, and returns only the encrypted result.

Rule 3 — Clearing state is a defence-in-depth measure, not the primary gate

Actively clearing codeStateByAccountId / equivalent on session lock transitions is correct and should be retained. However, display and copy code must also gate at the final render/copy point independently of whether the clearing has run. The assumption is that stale state may always exist; the gate must hold regardless.


Consequences

  • AccountCard (Android) and equivalent iOS code must be updated to use the three-case state enum rather than deriving state from codeState?.code and sessionUnlocked separately.
  • BackupExportMaterial (iOS) is deleted. SecretVault.exportForBackup accepts an encryption closure and returns only BackupExportPackage (encrypted bytes + metadata).
  • All new security-sensitive UI state in both apps follows the explicit enum pattern before merge.
  • Code review checklist: any ?. or == null / ?? operating on security-sensitive display state is a review flag requiring justification.

References

  • Android security-review finding a9cb76a7 — stale OTP codes after session lock
  • iOS security-review finding c571c88a — BackupExportMaterial vault boundary regression