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
//! Regression: Caesar/ROT-N MULTI-SHIFT REGISTRY behaviour.
//!
//! Sibling file `regression_caesar_decoder.rs` locks the single-shift recovery
//! + shape gates. This file takes the DISTINCT angle of the shift *registry*:
//! the `matched_caesar_shifts` `[bool; 26]` selection table, the exact
//! rotated-prefix ⇔ forward-shift BIJECTION it encodes, index-0 never being a
//! shift, multi-shift unions, and how `decode_chunk` fans those selected shifts
//! into emitted sub-chunks (recovery, source-type tag, min-len boundary,
//! zero-emission when no shift is selected, recursion refusal on nested
//! `/caesar` source types).
//!
//! Every assertion pins a concrete value (exact bytes / exact bool / exact
//! `[bool; 26]` table / exact count), never a bare non-emptiness check.
//!
//! The `[bool; 26]` bijection assertions assume the rotated-prefix automaton
//! built successfully; it is built once from the compile-time constant
//! `KNOWN_PREFIXES`, so on any host it is the deterministic AC path (the
//! all-25 fallback is a Law-10 invariant-violation branch, not a host mode).
use keyhog_core::{Chunk, ChunkMetadata};
use keyhog_scanner::testing::decode_caesar;
use keyhog_scanner::testing::CaesarDecoder;
/// AWS-access-key-shaped plaintext credential (starts with the `AKIA` known
/// prefix; 20 chars ≥ `MIN_CAESAR_LEN`; carries digits and a long alnum run).
const PLANTED_AKIA: &str = "AKIA1234567890ABCDEF";
/// `PLANTED_AKIA` forward-shifted by 3 (the on-disk "encoded" form). Its
/// inverse-recovery shift is 26 − 3 = 23.
const AKIA_ENC_SHIFT3: &str = "DNLD1234567890DEFGHI";
/// A GitHub-PAT-shaped credential and its ROT13 (shift 13) form. ROT13 is its
/// own inverse, so shift 13 both encodes and recovers.
const GHP_ORIG: &str = "ghp_1234567890abcdefgh";
const GHP_ROT13: &str = "tuc_1234567890nopqrstu";
/// A Slack-bot-token-shaped credential forward-shifted by 5; recovery shift is
/// 26 − 5 = 21.
const XOXB_ENC_SHIFT5: &str = "ctcg-0123456789fghijk";
fn chunk(data: &str, source_type: &str, path: Option<&str>) -> Chunk {
Chunk {
data: data.into(),
metadata: ChunkMetadata {
source_type: source_type.into(),
path: path.map(Into::into),
..Default::default()
},
}
}
/// True iff some decoded output chunk's data equals `needle` exactly.
fn any_output_equals(out: &[Chunk], needle: &str) -> bool {
out.iter().any(|oc| {
let data: &str = &oc.data;
data == needle
})
}
/// The shift-selection table computed *independently* of the automaton, from
/// the forward-shift definition: shift `k` is selectable iff some KNOWN_PREFIX
/// appears in `caesar_shift(candidate, k)`. `matched_caesar_shifts` MUST equal
/// this for every candidate (the rotated-prefix ⇔ forward-shift bijection).
fn expected_shift_table(candidate: &str) -> [bool; 26] {
let mut expected = [false; 26];
for k in 1..=25u8 {
let shifted = decode_caesar::caesar_shift(candidate, k);
expected[k as usize] = (&*decode_caesar::KNOWN_PREFIXES)
.iter()
.any(|prefix| shifted.contains(prefix.as_str()));
}
expected
}
// ---------------------------------------------------------------------------
// caesar_shift primitive: exact outputs, round-trip, non-alpha passthrough.
// ---------------------------------------------------------------------------
#[test]
fn caesar_shift_produces_exact_reference_strings() {
// Forward shift 3 of the AWS credential: 20 letters rotate, 10 digits are
// identity (position-wise).
assert_eq!(
decode_caesar::caesar_shift(PLANTED_AKIA, 3),
AKIA_ENC_SHIFT3
);
// ROT13 of the GitHub PAT: `_` and the digits are fixed, letters rotate 13.
assert_eq!(decode_caesar::caesar_shift(GHP_ORIG, 13), GHP_ROT13);
// Forward shift 5 of the Slack token: `-` and digits fixed.
assert_eq!(
decode_caesar::caesar_shift("xoxb-0123456789abcdef", 5),
XOXB_ENC_SHIFT5
);
}
#[test]
fn rot13_roundtrips_a_known_secret_to_itself() {
// ROT13 is an involution: encoding then re-applying shift 13 restores the
// exact original credential byte-for-byte.
let encoded = decode_caesar::caesar_shift(GHP_ORIG, 13);
assert_eq!(encoded, GHP_ROT13);
assert_eq!(decode_caesar::caesar_shift(&encoded, 13), GHP_ORIG);
}
#[test]
fn caesar_shift_leaves_non_alphabetic_bytes_unchanged() {
// Digits, '-', '_', '.', and space are all shift-identity; a string with no
// ASCII letters is returned verbatim under any shift.
let non_alpha = "12-34_56.78 90";
assert_eq!(decode_caesar::caesar_shift(non_alpha, 7), non_alpha);
assert_eq!(decode_caesar::caesar_shift(non_alpha, 1), non_alpha);
assert_eq!(decode_caesar::caesar_shift(non_alpha, 25), non_alpha);
}
#[test]
fn caesar_shift_inverse_recovers_original_for_every_shift() {
// For k ∈ 1..=25, applying shift k then shift (26 − k) is the identity
// (mod-26 composition), so any of the 25 non-trivial encodings is exactly
// reversible. This is what makes the 25-shift registry lossless.
let secret = "AbYz1234567890Xk";
for k in 1..=25u8 {
let encoded = decode_caesar::caesar_shift(secret, k);
let recovered = decode_caesar::caesar_shift(&encoded, 26 - k);
assert_eq!(
recovered,
secret,
"shift {k} then {} must round-trip",
26 - k
);
}
}
// ---------------------------------------------------------------------------
// matched_caesar_shifts: the [bool; 26] shift-selection registry.
// ---------------------------------------------------------------------------
#[test]
fn matched_shifts_index_zero_is_never_a_selectable_shift() {
// The registry indexes k ∈ 1..=25; slot 0 (the trivial identity shift) is
// never populated, whatever the candidate.
assert!(!decode_caesar::matched_caesar_shifts(AKIA_ENC_SHIFT3)[0]);
assert!(!decode_caesar::matched_caesar_shifts(GHP_ROT13)[0]);
assert!(!decode_caesar::matched_caesar_shifts("zzzzzzzz12345678")[0]);
}
#[test]
fn matched_shifts_selects_the_exact_inverse_recovery_shift() {
// AKIA encoded +3 → recovery shift 23 is flagged.
assert!(decode_caesar::matched_caesar_shifts(AKIA_ENC_SHIFT3)[23]);
// ghp_ ROT13 → recovery shift 13 is flagged.
assert!(decode_caesar::matched_caesar_shifts(GHP_ROT13)[13]);
// xoxb- encoded +5 → recovery shift 21 is flagged.
assert!(decode_caesar::matched_caesar_shifts(XOXB_ENC_SHIFT5)[21]);
}
#[test]
fn matched_shifts_equals_forward_shift_bijection_on_akia() {
// Flagship registry lock: the automaton-computed table is byte-identical to
// the forward-shift reference table for the AKIA-encoded candidate. The
// reference here flags exactly {23}; the automaton must agree everywhere.
let table = decode_caesar::matched_caesar_shifts(AKIA_ENC_SHIFT3);
let expected = expected_shift_table(AKIA_ENC_SHIFT3);
assert_eq!(table, expected);
// Pin the concrete content: shift 23 selected, all others clear.
for k in 1..=25usize {
assert_eq!(table[k], k == 23, "shift {k} selection mismatch");
}
}
#[test]
fn matched_shifts_equals_forward_shift_bijection_on_rot13_ghp() {
// The bijection holds even when a candidate legitimately selects MORE than
// one shift (the ghp_ ROT13 form incidentally aligns a second prefix at
// another shift). The registry must match the forward reference exactly,
// not merely include the recovery shift.
let table = decode_caesar::matched_caesar_shifts(GHP_ROT13);
let expected = expected_shift_table(GHP_ROT13);
assert_eq!(table, expected);
// Shift 13 (the ROT13 recovery) is definitely among the selected set.
assert!(table[13]);
// At least two shifts are selected here (multi-shift, not single).
let selected = table.iter().filter(|&&b| b).count();
assert!(
selected >= 2,
"expected multi-shift selection, got {selected}"
);
}
#[test]
fn matched_shifts_selects_nothing_for_a_prefix_free_candidate() {
// An all-lowercase 'z' run plus digits: no rotation of any KNOWN_PREFIX can
// appear (rotated prefixes carry uppercase / `_` / `-` / `.` / a leading
// `0`, none present), so the whole 25-shift fan-out is provably dead work.
let candidate = "zzzzzzzz12345678";
let table = decode_caesar::matched_caesar_shifts(candidate);
assert_eq!(table, expected_shift_table(candidate));
assert_eq!(table.iter().filter(|&&b| b).count(), 0);
}
#[test]
fn matched_shifts_unions_multiple_distinct_shifts_in_one_candidate() {
// A candidate carrying the rotated form of `AKIA` (→ recovery shift 23) AND
// the rotated form of `ghp_` (→ recovery shift 13) selects BOTH shifts: the
// registry is a union over prefixes, not first-match-wins.
let candidate = "DNLD1234tuc_5678";
let table = decode_caesar::matched_caesar_shifts(candidate);
assert!(table[13], "ghp_ rotation must select shift 13");
assert!(table[23], "AKIA rotation must select shift 23");
assert_eq!(table, expected_shift_table(candidate));
}
// ---------------------------------------------------------------------------
// decode_chunk: fanning the selected shifts into emitted sub-chunks.
// ---------------------------------------------------------------------------
#[test]
fn decode_chunk_recovers_credential_via_registry_selected_shift() {
// A file line carrying the +3-encoded AKIA credential. `decode_chunk` must
// select shift 23 and emit the recovered plaintext as a `/caesar` chunk.
let body = format!("token = \"{AKIA_ENC_SHIFT3}\"\n");
let out = CaesarDecoder
.decode_chunk(&chunk(&body, "src", Some("creds.env")))
.expect("should decode successfully");
// The exact recovered credential is present.
assert!(
any_output_equals(&out, PLANTED_AKIA),
"must recover {PLANTED_AKIA}: {out:#?}"
);
// Every emitted sub-chunk carries the parent source-type with `/caesar`
// appended, and is itself credential-shaped (a real shift, not noise).
for oc in &out {
assert_eq!(oc.metadata.source_type.as_ref(), "src/caesar");
let data: &str = &oc.data;
assert!(
decode_caesar::caesar_credential_shape_gate(data),
"emitted chunk must be credential-shaped: {data:?}"
);
}
}
#[test]
fn decode_chunk_emits_nothing_when_registry_selects_no_shift() {
// The prefix-free candidate selects zero shifts, so the decoder emits no
// sub-chunks at all (the whole fan-out is skipped).
let body = "value = \"zzzzzzzz12345678\"\n";
let out = CaesarDecoder
.decode_chunk(&chunk(body, "src", None))
.expect("should decode successfully");
assert_eq!(out.len(), 0, "no shift selected → no emission: {out:#?}");
}
#[test]
fn decode_chunk_enforces_min_caesar_len_boundary() {
// 15-char encoded candidate: below MIN_CAESAR_LEN (16), so even though its
// rotated form would select shift 23, it is skipped entirely.
assert_eq!(decode_caesar::MIN_CAESAR_LEN, 16);
let short_body = "k=\"DNLD1234567890D\"\n"; // 15-char token
let short_out = CaesarDecoder
.decode_chunk(&chunk(short_body, "src", None))
.expect("should decode successfully");
assert_eq!(
short_out.len(),
0,
"15-char candidate must be skipped: {short_out:#?}"
);
// 16-char encoded candidate: at the boundary, recovered to `AKIA1234567890AB`.
let long_body = "k=\"DNLD1234567890DE\"\n"; // 16-char token
let long_out = CaesarDecoder
.decode_chunk(&chunk(long_body, "src", None))
.expect("should decode successfully");
assert!(
any_output_equals(&long_out, "AKIA1234567890AB"),
"16-char candidate must recover AKIA1234567890AB: {long_out:#?}"
);
}
#[test]
fn decode_chunk_refuses_recursion_on_nested_caesar_source_type() {
// A source-type that already contains a `/caesar` segment anywhere (here a
// base64→caesar chain) is our own prior output; re-shifting it would fold
// the value back, so the decoder returns nothing regardless of content.
let body = format!("token = \"{AKIA_ENC_SHIFT3}\"\n");
let out = CaesarDecoder
.decode_chunk(&chunk(&body, "raw/base64/caesar", Some("creds.env")))
.expect("should decode successfully");
assert_eq!(
out.len(),
0,
"nested /caesar source must not recurse: {out:#?}"
);
}