Commit Graph

2 Commits

Author SHA1 Message Date
Víctor Falcón 9e90db293c Purge check uses name_iv source of truth, not the stale encrypted flag
Reviewer finding (architecture, major): PurgeResidualEncryptionArtifactsJob
decided 'no encrypted accounts remain' from the accounts.encrypted flag, but the
codebase already treats that flag as unreliable — the align_accounts_encrypted_
flag_with_plaintext_names migration exists precisely because stale flags occur,
and FindsUsersWithLegacyEncryption (used by the encryption:* commands) keys off
the per-row name_iv column instead. Because this purge is destructive (it drops
the salt and EncryptedMessage, the only key material for anything still
encrypted at rest), an account with an encrypted name (name_iv set) but a stale
encrypted=false flag could have its key material destroyed.

Aligns the job's residual-data check with that source of truth: accounts are
residual when name_iv is not null, matching the transactions check (which
already used description_iv/notes_iv). The job's guard now mirrors
FindsUsersWithLegacyEncryption exactly, so the destructive purge only runs when
no *_iv data remains. The loose encrypted-flag gate in share() is kept only as a
cheap dispatch filter; the job re-verifies with the safe predicate before
touching anything.

Tests: replaces the flag-based 'keeps salt' case with a name_iv-based one and
adds a case proving a stale encrypted=false flag with an encrypted name still
blocks the purge.
2026-07-04 20:33:10 +02:00
Víctor Falcón b7ff4e074d Move residual-encryption cleanup out of Inertia share() into a queued job
HandleInertiaRequests::share() is an Inertia shared-data provider and should be
read-only, but it ran a DELETE + UPDATE against the user on every non-API web
GET to purge the leftover encryption salt and EncryptedMessage once a user no
longer had any encrypted accounts or transactions. Writing to the database
while resolving render props is a side effect in the wrong place.

The existing encryption commands do not cover this case: both
NotifyEncryptedDataRemovalCommand and DeleteEncryptedDataAccountsCommand target
users who *still* have encrypted data, whereas this cleanup finalizes users who
have *finished* decrypting. To preserve that eventual-cleanup semantics without
writing during the render, the work now goes to a new
PurgeResidualEncryptionArtifactsJob dispatched from share(). The job re-checks
the condition on execution and is idempotent, so repeat dispatches are no-ops.

The two ->exists() checks stay because they still feed the hasEncryptedAccounts
and hasEncryptedTransactions props.

Tests: InertiaSharedDataTest asserts a web GET no longer mutates the user inline
and instead queues the job (and does not queue it when there is no salt); a new
PurgeResidualEncryptionArtifactsJobTest covers the clear, the two 'still has
encrypted data' guards, and the null-salt no-op.
2026-07-04 20:18:49 +02:00