kbparse_lib/
keybag.rs

1type BoxedError = Box<dyn std::error::Error>;
2
3/// Represents a individual class key
4///
5/// Each class key is tied to a specified security level
6///
7/// See: https://support.apple.com/guide/security/keychain-data-protection-secb0694df1a/1/web/1
8#[derive(Default)]
9pub struct Keybagv5ClassKey {
10    pub uuid: Keybagv5Item,
11    pub class: Keybagv5Item,
12    pub key_type: Keybagv5Item,
13    pub wrap: Keybagv5Item,
14    pub wrapped_key: Keybagv5Item,
15    pub pbky: Option<Keybagv5Item>,
16}
17impl std::fmt::Debug for Keybagv5ClassKey {
18    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
19        f.debug_struct("Keybagv5Item")
20            .field("UUID", &self.uuid)
21            .field("Key Class", &self.class)
22            .field("Key Type", &self.key_type)
23            .field("Wrap", &self.wrap)
24            .field("Wrapped Key", &self.wrapped_key)
25            .field("PBKY", &self.pbky)
26            .finish()
27    }
28}
29
30/// Keybag Metadata
31///
32/// This data is always comes after the bag type and version
33#[derive(Default)]
34pub struct Keybagv5Metadata {
35    pub uuid: Keybagv5Item,
36    pub hmac: Keybagv5Item,
37    pub wrap: Keybagv5Item,
38    pub salt: Keybagv5Item,
39    pub iter: Keybagv5Item,
40    pub grce: Keybagv5Item,
41    pub cfgf: Keybagv5Item,
42    pub tkmt: Keybagv5Item,
43    pub usid: Keybagv5Item,
44    pub grid: Keybagv5Item,
45}
46impl std::fmt::Debug for Keybagv5Metadata {
47    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
48        f.debug_struct("Keybagv5Item")
49            .field("UUID", &self.uuid)
50            .field("HMAC", &self.hmac)
51            .field("WRAP", &self.wrap)
52            .field("SALT", &self.salt)
53            .field("ITER", &self.iter)
54            .field("GRCE", &self.grce)
55            .field("CFGF", &self.cfgf)
56            .field("TKMT", &self.tkmt)
57            .field("USID", &self.usid)
58            .field("GRID", &self.grid)
59            .finish()
60    }
61}
62
63/// Tag agnostic representation of a singular item within the Keybag.
64/// This is the lowest level struct in the keybag hierarchy and thus the data
65/// is kept in a mostly raw state.
66#[derive(Default, Clone)]
67pub struct Keybagv5Item {
68    /// String representation of what type of data is in the item
69    pub tag: String,
70    /// Byte length of [data]
71    pub len: u32,
72    /// Raw hex bytes of keybag item
73    pub data: Vec<u8>,
74}
75
76impl Keybagv5Item {
77    fn data_as_u32(&self) -> Result<u32, BoxedError> {
78        if self.len > 4 {
79            // Too large
80            Err(format!("Data length [{}] is too large to convert to u32", self.len).into())
81        } else {
82            let tmp = self.data.as_slice();
83            Ok(u32::from_be_bytes(tmp.try_into()?))
84        }
85    }
86}
87
88impl std::fmt::Debug for Keybagv5Item {
89    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
90        f.debug_struct("Keybagv5Item")
91            .field("tag", &self.tag)
92            .field("len", &format_args!("{} ({:#02X?})", &self.len, &self.len))
93            .field("data", &format_args!("{:02X?}", &self.data))
94            .finish()
95    }
96}
97
98const KB_TAG_LEN: usize = 4;
99const KB_SZ_LEN: usize = KB_TAG_LEN;
100const KB_EXCLUDED_LEN: usize = 36;
101
102/// Structure defining the Apple Keybag Version 5
103///
104/// The Keybag length is the length from the DATA tag, which describes the total
105/// legnth of what Apple considers the Keybag. It != Total File Size. It doesn't
106/// consider 36 bytes, of the total data:
107///      data_tag(4) + data_len(4) + sig_tag(4) + sig_len(4) + sig(20)
108#[derive(Default)]
109pub struct Keybagv5 {
110    pos: usize,
111    pub len: u32,
112    pub kb_type: Keybagv5Item,
113    pub kb_vers: Keybagv5Item,
114    pub metadata: Keybagv5Metadata,
115    pub class_keys: Vec<Keybagv5ClassKey>,
116    pub sig: Keybagv5Item,
117}
118
119impl Keybagv5 {
120    pub fn parse(raw: &[u8]) -> Result<Keybagv5, BoxedError> {
121        // Create a base default Keybag
122        let mut kb = Keybagv5::default();
123
124        // First tag should always be DATA
125        // Only pull the tag and len, the actual
126        // data is the rest of the bag
127        let tag = kb.parse_tag(raw)?;
128        kb.pos += KB_TAG_LEN;
129
130        // Confirm 'DATA' is first 4 bytes
131        // Using this to essentially say "OK this is a Apple keybag"
132        match "DATA" == tag {
133            true => {
134                kb.len = kb.parse_tag_len(raw)?;
135                kb.pos += KB_SZ_LEN;
136            }
137            false => return Err("DATA tag not found in bytes provided".into()),
138        }
139
140        kb.kb_vers = kb.parse_item(raw)?;
141        if kb.get_vers()? != 5 {
142            let msg = format!(
143                "Only Keybag version 5 supported. Version {} found...",
144                kb.get_vers()?
145            );
146            return Err(msg.into());
147        }
148
149        kb.kb_type = kb.parse_item(raw)?;
150        // TODO confirm bag type?
151
152        // Parse Metadata
153        let item_meta_uuid = kb.parse_item(raw)?;
154        kb.metadata = Keybagv5Metadata {
155            uuid: item_meta_uuid,
156            hmac: kb.parse_item(raw)?,
157            wrap: kb.parse_item(raw)?,
158            salt: kb.parse_item(raw)?,
159            iter: kb.parse_item(raw)?,
160            grce: kb.parse_item(raw)?,
161            cfgf: kb.parse_item(raw)?,
162            tkmt: kb.parse_item(raw)?,
163            usid: kb.parse_item(raw)?,
164            grid: kb.parse_item(raw)?,
165        };
166
167        // Parse class keys
168        // `pos` is the read position within the raw data. When the `DATA`
169        // tag was parsed and its `len` retrieved, that length doesn't account for
170        // the 8 bytes tht had to be parsed to get there but the _keybag length_
171        // accounts for all bytes in the file. The conditional needs to account
172        // for these 8 bytes to be accurate
173        while kb.pos - (KB_TAG_LEN + KB_SZ_LEN) != kb.get_len() {
174            let ck = kb.parse_key_class(raw)?;
175            kb.class_keys.push(ck);
176        }
177
178        // Get Signature
179        kb.sig = kb.parse_item(raw)?;
180
181        Ok(kb)
182    }
183
184    #[inline(always)]
185    /// Returns the total size of the parsed user keybag
186    pub fn get_len(&self) -> usize {
187        self.len as usize
188    }
189
190    /// Returns the version of the parsed keybag as a u32
191    pub fn get_vers(&self) -> Result<u32, BoxedError> {
192        self.kb_vers.data_as_u32()
193    }
194
195    /// Returns the type of the parsed keybag as a u32
196    /// <https://support.apple.com/guide/security/keybags-for-data-protection-sec6483d5760/web>
197    pub fn get_type(&self) -> Result<u32, BoxedError> {
198        self.kb_type.data_as_u32()
199    }
200
201    // A wrapper around parse_tag() to express intent
202    #[inline(always)]
203    fn peek_tag<'a>(&'a self, raw: &'a [u8]) -> Result<String, BoxedError> {
204        self.parse_tag(raw)
205    }
206
207    fn parse_tag<'a>(&'a self, raw: &'a [u8]) -> Result<String, BoxedError> {
208        if self.pos + KB_TAG_LEN < KB_EXCLUDED_LEN + self.get_len() {
209            let tag = std::str::from_utf8(&raw[self.pos..(self.pos + KB_TAG_LEN)])?;
210            Ok(tag.to_owned())
211        } else {
212            Err("Number of bytes requested larger than Keybag size".into())
213        }
214    }
215
216    fn parse_tag_len<'a>(&'a self, raw: &'a [u8]) -> Result<u32, BoxedError> {
217        if self.pos + KB_TAG_LEN < KB_EXCLUDED_LEN + self.get_len() {
218            Ok(u32::from_be_bytes(
219                raw[self.pos..(self.pos + KB_SZ_LEN)].try_into()?,
220            ))
221        } else {
222            Err("Number of bytes requested larger than Keybag size".into())
223        }
224    }
225
226    fn parse_item(&mut self, raw: &[u8]) -> Result<Keybagv5Item, BoxedError> {
227        // println!("parse_item: pos:{:#?}", self.pos);
228        let tag = self.parse_tag(raw)?;
229        self.pos += KB_TAG_LEN;
230        let tlen = self.parse_tag_len(raw)?;
231        self.pos += KB_SZ_LEN;
232
233        // Need to be <=, as last item should read up to the final byte
234        if (self.pos + tlen as usize) <= KB_EXCLUDED_LEN + self.get_len() {
235            let bytes = &raw[self.pos..(self.pos + tlen as usize)];
236            self.pos += tlen as usize;
237
238            // TODO
239            // Maybe find a way to not require the copy??
240            Ok(Keybagv5Item {
241                tag: tag.to_owned(),
242                len: tlen,
243                data: bytes.to_vec(),
244            })
245        } else {
246            Err("Number of bytes requested larger than Keybag size".into())
247        }
248    }
249
250    fn parse_key_class(&mut self, raw: &[u8]) -> Result<Keybagv5ClassKey, BoxedError> {
251        Ok(Keybagv5ClassKey {
252            uuid: self.parse_item(raw)?,
253            class: self.parse_item(raw)?,
254            key_type: self.parse_item(raw)?,
255            wrap: self.parse_item(raw)?,
256            wrapped_key: self.parse_item(raw)?,
257            // Not all class key items have a pbky item
258            pbky: if "PBKY" == self.peek_tag(raw)? {
259                Some(self.parse_item(raw)?)
260            } else {
261                None
262            },
263        })
264    }
265}
266
267impl std::fmt::Debug for Keybagv5 {
268    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
269        f.debug_struct("Keybag")
270            .field("pos", &self.pos)
271            .field("len", &self.len)
272            .field("KB Version", &self.kb_vers)
273            .field("KB Type", &self.kb_type)
274            .field("KB Metadata", &self.metadata)
275            .field("KB Class Keys", &self.class_keys)
276            .field("KB signature", &self.sig)
277            .finish()
278    }
279}
280
281#[cfg(test)]
282mod tests {
283
284    #[test]
285    fn not_keybag_file() {
286        let test_file_data = [
287            0x2F, 0xCD, 0xCE, 0xDB, 0xE9, 0x99, 0x4B, 0xA4, 0xAD, 0x38, 0x9C, 0x59, 0x31, 0x25,
288            0x43, 0x5F,
289        ];
290
291        assert_eq!(true, super::Keybagv5::parse(&test_file_data).is_err());
292    }
293
294    #[test]
295    fn bounds_check_parse_tag() {
296        let mut bad_kb = super::Keybagv5::default();
297        // parse_tag account for 36 bytes not considered part of length
298        // negate that by setting this to 36
299        bad_kb.pos = 36;
300        bad_kb.len = 3;
301
302        let test_file_data = [0x2F, 0xCD, 0xCE];
303
304        assert_eq!(true, bad_kb.parse_tag(&test_file_data).is_err());
305    }
306
307    #[test]
308    fn bounds_check_parse_tag_len() {
309        let mut bad_kb = super::Keybagv5::default();
310        // parse_tag account for 36 bytes not considered part of length
311        // negate that by setting this to 36
312        bad_kb.pos = 36;
313        bad_kb.len = 3;
314
315        let test_file_data = [0x2F, 0xCD, 0xCE];
316
317        assert_eq!(true, bad_kb.parse_tag_len(&test_file_data).is_err());
318    }
319
320    #[test]
321    fn bounds_check_parse_item_bad_length() {
322        let mut bad_kb = super::Keybagv5::default();
323        // parse_tag account for 36 bytes not considered part of length
324        // negate that by setting this to 36
325        bad_kb.pos = 36;
326        bad_kb.len = 12;
327
328        // 0x00 * 36 to account for fake pos
329        // Fake tag of 'DATA'
330        // Size should be 0xff000000 (4278190080)
331        // parse_item should catch the bad length
332        let test_file_data = [
333            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
334            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
335            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x41, 0x54, 0x41, 0xFF, 0x00,
336            0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
337        ];
338
339        assert_eq!(true, bad_kb.parse_item(&test_file_data).is_err());
340    }
341}