#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeybagTag {
Unknown = 0x00,
WrappingKey = 0x01,
VolumeKey = 0x02,
VolumeUnlockRecords = 0x03,
VolumePassphraseHint = 0x04,
UserPayload = 0xf8,
}
impl KeybagTag {
#[must_use]
pub fn from_u16(tag: u16) -> Self {
match tag {
0x01 => Self::WrappingKey,
0x02 => Self::VolumeKey,
0x03 => Self::VolumeUnlockRecords,
0x04 => Self::VolumePassphraseHint,
0xf8 => Self::UserPayload,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct EncryptionState {
pub encrypted: bool,
pub tags_present: Vec<KeybagTag>,
pub has_passphrase_hint: bool,
pub unknown_tags: Vec<(u16, u64)>,
}
const 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;
pub fn read_keybag(data: &[u8]) -> crate::Result<EncryptionState> {
let nkeys = (crate::bytes::le_u16(data, KL_NKEYS) as usize).min(MAX_KEYBAG_ENTRIES);
let mut tags_present = Vec::new();
let mut unknown_tags = Vec::new();
let mut off = KL_ENTRIES_OFF;
for _ in 0..nkeys {
if off + KE_HEADER_LEN > data.len() {
break;
}
let raw_tag = crate::bytes::le_u16(data, off + KE_TAG);
let tag = KeybagTag::from_u16(raw_tag);
let keylen = crate::bytes::le_u16(data, off + KE_KEYLEN) as usize;
if tag == KeybagTag::Unknown {
unknown_tags.push((raw_tag, off as u64));
}
if !tags_present.contains(&tag) {
tags_present.push(tag);
}
let entry_len = (KE_HEADER_LEN + keylen + 15) & !15;
off += entry_len.max(16);
}
let has_passphrase_hint = tags_present.contains(&KeybagTag::VolumePassphraseHint);
let encrypted = tags_present.iter().any(|t| {
matches!(
t,
KeybagTag::WrappingKey | KeybagTag::VolumeKey | KeybagTag::VolumeUnlockRecords
)
});
Ok(EncryptionState {
encrypted,
tags_present,
has_passphrase_hint,
unknown_tags,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn keybag(entries: &[(u16, usize)]) -> Vec<u8> {
let mut data = vec![0u8; 16];
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 {
let mut e = vec![0u8; 24 + keylen];
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);
data.extend_from_slice(&e);
}
let nbytes = data.len() as u32;
data[4..8].copy_from_slice(&nbytes.to_le_bytes()); data
}
#[test]
fn reads_volume_key_and_hint_tags() {
let kb = keybag(&[(0x02, 32), (0x04, 8)]);
let st = read_keybag(&kb).expect("parse keybag");
assert!(st.encrypted, "a keybag with a volume key is encrypted");
assert!(st.tags_present.contains(&KeybagTag::VolumeKey));
assert!(st.tags_present.contains(&KeybagTag::VolumePassphraseHint));
assert!(st.has_passphrase_hint);
}
#[test]
fn empty_keybag_reports_not_encrypted() {
let kb = keybag(&[]);
let st = read_keybag(&kb).expect("parse empty keybag");
assert!(!st.encrypted);
assert!(st.tags_present.is_empty());
assert!(!st.has_passphrase_hint);
}
#[test]
fn unknown_tag_maps_to_unknown_and_records_raw_value() {
let kb = keybag(&[(0x55, 4)]);
let st = read_keybag(&kb).expect("parse keybag");
assert!(st.tags_present.contains(&KeybagTag::Unknown));
assert_eq!(st.unknown_tags, vec![(0x55u16, 16u64)]);
}
}