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