cipher-guard
Skill0 starsChanged 4 months agoMITView on GitHub
Audits whether application-level field encryption stays correct over the data's whole lifetime, not just at the moment it's written — a deterministic/searchable scheme used for equality lookups that silently stops enforcing uniqueness once a field has mixed old/new encryption schemes, a "we rotated the key" claim that only re-wrapped the data-encryption key (so a believed-compromised key can still decrypt every stored row), and an envelope-encryption key policy that's grown wider than the one application role it was scoped for. Use when reviewing code that encrypts specific columns/fields (PII, payment details, health data), when a key rotation or key-compromise response is being planned, or when asked "is our encrypted-field uniqueness check actually working", "did rotating the key actually protect us", "does this KMS key policy still match who's supposed to use it", or "can we query this encrypted column safely".
In this repository46 other files it gives its agents.
- atomic-ioSkill
- backfill-pilotSkill
- backpressure-guardSkill
- blast-guardSkill
- changelogSkill
- clipboard-guardSkill
- clock-sweepSkill
- codebase-tourSkill
- consent-guardSkill
- devlogSkill
- dormancy-guardSkill
- env-detectiveSkill
- erasure-guardSkill
- export-guardSkill
- failover-guardSkill
- git-rescueSkill
- import-guardSkill
- job-wardenSkill
- migration-guardSkill
- overwrite-guardSkill
- pagination-guardSkill
- portability-auditSkill
- pref-guardSkill
- proration-guardSkill
- push-guardSkill
- quote-guardSkill
- reaccept-guardSkill
- readme-forgeSkill
- recall-guardSkill
- residency-guardSkill
- restore-guardSkill
- resync-guardSkill
- rollout-guardSkill
- rotation-guardSkill
- seat-guardSkill
- secret-spillSkill
- security-sweepSkill
- selectall-guardSkill
- ship-itSkill
- skew-checkSkill
- stale-guardSkill
- storm-guardSkill
- sunset-guardSkill
- throttle-guardSkill
- tombstoneSkill
- trail-guardSkill
The file
Files
cipher-guard
Field-level encryption gets treated as a done-once decision: pick AES-256-GCM or a vetted library, encrypt the column, ship it. But three separate correctness properties have to keep holding for the entire lifetime of the data, not just at write time, and each one fails in a way that still looks encrypted on casual inspection — the ciphertext is there, the library is real, nothing throws. First: if a field needs equality lookups (login by email, dedupe by SSN), it has to use a deterministic or blind-indexed scheme instead of random AEAD, and that choice has to stay honored everywhere the field is compared, including across a scheme migration — a uniqueness: true validation that silently stops working the moment old and new ciphertext schemes coexist isn't a hypothetical, it's a filed Rails bug. Second: "we rotated the key" has two different meanings with wildly different guarantees — re-wrapping the data-encryption key under a new master key (fast, touches only key metadata) versus re-encrypting the actual rows under a fresh data-encryption key (slow, touches everything) — and only one of those actually helps if the data-encryption key itself is the thing believed compromised. Third: envelope encryption's whole security model rests on the KMS/HSM key's access policy matching exactly the one application role meant to use it, and that policy drifts the same way any permission does — a debug grant added during an incident and never removed, a migration job that got Decrypt when it only needed Encrypt, a wildcard principal nobody meant to leave in.
None of this is "pick better crypto primitives" — assume AES-256-GCM, a real KMS, and competent algorithm choices are already in place. This skill audits whether the lifecycle around that encryption — query semantics, rotation procedure, key-access scope — is still doing what whoever designed it believed it does.
Step 1: find every field/column with application-level encryption
Grep for the encryption library's markers — encrypts/attr_encrypted (Rails), a KMS GenerateDataKey/Decrypt call wrapping a column, a custom EncryptedType/EncryptedField in an ORM, pgcrypto's pgp_sym_encrypt, a field-level client-side-encryption config in a Mongo/DynamoDB SDK. Note for each: what the field holds, whether anything ever queries it for equality or uniqueness (a login lookup, a validates_uniqueness_of, a WHERE col = ?, a dedupe job), and which key protects it (a static app-held key, an envelope-encrypted DEK, a KMS-direct call per field). Exclude whole-disk/TDE encryption and TLS-in-transit — those are infrastructure-level and out of scope here; this is about fields the application code itself encrypts and decides how to query.
Step 2: deterministic/searchable encryption vs. equality-query correctness
For every field found in step 1 that's compared for equality or uniqueness anywhere in the code:
- Does it actually need to be queryable, and is the scheme deliberate? Deterministic encryption (same plaintext → same ciphertext) and blind indexes both leak equality to anyone who can read the ciphertext column — two rows with the same plaintext are visibly linked even without decrypting either. That's an acceptable, necessary tradeoff for a field genuinely queried by equality (an email login lookup) and a needless one for a field encrypted deterministically by a library default when nothing ever queries it — check which case this is, not just whether the library call includes
deterministic: true. - Does the uniqueness/equality check survive a scheme migration? This is a real, filed bug, not a theoretical edge case: Rails' own Active Record Encryption has
rails/rails#51553("Validation Does Not Work with Multiple Encryption Scheme") — a column declaredencrypts :field, deterministic: true, previous: { deterministic: false }(the documented way to migrate an existing column onto deterministic encryption) silently stops enforcingvalidates :field, uniqueness: truefor any row still holding old-scheme ciphertext, because — per the framework's own maintainer response — "the uniqueness validator cannot issue a DB query to check for uniqueness" against ciphertext encrypted under the previous, non-deterministic scheme. The result: duplicate values get created for exactly the rows a migration-in-progress is most likely to still have. If this codebase has ever changed an encrypted field's determinism setting, added aprevious:/legacy-key fallback, or is mid-rotation on a field with a uniqueness constraint, check whether the validation (or the equivalent app-level uniqueness check) actually still fires against old-scheme rows, or only appears to because the test suite only exercises rows written after the migration. - Does a DB-level
UNIQUEconstraint on the column actually enforce anything? AUNIQUEindex on a column encrypted with random/non-deterministic AEAD can never catch a real duplicate plaintext — every encryption of the same value produces different ciphertext, so the constraint only ever collides by astronomical accident. If aUNIQUEconstraint exists on such a column, it's decorative; the real uniqueness guarantee (if any) has to live in a blind-index/deterministic companion column, and that companion column is the one that needs the step above applied to it, not the encrypted column itself.
Step 3: what "we rotated the key" actually did
Find the rotation mechanism — a scheduled KMS GenerateDataKey/re-wrap job, a documented incident-response runbook, an "automatic annual rotation" claim, a manual script. Then determine which of two operations it performs, because they have different costs and protect against different threats:
- Re-wrap (KEK rotation): a new master/key-encrypting key wraps the existing data-encryption key. Cheap — touches only key metadata, not any encrypted row — and it's what cloud KMS "automatic key rotation" features do by default (AWS KMS rotates the backing key material behind a CMK; the wrapped DEKs on disk are unchanged by that rotation). This protects against the master key being compromised. It does nothing if the data-encryption key itself, or the plaintext it protects, is what's believed exposed — the same filed design split shows up verbatim in
pgexperts/chisel#140, which names "credential rotation" (re-wrap, O(1), "touches only the superblock") as a different operation from "bulk DEK rotation" (full re-encryption, O(total_pages), "must rewrite the entire database file"), and states plainly that the bulk path is "only needed when the DEK itself is believed compromised — e.g., a process memory dump exposed the in-session DEK." - Re-encrypt (DEK rotation): every row actually gets decrypted under the old DEK and re-encrypted under a new one. This is the only one of the two that helps after a DEK-level compromise, and it's also the expensive, easy-to-skip one — check whether it's a real, runnable procedure (batched, resumable, able to dual-read old-and-new-key ciphertext during the migration window so reads don't break mid-rotation) or just a sentence in a security policy doc ("keys are rotated annually") with no corresponding code path that actually touches existing rows.
If an incident response plan, a compliance doc, or a comment claims a key was rotated in response to a suspected leak, confirm which operation it describes — a plan that re-wraps and declares the incident closed hasn't actually invalidated the exposed key material's ability to decrypt the data it already protects.
Step 4: does the key's access policy still match the one role meant to use it
For each KMS/HSM key or envelope-encryption master key found in step 1, read its actual policy/grants (Terraform/CloudFormation key policy resource, console-exported JSON, IAM policy attached to the role) rather than trusting a comment about intended scope:
- Wildcard or over-broad principals. AWS's own KMS IAM best-practices guidance warns specifically that an IAM policy statement with
Resource: "*"can cover KMS keys the account doesn't own, so a principal granted blanketkms:Decryptcould decrypt ciphertext under any key that happens to have granted that account access — not just the key this review started from. Check forResource: "*"onkms:Decrypt/kms:GenerateDataKeystatements and for key-policy principals wider than the single service role that's supposed to hold this key. - Encrypt/Decrypt asymmetry. A job, pipeline, or role that only ever needs to encrypt new data (an ingest/write path) but was granted
Decrypttoo (often becauseEncryptandDecryptget granted as a pair by habit) widens the blast radius of that one credential leaking — check whether each grantee's actual usage (what the code calls) matches what it's allowed to call. - Stale grants. A grant added for a one-off migration, a debugging session, or a since-decommissioned service that's still listed in the policy. These don't show up by reading the application code that uses the key correctly — they show up by diffing the policy's grantee list against the set of roles the current codebase actually invokes this key from.
Report
Lead with which of the three properties actually failed and the concrete data at risk — don't report "uses deterministic encryption" or "has a rotation job" as findings on their own; those are neutral facts until step 2/3/4 shows the specific thing that's supposed to hold doesn't. BLOCK for a confirmed gap with real exposure: a uniqueness check that's provably bypassed for existing rows (step 2), a rotation procedure that only re-wraps when the threat model requires re-encryption (step 3) with no real re-encryption path existing at all, or a key policy with a wildcard/Resource: "*" grant or a stale principal still able to decrypt (step 4). CAUTION when the mechanism is right in shape but unverified at the edges — a re-encryption path exists but hasn't been run against production scale, a key policy looks scoped correctly today but nothing re-diffs it against actual usage on a schedule, a deterministic field's leakage is an accepted tradeoff but was never written down as a deliberate decision. SAFE only with evidence: the uniqueness/equality check traced through an actual query against the actual ciphertext scheme in use (not just a passing test against freshly-written rows), a rotation procedure whose code path matches what the threat model actually requires, and a key policy whose grantees were diffed against real current callers rather than trusted from when it was first written.
Boundaries
- Not
secret-spill's territory — that skill handles a credential that already leaked into git history and needs rotation/purge/verification; this skill audits whether an application's own at-rest field encryption design stays correct over time, independent of any specific leak. - Not
security-sweep's territory — that skill reviews a diff for exploitable payload/transport-level findings (injection, IDOR, SSRF). This skill is a standing architecture audit of encryption lifecycle correctness, not a per-diff vulnerability scan, and doesn't overlap with TLS/transport security at all. - Not a general cloud-IAM least-privilege audit — it only compares one encryption key's policy against the single application role that key was scoped for, per step 4. A broader "are all our IAM roles over-permissioned" review is a different, already-covered problem; don't widen this skill's report into one.
- Doesn't pick an encryption scheme, library, or algorithm from scratch — assumes a real decision (AES-256-GCM, a vetted KMS, deterministic-vs-random per field) has already been made and audits whether it's still being honored, not which one a new feature should choose.
- Can't observe a cloud key's actual policy history or who holds console/API access to change it beyond what's checked into infrastructure-as-code or exportable today — if the real policy lives only in a console someone edited by hand, say that plainly rather than treating the IaC definition as ground truth.
- Can't measure whether a field is actually queried for equality anywhere outside the codebase being reviewed (a downstream service, an analytics export, an ops script) — flag that as an open question rather than asserting a deterministic scheme is unnecessary just because this repo doesn't query it.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

