$npx -y skills add Lonsdale201/wp-agent-skills --skill bd-securityApply better-data's security discipline when touching Secret, EncryptionEngine, #[Sensitive], #[Encrypted], MetaKeyRegistry::register, RequestSource guards, or user_pass handling. Loud-over-silent — missing key throws, tampered ciphertext throws, unknown strict-whitelist field th
| 1 | # better-data: Security-sensitive changes |
| 2 | |
| 3 | For library maintainers touching anything in better-data's security perimeter — `Secret`, `EncryptionEngine`, `#[Sensitive]`, `#[Encrypted]`, `RequestSource` guards, password handling. Mistakes in this perimeter aren't bugs that show up in tests; they're regressions that ship plaintext to disk or leak credentials in logs. |
| 4 | |
| 5 | ## Misconception this skill corrects |
| 6 | |
| 7 | > "I'll add a debug-mode that logs the encrypted value's plaintext when developer mode is on — it's only for development." |
| 8 | |
| 9 | Don't. The security-feature-with-bypass is the worst outcome — caller assumes redaction is universal, log infrastructure picks up the "debug" path in production by accident, plaintext lands in CloudWatch / Sentry / wp-debug.log forever. The discipline is no-bypass: `Secret`'s `__toString` returns `'***'` ([src/Secret.php:84-87](Secret.php)), `jsonSerialize` returns `'***'` ([src/Secret.php:89-92](Secret.php)), `__debugInfo` (controls `var_dump` / `print_r`) returns `['value' => '***']` ([src/Secret.php:99-103](Secret.php)), `__serialize` THROWS `SecretSerializationException` ([src/Secret.php:105-109](Secret.php)). |
| 10 | |
| 11 | The throwing `__serialize` is deliberate — a caller serialized a `Secret` has already made a security-relevant mistake. Relaxing it to redact instead would silently let the bug ship. The exception forces them to either `->reveal()` explicitly (audit point) or rethink the flow. |
| 12 | |
| 13 | Other AI-prone misconceptions: |
| 14 | |
| 15 | - "I'll cache the encryption key in a static property to avoid re-reading the constant on every call." Wrong — `EncryptionEngine` deliberately re-reads on every call ([src/Encryption/EncryptionEngine.php:53-54](EncryptionEngine.php)) so key rotation via `BETTER_DATA_ENCRYPTION_KEY_PREVIOUS` actually works. A process-long cache defeats rotation. |
| 16 | - "`==` and `===` are fine for comparing two `Secret`s; the constant-time stuff is paranoia." Wrong — string compare is timing-dependent and leaks length / first-byte equality through repeated probing. `Secret::equals` uses `hash_equals` ([src/Secret.php:78-82](Secret.php)). Always. |
| 17 | - "If decryption fails, return null and the caller falls back to the default." Wrong — silent failure on decrypt is worse than an exception. A caller that gets `null` instead of the expected secret may treat it as "user never set a key" and proceed. `EncryptionEngine::decrypt` throws `DecryptionFailedException` ([src/Encryption/EncryptionEngine.php:109,130](EncryptionEngine.php)). |
| 18 | |
| 19 | ## When to use this skill |
| 20 | |
| 21 | Trigger when ANY of the following is true: |
| 22 | |
| 23 | - The diff touches `src/Secret.php`, `src/Encryption/EncryptionEngine.php`, `src/Attribute/Encrypted.php`, `src/Attribute/Sensitive.php`. |
| 24 | - The diff touches `MetaKeyRegistry::register`, route-owned-field handling, or `RequestSource` guards. |
| 25 | - New code introduces a `Secret` typed property OR `#[Encrypted]` attribute. |
| 26 | - New code calls `EncryptionEngine::encrypt` / `decrypt` / `looksEncrypted`. |
| 27 | - New code introduces password handling (`user_pass`, hash storage, comparison). |
| 28 | - Reviewing a PR that adds a "debug mode" / "verbose log" / "for development" flag near sensitive material. |
| 29 | |
| 30 | ## Workflow |
| 31 | |
| 32 | ### 1. Threat model first (in the PR description or a code comment) |
| 33 | |
| 34 | Write down explicitly: |
| 35 | |
| 36 | - **What is this change preventing?** (e.g. "API tokens stored as plaintext in `wp_options`.") |
| 37 | - **What remains un-prevented?** (e.g. "Plaintext key in PHP memory between hydration and use; an attacker with PHP memory access can still read it.") |
| 38 | - **What's the threat model?** (e.g. "Database compromise, log file exposure. NOT defending against in-memory attackers — that requires HSM-grade key management.") |
| 39 | |
| 40 | The goal: a future contributor reads the comment and knows whether their proposed change preserves or breaks the model. |
| 41 | |
| 42 | ### |