Skip to main content

gpg_inspector_lib/packet/
user_attribute.rs

1//! User Attribute packet parsing (tag 17).
2//!
3//! User Attribute packets can store additional data about a key holder,
4//! most commonly an image (photo ID). The packet contains one or more
5//! subpackets, each with its own type and data.
6//!
7//! RFC 4880 Section 5.12
8
9use crate::error::Result;
10use crate::stream::ByteStream;
11
12use super::Field;
13
14/// Image encoding format in a User Attribute image subpacket.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ImageFormat {
17    /// JPEG image (format 1).
18    Jpeg,
19    /// Private/experimental format (100-110).
20    Private(u8),
21    /// Unknown format.
22    Unknown(u8),
23}
24
25impl ImageFormat {
26    fn from_u8(b: u8) -> Self {
27        match b {
28            1 => ImageFormat::Jpeg,
29            100..=110 => ImageFormat::Private(b),
30            _ => ImageFormat::Unknown(b),
31        }
32    }
33
34    fn description(&self) -> String {
35        match self {
36            ImageFormat::Jpeg => "1 (JPEG)".to_string(),
37            ImageFormat::Private(n) => format!("{} (Private/Experimental)", n),
38            ImageFormat::Unknown(n) => format!("{} (Unknown)", n),
39        }
40    }
41}
42
43/// A subpacket within a User Attribute packet.
44#[derive(Debug, Clone)]
45pub enum UserAttributeSubpacket {
46    /// Image subpacket (type 1).
47    Image {
48        /// Header version (typically 1).
49        header_version: u8,
50        /// Image encoding format.
51        format: ImageFormat,
52        /// Raw image data.
53        image_data: Vec<u8>,
54    },
55    /// Unknown subpacket type.
56    Unknown {
57        /// Subpacket type ID.
58        subpacket_type: u8,
59        /// Raw subpacket data.
60        data: Vec<u8>,
61    },
62}
63
64/// Parsed User Attribute packet.
65#[derive(Debug, Clone)]
66pub struct UserAttributePacket {
67    /// List of attribute subpackets.
68    pub subpackets: Vec<UserAttributeSubpacket>,
69}
70
71/// Parses a User Attribute packet body.
72pub fn parse_user_attribute(
73    stream: &mut ByteStream,
74    fields: &mut Vec<Field>,
75    body_offset: usize,
76) -> Result<UserAttributePacket> {
77    let mut subpackets = Vec::new();
78
79    while !stream.is_empty() {
80        let subpacket_start = body_offset + stream.pos();
81        let (subpacket_len, _len_size) = parse_subpacket_length(stream)?;
82
83        if subpacket_len == 0 {
84            continue;
85        }
86
87        let _type_pos = body_offset + stream.pos();
88        let subpacket_type = stream.octet()?;
89        let data_len = subpacket_len - 1; // Subtract type byte
90
91        let subpacket = match subpacket_type {
92            1 => parse_image_subpacket(stream, fields, body_offset, data_len)?,
93            100..=110 => {
94                let data_start = body_offset + stream.pos();
95                let data = stream.bytes(data_len)?;
96                fields.push(Field::field(
97                    "Private Subpacket",
98                    format!("Type {}, {} bytes", subpacket_type, data.len()),
99                    (subpacket_start, data_start + data_len),
100                ));
101                UserAttributeSubpacket::Unknown {
102                    subpacket_type,
103                    data,
104                }
105            }
106            _ => {
107                let data_start = body_offset + stream.pos();
108                let data = stream.bytes(data_len)?;
109                fields.push(Field::field(
110                    "Unknown Subpacket",
111                    format!("Type {}, {} bytes", subpacket_type, data.len()),
112                    (subpacket_start, data_start + data_len),
113                ));
114                UserAttributeSubpacket::Unknown {
115                    subpacket_type,
116                    data,
117                }
118            }
119        };
120
121        subpackets.push(subpacket);
122    }
123
124    Ok(UserAttributePacket { subpackets })
125}
126
127/// Parses an image subpacket (type 1).
128fn parse_image_subpacket(
129    stream: &mut ByteStream,
130    fields: &mut Vec<Field>,
131    body_offset: usize,
132    data_len: usize,
133) -> Result<UserAttributeSubpacket> {
134    let header_start = body_offset + stream.pos();
135
136    // Image header: 2 bytes (little-endian length), 1 byte version, 1 byte format
137    // For v1 headers, total header length is 16 bytes (including the length field)
138    let header_len_lo = stream.octet()? as u16;
139    let header_len_hi = stream.octet()? as u16;
140    let header_len = header_len_lo | (header_len_hi << 8);
141
142    let header_version = stream.octet()?;
143
144    fields.push(Field::field(
145        "Image Subpacket",
146        format!("Header v{}, {} bytes total", header_version, header_len),
147        (header_start, header_start + 3),
148    ));
149
150    if header_version == 1 {
151        let format_pos = body_offset + stream.pos();
152        let format_byte = stream.octet()?;
153        let format = ImageFormat::from_u8(format_byte);
154        fields.push(Field::subfield(
155            "Image Format",
156            format.description(),
157            (format_pos, format_pos + 1),
158        ));
159
160        // Skip remaining header bytes (12 reserved bytes for v1)
161        let remaining_header = header_len as usize - 4; // Already read 4 bytes
162        if remaining_header > 0 && stream.remaining() >= remaining_header {
163            stream.skip(remaining_header)?;
164        }
165
166        // Rest is image data
167        let image_start = body_offset + stream.pos();
168        let image_bytes_available = data_len.saturating_sub(header_len as usize);
169        let image_data = if image_bytes_available > 0 && stream.remaining() >= image_bytes_available
170        {
171            stream.bytes(image_bytes_available)?
172        } else {
173            stream.rest()
174        };
175
176        // Try to detect image type from magic bytes
177        let detected_type = detect_image_type(&image_data);
178        let image_desc = if let Some(detected) = detected_type {
179            format!("{} bytes ({})", image_data.len(), detected)
180        } else {
181            format!("{} bytes", image_data.len())
182        };
183
184        fields.push(Field::subfield(
185            "Image Data",
186            image_desc,
187            (image_start, image_start + image_data.len()),
188        ));
189
190        Ok(UserAttributeSubpacket::Image {
191            header_version,
192            format,
193            image_data,
194        })
195    } else {
196        // Unknown header version, store as raw data
197        let data_start = body_offset + stream.pos();
198        let remaining = data_len.saturating_sub(3); // Already read 3 bytes
199        let data = if remaining > 0 {
200            stream.bytes(remaining)?
201        } else {
202            Vec::new()
203        };
204
205        fields.push(Field::subfield(
206            "Image Data",
207            format!("{} bytes (unknown header v{})", data.len(), header_version),
208            (data_start, data_start + data.len()),
209        ));
210
211        Ok(UserAttributeSubpacket::Image {
212            header_version,
213            format: ImageFormat::Unknown(0),
214            image_data: data,
215        })
216    }
217}
218
219/// Parses a subpacket length (same format as signature subpackets).
220fn parse_subpacket_length(stream: &mut ByteStream) -> Result<(usize, usize)> {
221    let first = stream.octet()? as usize;
222    if first < 192 {
223        Ok((first, 1))
224    } else if first < 255 {
225        let second = stream.octet()? as usize;
226        Ok((((first - 192) << 8) + second + 192, 2))
227    } else {
228        let len = stream.uint32()? as usize;
229        Ok((len, 5))
230    }
231}
232
233/// Attempts to detect image type from magic bytes.
234fn detect_image_type(data: &[u8]) -> Option<&'static str> {
235    if data.len() < 4 {
236        return None;
237    }
238
239    // JPEG: FF D8 FF
240    if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
241        return Some("JPEG detected");
242    }
243
244    // PNG: 89 50 4E 47
245    if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
246        return Some("PNG detected");
247    }
248
249    // GIF: 47 49 46 38
250    if data.starts_with(&[0x47, 0x49, 0x46, 0x38]) {
251        return Some("GIF detected");
252    }
253
254    // BMP: 42 4D
255    if data.starts_with(&[0x42, 0x4D]) {
256        return Some("BMP detected");
257    }
258
259    None
260}