gpg_inspector_lib/
armor.rs1use std::sync::Arc;
21
22use crate::error::{Error, Result};
23
24const BASE64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
25
26fn base64_decode_char(c: u8) -> Option<u8> {
27 BASE64_CHARS.iter().position(|&x| x == c).map(|p| p as u8)
28}
29
30fn base64_decode(input: &str) -> Result<Vec<u8>> {
31 let input: Vec<u8> = input
32 .bytes()
33 .filter(|&b| !b.is_ascii_whitespace())
34 .collect();
35
36 let mut output = Vec::with_capacity(input.len() * 3 / 4);
37 let mut buffer = 0u32;
38 let mut bits = 0;
39
40 for &byte in &input {
41 if byte == b'=' {
42 break;
43 }
44 let value = base64_decode_char(byte)
45 .ok_or_else(|| Error::Base64Error(format!("Invalid character: {}", byte as char)))?;
46
47 buffer = (buffer << 6) | (value as u32);
48 bits += 6;
49
50 if bits >= 8 {
51 bits -= 8;
52 output.push((buffer >> bits) as u8);
53 buffer &= (1 << bits) - 1;
54 }
55 }
56
57 Ok(output)
58}
59
60fn crc24(data: &[u8]) -> u32 {
61 const CRC24_INIT: u32 = 0xB704CE;
62 const CRC24_POLY: u32 = 0x1864CFB;
63
64 let mut crc = CRC24_INIT;
65 for &byte in data {
66 crc ^= (byte as u32) << 16;
67 for _ in 0..8 {
68 crc <<= 1;
69 if crc & 0x1000000 != 0 {
70 crc ^= CRC24_POLY;
71 }
72 }
73 }
74 crc & 0xFFFFFF
75}
76
77pub struct ArmorResult {
81 pub bytes: Arc<[u8]>,
83 pub armor_type: Arc<str>,
85}
86
87pub fn decode_armor(input: &str) -> Result<ArmorResult> {
123 let lines: Vec<&str> = input.lines().collect();
124 let (bytes, armor_type, _next) = decode_block(&lines, 0)?;
125
126 Ok(ArmorResult {
127 bytes: bytes.into(),
128 armor_type: armor_type.into(),
129 })
130}
131
132fn decode_block(lines: &[&str], from: usize) -> Result<(Vec<u8>, String, usize)> {
137 let header_idx = lines[from..]
138 .iter()
139 .position(|line| line.starts_with("-----BEGIN ") && line.ends_with("-----"))
140 .map(|p| p + from)
141 .ok_or_else(|| Error::InvalidArmor("Missing BEGIN header".into()))?;
142
143 let header_line = lines[header_idx];
144 let armor_type = header_line
145 .strip_prefix("-----BEGIN ")
146 .and_then(|s| s.strip_suffix("-----"))
147 .ok_or_else(|| Error::InvalidArmor("Invalid BEGIN header format".into()))?
148 .to_string();
149
150 let end_marker = format!("-----END {}-----", armor_type);
151 let footer_idx = lines[header_idx..]
152 .iter()
153 .position(|line| *line == end_marker)
154 .map(|p| p + header_idx)
155 .ok_or_else(|| Error::InvalidArmor("Missing END header".into()))?;
156
157 let mut body_start = header_idx + 1;
158 while body_start < footer_idx {
159 let line = lines[body_start];
160 if line.is_empty() || line.contains(':') {
161 body_start += 1;
162 } else {
163 break;
164 }
165 }
166
167 let body_lines: Vec<&str> = lines[body_start..footer_idx]
168 .iter()
169 .copied()
170 .filter(|line| !line.is_empty())
171 .collect();
172
173 if body_lines.is_empty() {
174 return Err(Error::InvalidArmor("Empty body".into()));
175 }
176
177 let checksum_line = body_lines
178 .last()
179 .filter(|line| line.starts_with('='))
180 .copied();
181
182 let data_lines = if checksum_line.is_some() {
183 &body_lines[..body_lines.len() - 1]
184 } else {
185 &body_lines[..]
186 };
187
188 let base64_data: String = data_lines.join("");
189 let bytes = base64_decode(&base64_data)?;
190
191 if let Some(checksum_str) = checksum_line {
192 let checksum_b64 = &checksum_str[1..];
193 let checksum_bytes = base64_decode(checksum_b64)?;
194 if checksum_bytes.len() >= 3 {
195 let expected = ((checksum_bytes[0] as u32) << 16)
196 | ((checksum_bytes[1] as u32) << 8)
197 | (checksum_bytes[2] as u32);
198 let actual = crc24(&bytes);
199 if expected != actual {
200 return Err(Error::ChecksumMismatch { expected, actual });
201 }
202 }
203 }
204
205 Ok((bytes, armor_type, footer_idx + 1))
206}
207
208pub struct ArmorBlock {
210 pub armor_type: Arc<str>,
212 pub range: (usize, usize),
215}
216
217pub struct MultiArmorResult {
220 pub bytes: Arc<[u8]>,
223 pub blocks: Vec<ArmorBlock>,
225 pub cleartext: Option<Arc<str>>,
228}
229
230const CLEARTEXT_HEADER: &str = "-----BEGIN PGP SIGNED MESSAGE-----";
231const SIGNATURE_HEADER: &str = "-----BEGIN PGP SIGNATURE-----";
232
233pub fn decode_armor_multi(input: &str) -> Result<MultiArmorResult> {
248 let lines: Vec<&str> = input.lines().collect();
249
250 let mut bytes: Vec<u8> = Vec::new();
251 let mut blocks: Vec<ArmorBlock> = Vec::new();
252 let mut cleartext: Option<Arc<str>> = None;
253 let mut idx = 0;
254
255 while let Some(begin_rel) = lines[idx..]
256 .iter()
257 .position(|line| line.starts_with("-----BEGIN ") && line.ends_with("-----"))
258 {
259 let begin_idx = idx + begin_rel;
260
261 if lines[begin_idx] == CLEARTEXT_HEADER {
262 let (text, sig_idx) = read_cleartext(&lines, begin_idx + 1)?;
263 if cleartext.is_none() {
264 cleartext = Some(text.into());
265 }
266 idx = sig_idx;
268 continue;
269 }
270
271 let (block_bytes, armor_type, next) = decode_block(&lines, begin_idx)?;
272 let start = bytes.len();
273 bytes.extend_from_slice(&block_bytes);
274 blocks.push(ArmorBlock {
275 armor_type: armor_type.into(),
276 range: (start, bytes.len()),
277 });
278 idx = next;
279 }
280
281 if blocks.is_empty() {
282 return Err(Error::InvalidArmor("Missing BEGIN header".into()));
283 }
284
285 Ok(MultiArmorResult {
286 bytes: bytes.into(),
287 blocks,
288 cleartext,
289 })
290}
291
292fn read_cleartext(lines: &[&str], mut idx: usize) -> Result<(String, usize)> {
297 while idx < lines.len() && !lines[idx].is_empty() {
299 idx += 1;
300 }
301 if idx < lines.len() {
302 idx += 1; }
304
305 let mut text_lines: Vec<&str> = Vec::new();
306 while idx < lines.len() {
307 let line = lines[idx];
308 if line == SIGNATURE_HEADER {
309 return Ok((text_lines.join("\n"), idx));
310 }
311 text_lines.push(line.strip_prefix("- ").unwrap_or(line));
312 idx += 1;
313 }
314
315 Err(Error::InvalidArmor(
316 "Cleartext signed message missing signature block".into(),
317 ))
318}
319
320pub fn looks_binary(bytes: &[u8]) -> bool {
327 bytes.first().is_some_and(|&b| b & 0x80 != 0)
328}