diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index fe94b30..772ef86 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -60,7 +60,7 @@ tests/ - Checks the current user's permission (`view decrypted sensitive fields`). - **Authorized**: strips `enc:v1:` prefix and decrypts the value. - **Unauthorized**: replaces the value with the mask string (default `••••••`). -4. If decryption fails (e.g. key rotation), returns raw ciphertext, logs a warning, and dispatches a CP error toast to the current user (HTTP context only; deduplicated to once per form per hour via `Cache::add`). +4. If decryption fails (e.g. key rotation), returns raw ciphertext and logs a warning. ### Addon Settings diff --git a/docs/PLAN.md b/docs/PLAN.md index 0692e3a..dd9f57c 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -180,8 +180,7 @@ Edition is detected via the **Statamic Editions API**: `Addon::edition()` reads 5. isEncrypted detects prefix 6. mask returns configured value 7. decrypt returns non-encrypted as-is -8. Failed decrypt does not dispatch toast in console context -9. Failed decrypt without context does not dispatch toast in console context +8. Failed decrypt returns raw value and logs warning (no side-effects) ### Feature (SensitiveFieldsTest, 12 tests) 1. Sensitive field stored encrypted @@ -244,12 +243,3 @@ Larger teams need per-form control (e.g. HR form vs. contact form handled by dif - Both `DecryptingSubmissionRepository` and `DecryptingSubmissionQueryBuilder` check global then per-form permission via `isAuthorizedForForm(string $formHandle)`. --- - -### [FREE/PRO] CP notification on decrypt failure — Implemented - -Decryption failures are now surfaced in the CP as an error toast in addition to the existing `Log::warning`. - -- `FieldEncryptor::decrypt()` accepts an optional `string $context` parameter (form handle) for deduplication. -- In HTTP context, `Cache::add('sffields.decrypt_failure_notified.{context}', true, 3600)` is used as an atomic set-if-not-exists guard — at most one toast per form per hour. -- `Toast::error()` is skipped entirely when `app()->runningInConsole()` is true (commands, queue workers). -- `DecryptingSubmissionRepository` passes the form handle as `$context` when calling `decrypt()`. diff --git a/lang/en/messages.php b/lang/en/messages.php index b7e9f60..873e101 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -12,8 +12,6 @@ 'permission_form_label' => 'View Decrypted Sensitive Fields', 'permission_form_description' => 'Allow viewing decrypted values of sensitive fields in this form only', - 'decrypt_failure_toast' => 'One or more sensitive field values could not be decrypted. Your APP_KEY may have changed. Use sensitive-fields:rekey (Pro) to recover.', - 'settings_enabled_display' => 'Enabled', 'settings_enabled_instructions' => 'Enable or disable field encryption.', 'settings_mask_display' => 'Mask String', diff --git a/src/Encryption/FieldEncryptor.php b/src/Encryption/FieldEncryptor.php index 26c9b42..c9cddfa 100644 --- a/src/Encryption/FieldEncryptor.php +++ b/src/Encryption/FieldEncryptor.php @@ -4,12 +4,9 @@ namespace Isapp\SensitiveFormFields\Encryption; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Log; use Statamic\Addons\Addon; -use Statamic\Facades\CP\Toast; -use Statamic\Statamic; class FieldEncryptor { @@ -17,9 +14,6 @@ class FieldEncryptor // Allows detecting already-encrypted values and future format migrations. protected const PREFIX = 'enc:v1:'; - // Suppress repeated CP toasts for the same form within this window (seconds). - protected const NOTIFY_TTL = 3600; - public function __construct( protected Addon $addon, ) {} @@ -37,12 +31,7 @@ public function encrypt(string $value): string // Returns the value unchanged (with a log warning) on decryption failure, // e.g. after an APP_KEY rotation. Callers should treat the raw ciphertext // as an opaque fallback rather than a fatal error. - // - // $context is an optional form handle used to deduplicate CP toasts so that - // bulk failures (e.g. many submissions after an APP_KEY rotation) produce at - // most one toast per form per hour. Pass the form handle from repository callers; - // omit from CLI callers (toast is suppressed in console context anyway). - public function decrypt(string $value, string $context = ''): string + public function decrypt(string $value): string { if (! $this->isEncrypted($value)) { return $value; @@ -53,29 +42,6 @@ public function decrypt(string $value, string $context = ''): string } catch (\Throwable $e) { Log::warning('Failed to decrypt sensitive field value: ' . $e->getMessage()); - // Only dispatch CP toasts on actual CP requests — not API or frontend - // requests, which could consume the dedup window before a CP user visits. - // Cache::add() is atomic set-if-not-exists; the key is rolled back via - // Cache::forget() when toast delivery fails so the dedup window is not - // consumed without a toast being shown. Wrapped in try/catch so that - // cache or session failures never break the graceful fallback. - try { - if (Statamic::isCpRoute()) { - $cacheKey = 'sffields.decrypt_failure_notified.' . ($context ?: 'unknown'); - if (Cache::add($cacheKey, true, self::NOTIFY_TTL)) { - try { - Toast::error(__('statamic-sensitive-form-fields::messages.decrypt_failure_toast')); - } catch (\Throwable) { - // Roll back the dedup key so a future request with an active - // CP session can still deliver the toast. - Cache::forget($cacheKey); - } - } - } - } catch (\Throwable) { - // Cache failure must never convert a recoverable decrypt error into a fatal one. - } - return $value; } } diff --git a/src/Repositories/DecryptingSubmissionQueryBuilder.php b/src/Repositories/DecryptingSubmissionQueryBuilder.php index 0dc9e50..c2d4788 100644 --- a/src/Repositories/DecryptingSubmissionQueryBuilder.php +++ b/src/Repositories/DecryptingSubmissionQueryBuilder.php @@ -81,7 +81,7 @@ protected function decryptSubmission(Submission $submission): void } if ($canDecrypt) { - $submission->set($handle, $this->encryptor->decrypt($value, $submission->form()->handle())); + $submission->set($handle, $this->encryptor->decrypt($value)); } else { $submission->set($handle, $this->encryptor->mask()); } diff --git a/src/Repositories/DecryptingSubmissionRepository.php b/src/Repositories/DecryptingSubmissionRepository.php index 48d2fa1..28eadd7 100644 --- a/src/Repositories/DecryptingSubmissionRepository.php +++ b/src/Repositories/DecryptingSubmissionRepository.php @@ -104,7 +104,7 @@ protected function decryptSubmission(Submission $submission): void } if ($canDecrypt) { - $submission->set($handle, $this->encryptor->decrypt($value, $submission->form()->handle())); + $submission->set($handle, $this->encryptor->decrypt($value)); } else { $submission->set($handle, $this->encryptor->mask()); } diff --git a/tests/Unit/FieldEncryptorTest.php b/tests/Unit/FieldEncryptorTest.php index a3c63a4..217fdb7 100644 --- a/tests/Unit/FieldEncryptorTest.php +++ b/tests/Unit/FieldEncryptorTest.php @@ -66,25 +66,4 @@ public function test_decrypt_returns_non_encrypted_value_as_is() { $this->assertSame('plaintext', $this->encryptor->decrypt('plaintext')); } - - public function test_failed_decrypt_does_not_dispatch_toast_in_console_context() - { - // The test suite runs in console context (app()->runningInConsole() === true), - // so the Toast must never be called — verified by spying on the facade. - \Statamic\Facades\CP\Toast::spy(); - - $this->encryptor->decrypt('enc:v1:corrupted-ciphertext', 'contact'); - - \Statamic\Facades\CP\Toast::shouldNotHaveReceived('error'); - } - - public function test_failed_decrypt_without_context_does_not_dispatch_toast_in_console_context() - { - \Statamic\Facades\CP\Toast::spy(); - - // Backward-compatible: context parameter is optional. - $this->encryptor->decrypt('enc:v1:corrupted-ciphertext'); - - \Statamic\Facades\CP\Toast::shouldNotHaveReceived('error'); - } }