keyhog_scanner/decode/
hex.rs1use super::limits::{MAX_HEX_INPUT_LEN, MIN_HEX_CANDIDATE_LEN};
2use super::pipeline::{
3 push_batched_decoded_replacements, with_extracted_value_spans, ExtractedValue,
4};
5use super::{DecodeAdmissionSketch, Decoder, EncodedString};
6use keyhog_core::Chunk;
7
8pub(super) struct HexDecoder;
9
10impl Decoder for HexDecoder {
11 fn name(&self) -> &'static str {
12 "hex"
13 }
14
15 fn admission_sketch(&self, chunk: &Chunk) -> DecodeAdmissionSketch {
16 with_extracted_value_spans(&chunk.data, |candidates| {
17 let mut count = 0usize;
18 let mut bytes = 0usize;
19 for candidate in candidates
20 .iter()
21 .filter(|candidate| is_hex_candidate(candidate, MIN_HEX_CANDIDATE_LEN))
22 {
23 count = count.saturating_add(1);
24 bytes = bytes.saturating_add(candidate.value.len());
25 }
26 if count == 0 {
27 DecodeAdmissionSketch::NONE
28 } else {
29 DecodeAdmissionSketch::possible(DecodeAdmissionSketch::HEX, count, bytes)
30 }
31 })
32 }
33
34 fn decode_chunk(&self, chunk: &Chunk) -> Vec<Chunk> {
35 with_extracted_value_spans(&chunk.data, |candidates| {
36 let replacements = candidates
37 .iter()
38 .filter(|candidate| is_hex_candidate(candidate, MIN_HEX_CANDIDATE_LEN))
39 .filter_map(|candidate| {
40 let decoded = hex_decode(&candidate.value).ok()?;
41 let text = String::from_utf8(decoded).ok()?;
44 let (start, end) = candidate.span();
45 Some((start, end, text))
46 })
47 .collect();
48 push_batched_decoded_replacements(chunk, replacements, self.name())
49 })
50 }
51}
52
53pub fn find_hex_strings(text: &str, min_length: usize) -> Vec<EncodedString> {
56 find_hex_string_spans(text, min_length)
57 .into_iter()
58 .map(|candidate| EncodedString {
59 value: candidate.value,
60 })
61 .collect()
62}
63
64fn find_hex_string_spans(text: &str, min_length: usize) -> Vec<ExtractedValue> {
65 let mut results = Vec::new();
66 with_extracted_value_spans(text, |candidates| {
67 for candidate in candidates {
68 if is_hex_candidate(candidate, min_length) {
69 results.push(candidate.clone());
70 }
71 }
72 });
73 results
74}
75
76fn is_hex_candidate(candidate: &ExtractedValue, min_length: usize) -> bool {
77 let hex_len = candidate.value.bytes().filter(|byte| *byte != b'_').count();
86 hex_len >= min_length
87 && hex_len.is_multiple_of(2)
88 && candidate
89 .value
90 .bytes()
91 .all(|byte| byte == b'_' || byte.is_ascii_hexdigit())
92}
93
94#[allow(clippy::result_unit_err)]
98pub fn hex_decode(input: &str) -> Result<Vec<u8>, ()> {
99 if !input.as_bytes().contains(&b'_') {
100 if !input.len().is_multiple_of(2) || input.len() > MAX_HEX_INPUT_LEN {
101 return Err(());
102 }
103 return hex_simd::decode_to_vec(input.as_bytes()).map_err(|_| ());
104 }
105
106 let cleaned: String = input.chars().filter(|c| *c != '_').collect();
107 if !cleaned.len().is_multiple_of(2) || cleaned.len() > MAX_HEX_INPUT_LEN {
108 return Err(());
109 }
110 hex_simd::decode_to_vec(cleaned.as_bytes()).map_err(|_| ())
111}