1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
//! PERF tripwire: the Caesar decoder's 25x shift fan-out must be gated on its
//! own *alphabetic-run* precondition, so a benign chunk with no shiftable long
//! letter run is cheap — WITHOUT regressing the url/html/escape decoders.
//!
//! ## The defect (file:line evidence)
//!
//! `CaesarDecoder::decode_chunk` in
//! `crates/scanner/src/decode/caesar.rs:100` pays the full decode cost on
//! EVERY chunk that isn't source-code / a credential-URL line:
//!
//! * line 127 `for candidate in extract_encoded_values(&chunk.data)` — a
//! full O(n) char-level walk of the chunk that *allocates* one `String`
//! per quoted string / `key=value` / base64 run
//! (`crates/scanner/src/decode/pipeline/extractor.rs:4`), and
//! * line 142-160 `for shift in 1..=25u8 { let decoded =
//! caesar_shift(&candidate, shift); ... }` — TWENTY-FIVE fresh
//! `String::with_capacity(input.len())` allocations + full re-scans
//! (`caesar.rs:166`) for every candidate ≥ 16 chars that contains *any*
//! alphabetic byte (`caesar.rs:139`).
//!
//! A Caesar/ROT-N shift only moves `a-z`/`A-Z`; digits and punctuation are the
//! identity, and the shift is a position-wise BIJECTION on the string. The
//! final gate inside `looks_credential_shaped` (`caesar.rs`) is a
//! `KNOWN_PREFIXES` substring test on the SHIFTED text, so
//! `shift_k(c).contains(P)` ⟺ `c.contains(rot_{-k}(P))`. A candidate that
//! contains NONE of the `rot_{-k}(KNOWN_PREFIXES)` needles (for any prefix and
//! any `k` in `1..=25`) can therefore never yield a credential-shaped variant
//! under any shift, so its entire 25x `caesar_shift` fan-out + re-scan is dead
//! work. Today the decoder runs that fan-out on every shape-passing candidate
//! before discovering there was no prefix to find — so benign log/prose/
//! `key=value` traffic (the dominant real-world chunk shape) burns the full
//! fan-out for zero recoverable credentials.
//!
//! The blanket `has_decodable_payload` pipeline gate was reverted
//! (`crates/scanner/src/decode/pipeline.rs`) because it dropped url/html/escape
//! recall. A "longest alphabetic run ≥ 16" gate is ALSO unsound: a `0x` / `SG.`
//! / `hf_` prefix needs only a 1–2 letter run, so a credential-shaped shift can
//! arise from a chunk with no long alphabetic run. The correct fix is *local*
//! to the Caesar decoder and recall-EXACT: an Aho-Corasick prefilter over the
//! `rot_{-k}(KNOWN_PREFIXES)` needle set at the top of the candidate loop,
//! leaving the other 13 decoders untouched. This file pins exactly that
//! contract.
//!
//! ## Measured cost (release-fast profile; opt-level 3, thin-LTO, this host)
//!
//! On a benign `key=value` log chunk with long (≥16-char) values that contain
//! NO 16+ contiguous alphabetic run and NO known prefix:
//!
//! | chunk size | `CaesarDecoder::decode_chunk` | one linear 16+-alpha-run scan | ratio |
//! |------------|-------------------------------|-------------------------------|--------|
//! | 16 KB | 6363 µs | 7.6 µs | 838x |
//! | 32 KB | 12770 µs | 15.0 µs | 853x |
//! | 64 KB | 25625 µs | 31.5 µs | 814x |
//!
//! The decoder does **~800x** the work a single precondition scan needs to
//! reject the chunk. A correctly-gated decoder bails after roughly ONE such
//! scan, i.e. a small single-digit multiple. (Even an undifferentiated benign
//! prose chunk measured 63-80x — the gate is missed on every benign shape.)
//!
//! ## Why this assertion is robust to CI/machine variance
//!
//! It is a RATIO of two in-process measurements over the SAME bytes:
//! * `caesar` = best-of-K `CaesarDecoder::decode_chunk`, and
//! * `scan` = best-of-K of a single linear pass computing the longest
//! contiguous ASCII-alphabetic run (exactly the precondition the fix adds).
//! Both are O(n) in chunk size, so the ratio is hardware-independent (it held
//! 838 / 853 / 814 across 16 / 32 / 64 KB). The tripwire floor is 50x: ~16x
//! below the measured ~840x (so an unoptimized build trips it under any
//! plausible jitter) and ~10x above an optimized ~5x (so the gated build won't
//! flake). Best-of-K (K=7, keep the min) strips scheduler noise.
//!
//! Build/run:
//! cd /media/mukund-thiru/SanthData/Santh/software/keyhog && \
//! CARGO_TARGET_DIR=/mnt/FlareTraining/santh-archive/cargo-target \
//! cargo test -p keyhog-scanner --test perf_decode_caesar --no-run
use ;
use CaesarDecoder;
use ;
use Instant;
/// Mirrors `MIN_CAESAR_LEN` in `crates/scanner/src/decode/caesar.rs:27`. A
/// Caesar variant can only become credential-shaped if some contiguous
/// alphabetic run is at least this long (there is nothing else for the shift
/// to act on), so this is the natural alphabetic-run precondition length.
const MIN_CAESAR_LEN: usize = 16;
/// Best-of-K reps, keep the min. K=7 strips scheduler/jitter noise on a busy
/// CI box while staying fast (sub-second total even on the largest fixture).
const REPS: usize = 7;
/// Hardware-independent floor. Measured ~814-853x on release-fast on the audit
/// host (see module doc). A Caesar decoder gated on an alphabetic-run
/// precondition rejects this chunk after ~1 linear scan (low single-digit
/// multiple). 50x sits well between the two: it cannot pass while the fan-out
/// runs unconditionally, and it cannot flake once the gate lands.
const MAX_CAESAR_OVER_SCAN_RATIO: f64 = 50.0;
/// Build a benign chunk of `~target` bytes: dense `key=value` log lines whose
/// values are LONG (≥16 chars, so each is a Caesar candidate that PASSES the
/// shift-invariant shape gate — a digit plus an 8+ alphanumeric run) yet
/// contain NO `rot_{-k}(KNOWN_PREFIXES)` needle under any shift. The value
/// tokens are drawn from `[a-z1-9]` only: no uppercase (every uppercase-prefix
/// needle — `AKIA`/`ASIA`/`AIza`/`SG.`/`eyJ` — stays uppercase under a shift),
/// no `_ - .` (every lowercase-prefix needle — `ghp_`/`sk-`/`hf_`/… — keeps its
/// punctuation under a shift), and no `0` (the `0x` prefix needles are `0`+a
/// letter). A Caesar shift only moves letters, so NO shift of any token here can
/// contain a known prefix; the decoder's rotated-prefix prefilter rejects every
/// candidate after one Aho-Corasick pass, skipping the 25× fan-out entirely.
/// The `caesar_decode_yields_nothing` sanity check below proves this chunk is
/// genuinely credential-free, so the skip is 100% recall-safe.
/// Longest contiguous run of ASCII-alphabetic bytes. This is exactly the cheap
/// O(n) precondition a properly-gated Caesar decoder must run before its
/// extractor walk + 25x fan-out, and the per-byte baseline the ratio divides
/// by.
/// PERF TRIPWIRE. FAILS on the current code (the fan-out runs unconditionally);
/// PASSES once `CaesarDecoder::decode_chunk` gates the extractor walk + 25x
/// shift loop on a `longest_alpha_run(..) >= MIN_CAESAR_LEN` precondition and
/// bails cheaply on a run-free chunk.
/// RECALL GUARD (the half that keeps the optimization honest). The Caesar gate
/// must be LOCAL to the Caesar decoder — it must not become a pipeline-wide
/// "looks-decodable" gate like the reverted `has_decodable_payload`
/// (crates/scanner/src/decode/pipeline.rs:95), which dropped url/html/escape
/// recall. A `KNOWN_PREFIXES` credential wrapped in url-percent, HTML numeric
/// entity, and `\xNN` hex-escape must STILL be recovered by the full
/// `decode_chunk` pipeline. If a future "speed up Caesar" change accidentally
/// short-circuits the whole fan-out on these run-free wrappers, this fails.