uselesskey_core/srp/negative/
pem.rs1use alloc::string::{String, ToString};
36use alloc::vec::Vec;
37
38use crate::srp::hash::hash32;
39
40#[derive(Clone, Copy, Debug)]
42pub enum CorruptPem {
43 BadHeader,
45 BadFooter,
47 BadBase64,
49 Truncate {
51 bytes: usize,
53 },
54 ExtraBlankLine,
56}
57
58pub fn corrupt_pem(pem: &str, how: CorruptPem) -> String {
60 match how {
61 CorruptPem::BadHeader => replace_first_line(pem, "-----BEGIN CORRUPTED KEY-----"),
62 CorruptPem::BadFooter => replace_last_line(pem, "-----END CORRUPTED KEY-----"),
63 CorruptPem::BadBase64 => inject_bad_base64_line(pem),
64 CorruptPem::Truncate { bytes } => truncate_utf8_at_byte_boundary(pem, bytes),
65 CorruptPem::ExtraBlankLine => inject_blank_line(pem),
66 }
67}
68
69pub fn corrupt_pem_deterministic(pem: &str, variant: &str) -> String {
73 let digest = hash32(variant.as_bytes());
74 let bytes = digest.as_bytes();
75
76 match bytes[0] % 5 {
77 0 => corrupt_pem(pem, CorruptPem::BadHeader),
78 1 => corrupt_pem(pem, CorruptPem::BadFooter),
79 2 => corrupt_pem(pem, CorruptPem::BadBase64),
80 3 => corrupt_pem(pem, CorruptPem::ExtraBlankLine),
81 _ => {
82 let bytes = derived_truncate_len(pem, bytes);
83 corrupt_pem(pem, CorruptPem::Truncate { bytes })
84 }
85 }
86}
87
88fn derived_truncate_len(pem: &str, digest: &[u8; 32]) -> usize {
89 let total_bytes = pem.len();
90 if total_bytes <= 1 {
91 return 0;
92 }
93
94 let span = total_bytes - 1;
95 1 + (u16::from_be_bytes([digest[1], digest[2]]) as usize % span)
96}
97
98fn truncate_utf8_at_byte_boundary(input: &str, max_bytes: usize) -> String {
99 if max_bytes >= input.len() {
100 return input.to_string();
101 }
102
103 let end = input
104 .char_indices()
105 .map(|(idx, _)| idx)
106 .take_while(|idx| *idx <= max_bytes)
107 .last()
108 .unwrap_or(0);
109 input[..end].to_string()
110}
111
112fn replace_first_line(pem: &str, replacement: &str) -> String {
113 let mut lines = pem.lines();
114 let _first = lines.next();
115
116 let mut out = String::new();
117 out.push_str(replacement);
118 out.push('\n');
119
120 for l in lines {
121 out.push_str(l);
122 out.push('\n');
123 }
124
125 out
126}
127
128fn replace_last_line(pem: &str, replacement: &str) -> String {
129 let mut all: Vec<&str> = pem.lines().collect();
130 if all.is_empty() {
131 return replacement.to_string();
132 }
133 let last_idx = all.len() - 1;
134 all[last_idx] = replacement;
135
136 let mut out = String::new();
137 for l in all {
138 out.push_str(l);
139 out.push('\n');
140 }
141 out
142}
143
144fn inject_bad_base64_line(pem: &str) -> String {
145 let mut lines: Vec<&str> = pem.lines().collect();
146 if lines.len() < 3 {
147 return alloc::format!("{pem}\nTHIS_IS_NOT_BASE64!!!\n");
148 }
149
150 lines.insert(1, "THIS_IS_NOT_BASE64!!!");
151
152 let mut out = String::new();
153 for l in lines {
154 out.push_str(l);
155 out.push('\n');
156 }
157 out
158}
159
160fn inject_blank_line(pem: &str) -> String {
161 let mut lines: Vec<&str> = pem.lines().collect();
162 if lines.len() < 3 {
163 return alloc::format!("{pem}\n\n");
164 }
165 lines.insert(1, "");
166
167 let mut out = String::new();
168 for l in lines {
169 out.push_str(l);
170 out.push('\n');
171 }
172 out
173}
174
175#[cfg(all(test, feature = "std"))]
176mod tests {
177 use super::*;
178 use std::collections::HashSet;
179
180 #[test]
181 fn bad_header_replaces_first_line() {
182 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
183 let out = corrupt_pem(pem, CorruptPem::BadHeader);
184 assert!(out.starts_with("-----BEGIN CORRUPTED KEY-----\n"));
185 }
186
187 #[test]
188 fn bad_footer_replaces_last_line() {
189 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
190 let out = corrupt_pem(pem, CorruptPem::BadFooter);
191 assert!(out.contains("-----END CORRUPTED KEY-----\n"));
192 }
193
194 #[test]
195 fn bad_footer_on_empty_input_returns_replacement() {
196 let out = corrupt_pem("", CorruptPem::BadFooter);
197 assert_eq!(out, "-----END CORRUPTED KEY-----");
198 }
199
200 #[test]
201 fn bad_base64_short_input_inserts_line() {
202 let out = corrupt_pem("x", CorruptPem::BadBase64);
203 assert_eq!(out, "x\nTHIS_IS_NOT_BASE64!!!\n");
204 }
205
206 #[test]
207 fn extra_blank_line_short_input_appends_newlines() {
208 let out = corrupt_pem("x", CorruptPem::ExtraBlankLine);
209 assert_eq!(out, "x\n\n");
210 }
211
212 #[test]
213 fn truncate_variant_limits_length() {
214 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
215 let out = corrupt_pem(pem, CorruptPem::Truncate { bytes: 10 });
216 assert_eq!(out.len(), 10);
217 }
218
219 #[test]
220 fn truncate_variant_uses_byte_budget_without_breaking_utf8() {
221 let pem = "🔐KEY";
222 let out = corrupt_pem(pem, CorruptPem::Truncate { bytes: 3 });
223 assert_eq!(out, "");
224
225 let out = corrupt_pem(pem, CorruptPem::Truncate { bytes: 4 });
226 assert_eq!(out, "🔐");
227 }
228
229 #[test]
230 fn deterministic_truncate_never_exceeds_input_byte_length() {
231 let pem = "🔐A";
232 let variant = (0..256)
233 .map(|i| alloc::format!("corrupt:truncate:{i}"))
234 .find(|candidate| hash32(candidate.as_bytes()).as_bytes()[0] % 5 == 4)
235 .expect("a truncate variant should exist");
236 let out = corrupt_pem_deterministic(pem, &variant);
237 assert!(out.len() <= pem.len());
238 assert!(pem.starts_with(&out));
239 }
240
241 #[test]
242 fn deterministic_corruption_is_stable_for_same_variant() {
243 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
244 let first = corrupt_pem_deterministic(pem, "corrupt:variant-a");
245 let second = corrupt_pem_deterministic(pem, "corrupt:variant-a");
246 assert_eq!(first, second);
247 }
248
249 #[test]
250 fn deterministic_corruption_produces_multiple_shapes_across_variants() {
251 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
252 let variants = ["a", "b", "c", "d", "e", "f", "g", "h"];
253 let mut outputs = HashSet::new();
254 for v in variants {
255 outputs.insert(corrupt_pem_deterministic(pem, v));
256 }
257 assert!(outputs.len() >= 2);
258 }
259
260 fn find_variant(target: u8) -> String {
261 for i in 0u64.. {
262 let v = format!("v{i}");
263 if hash32(v.as_bytes()).as_bytes()[0] % 5 == target {
264 return v;
265 }
266 }
267 unreachable!()
268 }
269
270 #[test]
271 fn deterministic_pem_arm0_bad_header() {
272 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
273 let out = corrupt_pem_deterministic(pem, &find_variant(0));
274 assert!(out.contains("BEGIN CORRUPTED KEY"));
275 }
276
277 #[test]
278 fn deterministic_pem_arm1_bad_footer() {
279 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
280 let out = corrupt_pem_deterministic(pem, &find_variant(1));
281 assert!(out.contains("END CORRUPTED KEY"));
282 }
283
284 #[test]
285 fn deterministic_pem_arm2_bad_base64() {
286 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
287 let out = corrupt_pem_deterministic(pem, &find_variant(2));
288 assert!(out.contains("THIS_IS_NOT_BASE64!!!"));
289 }
290
291 #[test]
292 fn deterministic_pem_arm3_blank_line() {
293 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
294 let out = corrupt_pem_deterministic(pem, &find_variant(3));
295 assert!(out.contains("BEGIN TEST-----\n\n"));
296 }
297
298 #[test]
299 fn deterministic_pem_arm4_truncate() {
300 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
301 let out = corrupt_pem_deterministic(pem, &find_variant(4));
302 assert!(out.len() < pem.len());
303 }
304
305 #[test]
306 fn bad_base64_inserts_after_header_in_normal_pem() {
307 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
310 let out = corrupt_pem(pem, CorruptPem::BadBase64);
311 let lines: Vec<&str> = out.lines().collect();
312 assert_eq!(lines.len(), 4);
313 assert_eq!(lines[0], "-----BEGIN TEST-----");
314 assert_eq!(lines[1], "THIS_IS_NOT_BASE64!!!");
315 assert_eq!(lines[2], "AAA=");
316 }
317
318 #[test]
319 fn bad_base64_two_line_pem_appends() {
320 let pem = "line1\nline2";
323 let out = corrupt_pem(pem, CorruptPem::BadBase64);
324 assert_eq!(out, "line1\nline2\nTHIS_IS_NOT_BASE64!!!\n");
325 }
326
327 #[test]
328 fn blank_line_inserts_after_header_in_normal_pem() {
329 let pem = "-----BEGIN TEST-----\nAAA=\n-----END TEST-----\n";
331 let out = corrupt_pem(pem, CorruptPem::ExtraBlankLine);
332 let lines: Vec<&str> = out.lines().collect();
333 assert_eq!(lines.len(), 4);
334 assert_eq!(lines[0], "-----BEGIN TEST-----");
335 assert_eq!(lines[1], "");
336 assert_eq!(lines[2], "AAA=");
337 }
338
339 #[test]
340 fn blank_line_two_line_pem_appends() {
341 let pem = "line1\nline2";
342 let out = corrupt_pem(pem, CorruptPem::ExtraBlankLine);
343 assert_eq!(out, "line1\nline2\n\n");
344 }
345
346 #[test]
347 fn derived_truncate_len_exact_arithmetic() {
348 let mut digest = [0u8; 32];
351 digest[1] = 0x0A;
352 digest[2] = 0x0B;
353 assert_eq!(derived_truncate_len("0123456789", &digest), 7);
355 }
356
357 #[test]
358 fn derived_truncate_len_empty_returns_zero() {
359 let digest = [0u8; 32];
360 assert_eq!(derived_truncate_len("", &digest), 0);
361 }
362
363 #[test]
364 fn derived_truncate_len_single_char_returns_zero() {
365 let digest = [0xFF; 32];
366 assert_eq!(derived_truncate_len("x", &digest), 0);
367 }
368}