1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum KeybagTag {
24 Unknown = 0x00,
25 WrappingKey = 0x01,
26 VolumeKey = 0x02,
27 VolumeUnlockRecords = 0x03,
28 VolumePassphraseHint = 0x04,
29 UserPayload = 0xf8,
30}
31
32impl KeybagTag {
33 #[must_use]
35 pub fn from_u16(tag: u16) -> Self {
36 match tag {
37 0x01 => Self::WrappingKey,
38 0x02 => Self::VolumeKey,
39 0x03 => Self::VolumeUnlockRecords,
40 0x04 => Self::VolumePassphraseHint,
41 0xf8 => Self::UserPayload,
42 _ => Self::Unknown,
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
49#[non_exhaustive]
50pub struct EncryptionState {
51 pub encrypted: bool,
52 pub tags_present: Vec<KeybagTag>,
53 pub has_passphrase_hint: bool,
54 pub unknown_tags: Vec<(u16, u64)>,
58}
59
60const KL_NKEYS: usize = 2; const KL_ENTRIES_OFF: usize = 16; const KE_TAG: usize = 16; const KE_KEYLEN: usize = 18; const KE_HEADER_LEN: usize = 24; const MAX_KEYBAG_ENTRIES: usize = 4096;
68
69pub fn read_keybag(data: &[u8]) -> crate::Result<EncryptionState> {
78 let nkeys = (crate::bytes::le_u16(data, KL_NKEYS) as usize).min(MAX_KEYBAG_ENTRIES);
79 let mut tags_present = Vec::new();
80 let mut unknown_tags = Vec::new();
81 let mut off = KL_ENTRIES_OFF;
82 for _ in 0..nkeys {
83 if off + KE_HEADER_LEN > data.len() {
85 break;
86 }
87 let raw_tag = crate::bytes::le_u16(data, off + KE_TAG);
88 let tag = KeybagTag::from_u16(raw_tag);
89 let keylen = crate::bytes::le_u16(data, off + KE_KEYLEN) as usize;
90 if tag == KeybagTag::Unknown {
91 unknown_tags.push((raw_tag, off as u64));
92 }
93 if !tags_present.contains(&tag) {
94 tags_present.push(tag);
95 }
96 let entry_len = (KE_HEADER_LEN + keylen + 15) & !15;
98 off += entry_len.max(16);
99 }
100 let has_passphrase_hint = tags_present.contains(&KeybagTag::VolumePassphraseHint);
101 let encrypted = tags_present.iter().any(|t| {
104 matches!(
105 t,
106 KeybagTag::WrappingKey | KeybagTag::VolumeKey | KeybagTag::VolumeUnlockRecords
107 )
108 });
109 Ok(EncryptionState {
110 encrypted,
111 tags_present,
112 has_passphrase_hint,
113 unknown_tags,
114 })
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn keybag(entries: &[(u16, usize)]) -> Vec<u8> {
126 let mut data = vec![0u8; 16];
127 data[0..2].copy_from_slice(&1u16.to_le_bytes()); data[2..4].copy_from_slice(&(entries.len() as u16).to_le_bytes()); for &(tag, keylen) in entries {
130 let mut e = vec![0u8; 24 + keylen];
131 e[16..18].copy_from_slice(&tag.to_le_bytes()); e[18..20].copy_from_slice(&(keylen as u16).to_le_bytes()); let padded = (e.len() + 15) & !15; e.resize(padded, 0);
135 data.extend_from_slice(&e);
136 }
137 let nbytes = data.len() as u32;
138 data[4..8].copy_from_slice(&nbytes.to_le_bytes()); data
140 }
141
142 #[test]
143 fn reads_volume_key_and_hint_tags() {
144 let kb = keybag(&[(0x02, 32), (0x04, 8)]);
146 let st = read_keybag(&kb).expect("parse keybag");
147 assert!(st.encrypted, "a keybag with a volume key is encrypted");
148 assert!(st.tags_present.contains(&KeybagTag::VolumeKey));
149 assert!(st.tags_present.contains(&KeybagTag::VolumePassphraseHint));
150 assert!(st.has_passphrase_hint);
151 }
152
153 #[test]
154 fn from_u16_maps_every_known_tag() {
155 assert_eq!(KeybagTag::from_u16(0x01), KeybagTag::WrappingKey);
158 assert_eq!(KeybagTag::from_u16(0x02), KeybagTag::VolumeKey);
159 assert_eq!(KeybagTag::from_u16(0x03), KeybagTag::VolumeUnlockRecords);
160 assert_eq!(KeybagTag::from_u16(0x04), KeybagTag::VolumePassphraseHint);
161 assert_eq!(KeybagTag::from_u16(0xf8), KeybagTag::UserPayload);
162 assert_eq!(KeybagTag::from_u16(0x99), KeybagTag::Unknown);
163 }
164
165 #[test]
166 fn wrapping_key_and_unlock_records_are_encrypted() {
167 let kb = keybag(&[(0x01, 32), (0x03, 16)]);
170 let st = read_keybag(&kb).expect("parse keybag");
171 assert!(st.encrypted, "wrapping-key / unlock-records ⇒ encrypted");
172 assert!(st.tags_present.contains(&KeybagTag::WrappingKey));
173 assert!(st.tags_present.contains(&KeybagTag::VolumeUnlockRecords));
174 }
175
176 #[test]
177 fn empty_keybag_reports_not_encrypted() {
178 let kb = keybag(&[]);
179 let st = read_keybag(&kb).expect("parse empty keybag");
180 assert!(!st.encrypted);
181 assert!(st.tags_present.is_empty());
182 assert!(!st.has_passphrase_hint);
183 }
184
185 #[test]
186 fn unknown_tag_maps_to_unknown_and_records_raw_value() {
187 let kb = keybag(&[(0x55, 4)]);
190 let st = read_keybag(&kb).expect("parse keybag");
191 assert!(st.tags_present.contains(&KeybagTag::Unknown));
192 assert_eq!(st.unknown_tags, vec![(0x55u16, 16u64)]);
193 }
194
195 #[test]
196 fn header_claiming_more_entries_than_the_blob_stops_early() {
197 let mut data = vec![0u8; 24];
201 data[0..2].copy_from_slice(&1u16.to_le_bytes()); data[2..4].copy_from_slice(&4u16.to_le_bytes()); let st = read_keybag(&data).expect("parse truncated keybag");
204 assert!(st.tags_present.is_empty(), "no entry fits → nothing parsed");
205 }
206}