Repository navigation
Keep-rules follow-ups: unforgeable placeholders, CVV floor in text, card numbers in token text (0.17.1, #160102) - #72
Conversation
…olds Placeholders were named by position, `[KEPT-A-MASKED]`, `[KEPT-B-MASKED]`. An element whose entity-encoded text decoded to such a name got the kept value beside it copied in by the outer restore (review M4), and a credential hashed around a kept value hashed the placeholder, so every value there gave one token where the key walk gives each its own (M3). A placeholder is now named from a blake2b digest of the value as written: fourteen digit-free letters, base 26. The same value has the same name in any text (two identical kept values share one entry), a different value another, and a name written by anyone without the value restores nothing. Two values whose names collide are masked as without a keep rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y right before
In text the floor read only the key right before an object, so these were
kept where the key walk keeps nothing (review M2): `{"cvv": [ {…} ]}` (a
list between), `{"name": "cvv", "value": {…}}` (a pair labelled as one, which
the README said the floor covered), `"cvv" = {…}` and `'cvv' => {…}`.
`_find_kept` no longer looks into an object nothing matched whose parse has a
CVV or SAD key at its top level or is a pair labelled as one -- read as the
guard reads a pair, now `value_rules.pair_labelled` -- nor into one that does
not parse or names a key twice. It fails closed: a value under a sibling key
of such an object is masked, not kept. `_KEY_BEFORE` takes `=>`, and a quoted
key followed by `=` or `=>`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `[APPLE_PAY_RULE, KeepRule(lambda value: True)]`,
`{"outer": {"inner": <paymentData>}}` shipped the inner token unlabelled:
the keep rule matched the outer mapping first (review M8).
A keep match is now refused, walked as if nothing had matched, when a
label rule labels something strictly inside it (`keep_refused`:
`label_within` over its members, asked only when a label rule is among the
rules). The check sits where the guard's does, so the key walk, text, rule
2, bodies and `is_kept` all get it; `label_within` looks into a keep match
like any other mapping. For the value itself the first match still wins: a
keep rule listed first for the same value keeps it.
`test_an_inner_label_match_is_kept_with_the_kept_object` pinned 0.17.0's
reading and now pins this one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d, not read as a keep rule Such a rule loaded as a keep rule, the less safe reading (review M6). `load_value_rules` now reports it as a problem naming both forms, alone or in a collection: `configure_masking_value_rules` raises, and from the setting or env var it is dropped with a warning and listed by the Django boot check, which surfaces those problems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t holds token text The guard reads keys, pairs and whole leaves, so text inside a kept token's string slot shipped with it: `"data": "cvv=123"` (review N3, Redmine R3). Each slot must now be text in its alphabet. Apple Pay `paymentData`: `data`, `signature` and the header's `ephemeralPublicKey`, `publicKeyHash` and `wrappedKey` standard base64, `transactionId` and `applicationData` hex. PKPaymentToken: `transactionIdentifier` hex, `paymentMethod` short text (64 characters at most). Google Pay: `signature` and every `intermediateSigningKey.signatures` base64; `signedMessage` JSON text of `encryptedMessage`, `ephemeralPublicKey` and `tag` in base64 and nothing else, `signedKey` of `keyValue` in base64 and `keyExpiration` in digits and nothing else, both read strictly (a key written twice refused). A token that does not conform is masked as before. Every fixture token conforms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d token, what never raises, what keep rules cost - An entity-encoded wallet token is set aside by redact_body but not by the log processor's text pass: with pci about one entity-encoded Google Pay token in ten comes out altered, failing closed (review M5). - is_kept and mask_outside_kept never raise of their own; mask_outside_kept passes on the caller's mask errors (M7). The cost paragraph says what keep rules cost a line, and a line with a wallet. - A repr of a Google Pay ECv2 token inside JSON-encoded text is not kept: its escaped quotes no longer render back as written (N2). - CLAUDE.md: placeholder names, the text floor's reading of an object read whole, a label rule winning inside a keep match, the refused ambiguous rule, Ottu's slot alphabets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text floor read only the object right before a key, or the objects the
text rules read whole, three levels deep. A wallet token's own nesting
takes two, so `{"cvv": [ <token> ]}`, `{"name": "cvv", "value": <token>}`
and a token deeper under `securityCode` were kept for every real Ottu
token, where the key walk keeps nothing (review I1).
`kept_spans` now keeps nothing in text that names a CVV or SAD key, XML
tag or `{name, value}` pair label anywhere (`floored_text`): a quoted key
then `:`, `=` or `=>`, a bare key then `:` or `=>`, a key and `=` where a
key starts (not base64 padding inside a string), an element's tag, or a
pair's label, read with `classify_key(..., ALL_PACKS)` and the service's
safe keys, as written and entity-decoded. It is read only once a span is
found. `redact_body` keeps nothing in an entity-encoded element of a body
that names one. It fails closed: a value beside such a key is not kept in
text, though the key walk keeps it.
The per-object checks this replaces (`_floored_before`, `_KEY_BEFORE`,
`_floors`) add nothing the scan does not, and are gone; an object that
does not parse or names a key twice is read member by member again, as in
0.17.0. Ottu's KPay body, Connect's SDK Apple Pay request and a Google Pay
PaymentData body still keep their tokens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… holds a card number The guard read keys, pairs and whole leaves, so a card number written inside a kept value's string shipped with it, even in a slot's own alphabet: `"data": "4111…+cvv+123"` is base64, `"transactionId": "4111…ab"` hex (review I2). A text leaf that is no JSON text now holds card data when a run of 13 to 19 digits in it, joined by single separators at most and bounded by non-digits, passes Luhn (`patterns.holds_card_run`), epoch milliseconds (thirteen digits from a 1 or a 2) excepted as for a whole leaf. JSON text is read by its own leaves, so Google Pay's `keyExpiration` stays exempt. It is in the core guard, so every keep rule gets it. A CVV or other short value inside a string still ships with it, as the README now says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the last four `displayName` was any text of up to 64 characters, so `"Visa 1234 cvv 123"` shipped the CVV in a kept token (review I2). It is now at most 40 characters with no digit but one trailing group of exactly four after a space (`"Visa 0492"`, `"MasterCard 1234"`, `"Amex"`); anything else is no wallet's display name, and the token is not kept whole (its `paymentData` still is). The README says what the slot checks and the guard catch and what they cannot: a CVV written in a slot's own alphabet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… every field
`signedMessage` and `signedKey` were read as "at most" their fields, so
`"signedMessage": "{}"` was kept (review M7). `signedMessage` must now hold
`encryptedMessage`, `ephemeralPublicKey` and `tag`, and `signedKey`
`keyValue` and `keyExpiration`, and nothing else. The `GOOGLE_PAY_V1`
fixture held only `encryptedMessage`, which no ECv1 token does: it now
holds all three.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e keep, raising nothing `keep_refused` asks the label rules about a keep match's members, which 0.17.0 never asked: a label matcher raising there made `redact_body` and `mask_by_patterns` raise where 0.17.0 returned the value kept (review M5). Its error now refuses the keep (fails closed): the value is masked as with the label rules alone, and nothing is raised from that question. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…allet in microseconds Measured on a KPay body holding an Apple Pay token (7 KB), `floored_text` took 0.9 ms as regexes tried at every character (a lookahead at each, and a bare name backtracking through every base64 word), and the guard's run check 74 µs a 3 KB leaf. `floored_text` now finds each `:` and `=` and reads the key back from there (a quote's opening found with `rfind`, a bare name matched on the reversed text before it), and reads a pair's label forward from an identifier key: about 20 µs. `holds_card_run` first marks digits and separators with one bytes translate: ASCII text with neither thirteen digits in a row nor a separator (base64, hex) holds no run, about 2.5 µs a 3 KB leaf. A name longer than 128 characters is read by its end, after `:` or `=>`, where it was not read at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de a keep match when it is the first match - The cost paragraph gives the measured figures (review 3): with Ottu's WALLET_RULES a line with no wallet token costs 1-4% more than 0.16.0's label rules and 3-15% more than no value rule; a line with a token has no single figure (an MPGS paymentToken line, a Google Pay message, a KPay body, each labelled / kept / unconfigured), and this release's checks add 15-40% over 0.17.0 there. - The M8 example needs the label rule listed before the catch-all keep rule, which otherwise is the inner value's first match too (review 4); CLAUDE.md and the value_rules docstring said "wherever it is listed". - CLAUDE.md: the guard's card-number run, a label rule's error refusing a keep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…characters or more Read like other text, about one random 64-hex id in two hundred holds a 13-19 digit run that passes Luhn, and an Apple Pay token carries two (`transactionId`, `transactionIdentifier`): 1.1% of tokens were refused and masked instead of kept. A run inside a whole hex token of 24 characters or more -- an ObjectId, a 32-hex gateway id, a 64-hex digest, nothing alphanumeric touching it -- is no card number, as Connect's own card scan reads one. Shorter hex (`4111…ab`) and hex glued to more letters are still read, and a whole-leaf card number is still refused first. On 3000 random tokens, none is refused now, Apple Pay or Google Pay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o, in one pass, skipping only a whole hex id Round 2 skipped a Luhn run inside any whole hex token in a leaf, and found the tokens again for every run: a crafted base64 `data` of `/4111111111111111deadbeef` repeated kept its token and cost 6.5 s at 400 KB (review r2, 1). Bounded by `/`, the hex inside base64 also shipped its card number (4). And the run was narrower than the pci rules': a card beside another digit group (`1234 4111…`, `4111… 123`) and 20 bare digits shipped (3). A leaf that is a whole hex id of 24 characters or more -- Apple Pay's `transactionId`, `transactionIdentifier`, `applicationData` -- is not read for a run; any other text leaf is read once with `holds_pan_run`, as the pci rules read a value: 12 digits or more joined by single separators. A leaf that is epoch milliseconds stays exempt, and a whole-leaf card number is still refused first. 400 KB of the crafted `data` is refused in 9 ms (the keep decision on its text 33 ms), linear in its size; of 3000 random Apple Pay tokens none is refused for a hex id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lly the last four
`displayName` accepted any non-digit text of up to 40 characters, so
`jane.roe@example.com` was kept (review r2, 6). It is now
`[A-Za-z][A-Za-z .&-]{0,39}`, optionally followed by one space and
exactly four digits (`Visa 0492`, `American Express`, `Amex`), and the
README says exactly that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…does The README said the text floor reads a name "as the key walk reads a key" (review r2, 2). It reads a quoted key, tag or pair label as written, up to 128 characters (a longer key by its last 128), with no `\uXXXX` escape decoded; the key walk parses JSON and reads more, so a key written with escapes, or longer with its CVV word first, floors there and not in text. Through Ottu's WALLET_RULES that gap ships nothing but a token's own text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rd's one-pass read Re-measured after the guard's read became one pass of `holds_pan_run` (this round): a Google Pay message line kept costs ~300 µs where it cost ~341 µs, and this release's checks add 10-35% over 0.17.0 on a wallet line, where the paragraph said 15-40%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A leaf of digits alone matched the hex-id exemption, so 24 digits or more written into a hex slot (a card number and eight digits in Apple Pay's `transactionId`) were not read for a card number. A hex id holds a hex letter; digits alone are read like any other text (#160102). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ame never names a CVV
A whole hex id of 24 characters or more is not read for a card number (the
guard's exemption for Apple Pay's ids), so a base64 slot written in hex
alone (`"data": "4111111111111111abcd1234"`) shipped a card number. A
wallet's base64 never is: Ottu's matchers refuse it. And a displayName
that reads as a CVV or other SAD ("CVV 1234", "Security Code 1234") is no
card's: a CID is four digits, the shape of the last four (#160102).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the hex letter The README names the canonical UUID as the guard's third exempt leaf and rewrites that paragraph; the text floor reads an XML tag of any length; the docstrings and ecsctx/CLAUDE.md say a skipped hex id holds a hex letter, and the stale "Not holds_pan_run" line is gone (#160102). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@claude review |
|
Claude finished @jab3z's task in 1m 6s —— View job Review of #72 (keep-rules follow-ups, 0.17.1)I read the code statically. I couldn't run the tests or my probes: the sandbox denied Overall: the design holds up. Naming placeholders from a digest of the value, applying the CVV/SAD floor to the whole text, and reading card runs inside kept text are all sound, and every failure path fails closed. I found two gaps, both in places where the PR text claims more than the code does. Neither blocks the PR, but I'd resolve both before release or add them to the "Left for a follow-up" list. Findings1.
2. The hex-id exemption lets a card number ship inside a hex-alphabet slot (
Minor
Not verified
|
… display name A PKPaymentToken's paymentMethod network and type were any text of 64 characters at most, so `"network": "cvv=123"` or an email in `type` shipped with the kept token: the guard reads keys, pairs and card numbers, not a CVV written in a string. Both are now words alone or nothing (letters, spaces, `.`, `&`, `-`, from a letter, 40 at most) that read as no CVV or other SAD, as the display name already was (bot review on #72). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The library side of #160102: what the 0.17.0 keep-rules round left, closed after three review rounds. A patch release (0.17.1): every change is a fix or a tightening. Some wallet tokens that 0.17.0 kept are now masked (fails closed); none that 0.17.0 masked now ships.
Placeholders can't be forged or confused (M4, M3)
[KEPT-<14 letters>-MASKED]), not its position.[KEPT-A-MASKED]in a KPay body) restores nothing.The CVV/SAD floor in text (M2)
{name, value}pair label anywhere keeps no wallet in it. It uses the key walk's classification, with safe keys honoured.{"cvv": [token]}can only be floored this way. Fails closed.<udf9>beside<password>, Connect's SDK Apple Pay request besidecvv_required, and a Google Pay body.The guard reads a card number inside a kept value's text (N3)
holds_pan_run). These now refuse the keep:"4111…+cvv+123"in base64,"4111…ab"in hex,"1234 4111…","4111… 12/25", 20 bare digits.keyExpiration), a canonical UUID, and a whole hex id of 24+ characters holding a hex letter.transactionIdsometimes holds a Luhn run.Ottu's matchers (N3, contrib)
signedMessageandsignedKeymust hold exactly their fields.displayNameis letters plus an optional last four, and never a CVV or SAD name ("CVV 1234").networkandtypeare letters alone, or empty, and never a CVV or SAD name ("network": "cvv=123"and an email intypewere kept before; from the bot review).Also
field_typeandkeep = Trueis refused as ambiguous (M6).Verification
golden/masking_0156.jsonis unchanged.displayName, and docs.Left for a follow-up
WALLET_RULES:int_is_panonly;\uXXXXescapes is read by the key walk, not the text floor (documented).displayNamefails the matcher; the token is then masked, which fails closed.Release
Patch, 0.17.1. Connect and Ottu PG then pin
>=0.17.1.Refs #160102
🤖 Generated with Claude Code