Skip to main content

filevault/
context.rs

1//! Targeted extraction from the decrypted CoreStorage metadata.
2//!
3//! The `com.apple.corestorage.lvf.encryption.context` XML plist carries the
4//! password protector's wrapped keys; block type `0x001a` carries the logical
5//! volume's `familyUUID` (the tweak-key input) and its size. A full plist parser
6//! is unnecessary and would be more attack surface: the values we need appear as
7//! `<key>NAME</key><data ID=..>BASE64</data>` and
8//! `<key>NAME</key><string ID=..>VALUE</string>` pairs, so bounded substring
9//! extraction is used (RESEARCH.md explicitly sanctions this). Every scan is
10//! bounds-checked and length-capped.
11
12use base64::engine::general_purpose::STANDARD as BASE64;
13use base64::Engine;
14
15use crate::error::FileVaultError;
16
17/// The password protector's wrapped-key material and PBKDF2 parameters.
18#[derive(Debug, Clone)]
19pub struct EncryptionContext {
20    /// PBKDF2 salt (16 bytes) from the `PassphraseWrappedKEKStruct` (offset 8).
21    pub salt: [u8; 16],
22    /// PBKDF2 iteration count. NOTE: on the fvdetest ground truth this reads
23    /// from struct offset 168 (RESEARCH.md's "172" is one field further on and
24    /// reads 1); offset 168 yields the verified 90506.
25    pub iterations: u32,
26    /// Wrapped KEK (24 bytes) from `PassphraseWrappedKEKStruct` offset 32.
27    pub wrapped_kek: [u8; 24],
28    /// Wrapped volume master key (24 bytes) from the `AES-XTS`
29    /// `KEKWrappedVolumeKeyStruct` offset 8.
30    pub wrapped_vmk: [u8; 24],
31    /// Protectors (crypto users) present, by `UserType`.
32    pub protectors: Vec<Protector>,
33    /// LV conversion status (`Complete` / `Converting` / …), if present.
34    pub conversion_status: Option<String>,
35}
36
37/// A CoreStorage crypto-user (protector) classified by its `UserType` code.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Protector {
40    /// Raw `UserType` code (e.g. `0x10000001` = password).
41    pub user_type: u32,
42    /// Human-readable classification of `user_type`.
43    pub kind: ProtectorKind,
44}
45
46/// Classification of a CoreStorage crypto-user `UserType`.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ProtectorKind {
49    /// Password (`0x10000001`).
50    Password,
51    /// Recovery / personal recovery key (`0x10000009`).
52    Recovery,
53    /// Institutional / keychain-backed key.
54    Institutional,
55    /// A crypto-user whose `UserType` is not one of the known codes; carries the
56    /// raw code so it is never silently lost.
57    Unknown,
58}
59
60impl ProtectorKind {
61    /// Classify a raw `UserType` code.
62    #[must_use]
63    pub fn from_user_type(user_type: u32) -> Self {
64        match user_type {
65            0x1000_0001 => ProtectorKind::Password,
66            0x1000_0009 => ProtectorKind::Recovery,
67            0x1000_0005 | 0x1000_000f => ProtectorKind::Institutional,
68            _ => ProtectorKind::Unknown,
69        }
70    }
71
72    /// A stable lowercase label.
73    #[must_use]
74    pub fn label(self) -> &'static str {
75        match self {
76            ProtectorKind::Password => "password",
77            ProtectorKind::Recovery => "recovery",
78            ProtectorKind::Institutional => "institutional",
79            ProtectorKind::Unknown => "unknown",
80        }
81    }
82}
83
84/// Offsets inside the `PassphraseWrappedKEKStruct` (see RESEARCH.md + ground
85/// truth: iterations live at 168, not 172).
86const PW_SALT_OFFSET: usize = 8;
87const PW_WRAPPED_KEK_OFFSET: usize = 32;
88const PW_ITERATIONS_OFFSET: usize = 168;
89/// Offset inside the `KEKWrappedVolumeKeyStruct` of the wrapped VMK.
90const KEK_WRAPPED_VMK_OFFSET: usize = 8;
91/// Largest base64 blob we will decode from the context (guards allocation).
92const MAX_BASE64_BLOB: usize = 4096;
93
94/// Find `<key>NAME</key><data ID=..>BASE64</data>` and return the decoded bytes
95/// of the first match. Bounds-checked; caps the encoded length.
96fn extract_data(metadata: &[u8], key_name: &str) -> Option<Vec<u8>> {
97    let value = extract_tagged(metadata, key_name, b"<data ID=", b"</data>")?;
98    let trimmed: Vec<u8> = value
99        .iter()
100        .copied()
101        .filter(|b| !b.is_ascii_whitespace())
102        .collect();
103    if trimmed.len() > MAX_BASE64_BLOB {
104        return None;
105    }
106    BASE64.decode(&trimmed).ok()
107}
108
109/// Find `<key>NAME</key><string ID=..>VALUE</string>` and return the string.
110fn extract_string(metadata: &[u8], key_name: &str) -> Option<String> {
111    let value = extract_tagged(metadata, key_name, b"<string ID=", b"</string>")?;
112    String::from_utf8(value).ok()
113}
114
115/// Find `<key>NAME</key><integer ...>0xHHHH</integer>` from position `from` and
116/// return (value, index just past the match).
117fn extract_integer_from(metadata: &[u8], key_name: &str, from: usize) -> Option<(u32, usize)> {
118    let (value, end) = extract_tagged_from(metadata, key_name, b"<integer", b"</integer>", from)?;
119    // `extract_tagged_from` already returns the inner text (e.g. `0x10000001`).
120    let text = std::str::from_utf8(&value).ok()?.trim();
121    let parsed = if let Some(hex) = text.strip_prefix("0x") {
122        u32::from_str_radix(hex, 16).ok()?
123    } else {
124        text.parse::<u32>().ok()?
125    };
126    Some((parsed, end))
127}
128
129/// Core substring machine: locate `<key>NAME</key>` then the following
130/// `open_tag` … its `>` … `close_tag`, returning the inner bytes.
131fn extract_tagged(
132    metadata: &[u8],
133    key_name: &str,
134    open_tag: &[u8],
135    close_tag: &[u8],
136) -> Option<Vec<u8>> {
137    extract_tagged_from(metadata, key_name, open_tag, close_tag, 0).map(|(v, _)| v)
138}
139
140fn extract_tagged_from(
141    metadata: &[u8],
142    key_name: &str,
143    open_tag: &[u8],
144    close_tag: &[u8],
145    from: usize,
146) -> Option<(Vec<u8>, usize)> {
147    let mut key = Vec::with_capacity(key_name.len() + 11);
148    key.extend_from_slice(b"<key>");
149    key.extend_from_slice(key_name.as_bytes());
150    key.extend_from_slice(b"</key>");
151
152    let start = from.checked_add(find_sub(metadata.get(from..)?, &key)?)?;
153    let after_key = start.checked_add(key.len())?;
154    let open_rel = find_sub(metadata.get(after_key..)?, open_tag)?;
155    let open_abs = after_key.checked_add(open_rel)?;
156    // Advance to the '>' that closes the opening tag.
157    let gt_rel = find_sub(metadata.get(open_abs..)?, b">")?;
158    let inner_start = open_abs.checked_add(gt_rel)?.checked_add(1)?;
159    let close_rel = find_sub(metadata.get(inner_start..)?, close_tag)?;
160    let inner_end = inner_start.checked_add(close_rel)?;
161    let inner = metadata.get(inner_start..inner_end)?.to_vec();
162    Some((inner, inner_end.checked_add(close_tag.len())?))
163}
164
165/// Bounded substring search (no regex; needle from a `&'static`/small buffer).
166fn find_sub(haystack: &[u8], needle: &[u8]) -> Option<usize> {
167    if needle.is_empty() || needle.len() > haystack.len() {
168        return None;
169    }
170    haystack
171        .windows(needle.len())
172        .position(|window| window == needle)
173}
174
175/// Enumerate the `CryptoUsers` protectors by scanning every `UserType` integer.
176fn extract_protectors(metadata: &[u8]) -> Vec<Protector> {
177    let mut out = Vec::new();
178    let mut cursor = 0;
179    while let Some((user_type, end)) = extract_integer_from(metadata, "UserType", cursor) {
180        out.push(Protector {
181            user_type,
182            kind: ProtectorKind::from_user_type(user_type),
183        });
184        cursor = end;
185        if out.len() >= 64 {
186            break; // cov:unreachable: real contexts carry a handful of crypto users
187        }
188    }
189    out
190}
191
192impl EncryptionContext {
193    /// Extract the encryption context from decrypted metadata.
194    ///
195    /// # Errors
196    /// [`FileVaultError::MetadataStructureMissing`] if the passphrase-wrapped or
197    /// KEK-wrapped struct is absent; [`FileVaultError::Base64`] on a bad blob;
198    /// [`FileVaultError::OutOfRange`] if a struct is too short.
199    pub fn extract(metadata: &[u8]) -> Result<Self, FileVaultError> {
200        let pw = extract_data(metadata, "PassphraseWrappedKEKStruct").ok_or(
201            FileVaultError::MetadataStructureMissing {
202                what: "PassphraseWrappedKEKStruct",
203            },
204        )?;
205
206        let salt = slice16(&pw, PW_SALT_OFFSET).ok_or(FileVaultError::OutOfRange {
207            what: "PassphraseWrappedKEKStruct salt",
208        })?;
209        let wrapped_kek =
210            slice24(&pw, PW_WRAPPED_KEK_OFFSET).ok_or(FileVaultError::OutOfRange {
211                what: "PassphraseWrappedKEKStruct wrapped KEK",
212            })?;
213        let iterations =
214            read_u32_le(&pw, PW_ITERATIONS_OFFSET).ok_or(FileVaultError::OutOfRange {
215                what: "PassphraseWrappedKEKStruct iterations",
216            })?;
217
218        // The AES-XTS KEKWrappedVolumeKeyStruct is the one that wraps the real
219        // volume key; the `BlockAlgorithm == None` sibling is a placeholder.
220        let vmk_struct = extract_kek_wrapped_aes_xts(metadata).ok_or(
221            FileVaultError::MetadataStructureMissing {
222                what: "AES-XTS KEKWrappedVolumeKeyStruct",
223            },
224        )?;
225        let wrapped_vmk =
226            slice24(&vmk_struct, KEK_WRAPPED_VMK_OFFSET).ok_or(FileVaultError::OutOfRange {
227                what: "KEKWrappedVolumeKeyStruct wrapped VMK",
228            })?;
229
230        Ok(EncryptionContext {
231            salt,
232            iterations,
233            wrapped_kek,
234            wrapped_vmk,
235            protectors: extract_protectors(metadata),
236            conversion_status: extract_string(metadata, "ConversionStatus"),
237        })
238    }
239}
240
241/// Extract the `KEKWrappedVolumeKeyStruct` whose preceding `BlockAlgorithm` is
242/// `AES-XTS`. Contexts list a `None` algorithm first, then `AES-XTS`; scan for
243/// the `AES-XTS` marker and take the next wrapped-key blob after it.
244fn extract_kek_wrapped_aes_xts(metadata: &[u8]) -> Option<Vec<u8>> {
245    let marker = b"<string ID=";
246    // Find the BlockAlgorithm key whose value is AES-XTS.
247    let mut cursor = 0;
248    loop {
249        let (algo, end) =
250            extract_tagged_from(metadata, "BlockAlgorithm", marker, b"</string>", cursor)?;
251        if algo == b"AES-XTS" {
252            return extract_data(metadata.get(end..)?, "KEKWrappedVolumeKeyStruct");
253        }
254        cursor = end;
255    }
256}
257
258/// The LV logical size (bytes) and family UUID, from the decrypted metadata.
259#[derive(Debug, Clone)]
260pub struct LogicalVolumeInfo {
261    /// LV logical size in bytes.
262    pub size: u64,
263    /// Family UUID string (tweak-key input), canonical form.
264    pub family_uuid: String,
265    /// LV identifier UUID string, if present.
266    pub lv_identifier: Option<String>,
267    /// LV name, if present.
268    pub name: Option<String>,
269}
270
271impl LogicalVolumeInfo {
272    /// Extract the LV info from decrypted metadata.
273    ///
274    /// # Errors
275    /// [`FileVaultError::MetadataStructureMissing`] if the family UUID is absent.
276    pub fn extract(metadata: &[u8]) -> Result<Self, FileVaultError> {
277        let family_uuid = extract_string(metadata, "com.apple.corestorage.lv.familyUUID").ok_or(
278            FileVaultError::MetadataStructureMissing {
279                what: "com.apple.corestorage.lv.familyUUID",
280            },
281        )?;
282        let size = extract_lv_size(metadata).unwrap_or(0);
283        let lv_identifier = extract_string(metadata, "com.apple.corestorage.lv.uuid");
284        let name = extract_string(metadata, "com.apple.corestorage.lv.name");
285        Ok(LogicalVolumeInfo {
286            size,
287            family_uuid,
288            lv_identifier,
289            name,
290        })
291    }
292}
293
294/// Extract the LV size from the `0x001a` volume record. The size appears as a
295/// little-endian u64 at a fixed offset in the record following the family UUID
296/// marker; if the structural read fails, `None` (the size is then reported as
297/// unknown rather than fabricated).
298fn extract_lv_size(metadata: &[u8]) -> Option<u64> {
299    // The LV size is carried as a plist integer `com.apple.corestorage.lv.size`
300    // when present; fall back to None otherwise.
301    if let Some((value, _)) = extract_u64_integer(metadata, "com.apple.corestorage.lv.size") {
302        return Some(value);
303    }
304    None
305}
306
307/// Like `extract_integer_from` but for a u64 plist integer.
308fn extract_u64_integer(metadata: &[u8], key_name: &str) -> Option<(u64, usize)> {
309    let (value, end) = extract_tagged_from(metadata, key_name, b"<integer", b"</integer>", 0)?;
310    let text = std::str::from_utf8(&value).ok()?.trim();
311    let parsed = if let Some(hex) = text.strip_prefix("0x") {
312        u64::from_str_radix(hex, 16).ok()?
313    } else {
314        text.parse::<u64>().ok()?
315    };
316    Some((parsed, end))
317}
318
319fn slice16(data: &[u8], off: usize) -> Option<[u8; 16]> {
320    let s = data.get(off..off.checked_add(16)?)?;
321    let mut out = [0u8; 16];
322    out.copy_from_slice(s);
323    Some(out)
324}
325
326fn slice24(data: &[u8], off: usize) -> Option<[u8; 24]> {
327    let s = data.get(off..off.checked_add(24)?)?;
328    let mut out = [0u8; 24];
329    out.copy_from_slice(s);
330    Some(out)
331}
332
333fn read_u32_le(data: &[u8], off: usize) -> Option<u32> {
334    let s = data.get(off..off.checked_add(4)?)?;
335    Some(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    fn hex(s: &str) -> Vec<u8> {
343        (0..s.len() / 2)
344            .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap())
345            .collect()
346    }
347
348    /// Build a 284-byte PassphraseWrappedKEKStruct with the ground-truth salt,
349    /// wrapped KEK and iteration count at their real offsets (8 / 32 / 168).
350    fn passphrase_struct() -> Vec<u8> {
351        let mut s = vec![0u8; 284];
352        s[PW_SALT_OFFSET..PW_SALT_OFFSET + 16]
353            .copy_from_slice(&hex("9bfcf480e4d9ad0eddd9ac6f47b85955"));
354        s[PW_WRAPPED_KEK_OFFSET..PW_WRAPPED_KEK_OFFSET + 24]
355            .copy_from_slice(&hex("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1"));
356        s[PW_ITERATIONS_OFFSET..PW_ITERATIONS_OFFSET + 4].copy_from_slice(&90506u32.to_le_bytes());
357        s
358    }
359
360    /// Build a KEKWrappedVolumeKeyStruct with the wrapped VMK at offset 8.
361    fn kek_wrapped_struct(wrapped_vmk_hex: &str) -> Vec<u8> {
362        let mut s = vec![0u8; 256];
363        s[KEK_WRAPPED_VMK_OFFSET..KEK_WRAPPED_VMK_OFFSET + 24]
364            .copy_from_slice(&hex(wrapped_vmk_hex));
365        s
366    }
367
368    fn b64(bytes: &[u8]) -> String {
369        BASE64.encode(bytes)
370    }
371
372    fn build_context_plist() -> Vec<u8> {
373        let pw = b64(&passphrase_struct());
374        // The `None` algorithm sibling first, then AES-XTS with the real wrapped VMK.
375        let none_kek = b64(&kek_wrapped_struct(
376            "000000000000000000000000000000000000000000000000",
377        ));
378        let xts_kek = b64(&kek_wrapped_struct(
379            "9a5b30e99f902ed8e2f03989e5f9c1543ed60512aa1dc9d1",
380        ));
381        format!(
382            "<dict ID=\"0\"><key>CryptoUsers</key><array ID=\"2\">\
383             <dict ID=\"3\">\
384             <key>PassphraseWrappedKEKStruct</key><data ID=\"4\">{pw}</data>\
385             <key>UserType</key><integer size=\"32\" ID=\"6\">0x10000001</integer>\
386             <key>BlockAlgorithm</key><string ID=\"7\">None</string>\
387             <key>KEKWrappedVolumeKeyStruct</key><data ID=\"8\">{none_kek}</data>\
388             <key>BlockAlgorithm</key><string ID=\"9\">AES-XTS</string>\
389             <key>KEKWrappedVolumeKeyStruct</key><data ID=\"10\">{xts_kek}</data>\
390             </dict>\
391             <dict ID=\"11\">\
392             <key>UserType</key><integer size=\"32\" ID=\"12\">0x10000009</integer>\
393             </dict>\
394             </array>\
395             <key>ConversionStatus</key><string ID=\"20\">Complete</string></dict>"
396        )
397        .into_bytes()
398    }
399
400    #[test]
401    fn extracts_encryption_context() {
402        let meta = build_context_plist();
403        let ctx = EncryptionContext::extract(&meta).unwrap();
404        assert_eq!(
405            ctx.salt,
406            *b"\x9b\xfc\xf4\x80\xe4\xd9\xad\x0e\xdd\xd9\xac\x6f\x47\xb8\x59\x55"
407        );
408        assert_eq!(ctx.iterations, 90506);
409        assert_eq!(
410            ctx.wrapped_kek.to_vec(),
411            hex("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1")
412        );
413        // The AES-XTS wrapped VMK, not the None sibling.
414        assert_eq!(
415            ctx.wrapped_vmk.to_vec(),
416            hex("9a5b30e99f902ed8e2f03989e5f9c1543ed60512aa1dc9d1")
417        );
418        assert_eq!(ctx.conversion_status.as_deref(), Some("Complete"));
419    }
420
421    #[test]
422    fn enumerates_protectors() {
423        let meta = build_context_plist();
424        let ctx = EncryptionContext::extract(&meta).unwrap();
425        assert_eq!(ctx.protectors.len(), 2);
426        assert_eq!(ctx.protectors[0].kind, ProtectorKind::Password);
427        assert_eq!(ctx.protectors[0].user_type, 0x1000_0001);
428        assert_eq!(ctx.protectors[1].kind, ProtectorKind::Recovery);
429    }
430
431    #[test]
432    fn missing_passphrase_struct_is_error() {
433        let meta = b"<dict><key>Nothing</key></dict>".to_vec();
434        assert!(matches!(
435            EncryptionContext::extract(&meta),
436            Err(FileVaultError::MetadataStructureMissing {
437                what: "PassphraseWrappedKEKStruct"
438            })
439        ));
440    }
441
442    #[test]
443    fn missing_aes_xts_kek_is_error() {
444        let pw = b64(&passphrase_struct());
445        let meta = format!(
446            "<key>PassphraseWrappedKEKStruct</key><data ID=\"4\">{pw}</data>\
447             <key>BlockAlgorithm</key><string ID=\"9\">None</string>\
448             <key>KEKWrappedVolumeKeyStruct</key><data ID=\"10\">{}</data>",
449            b64(&kek_wrapped_struct(
450                "000000000000000000000000000000000000000000000000"
451            ))
452        )
453        .into_bytes();
454        assert!(matches!(
455            EncryptionContext::extract(&meta),
456            Err(FileVaultError::MetadataStructureMissing {
457                what: "AES-XTS KEKWrappedVolumeKeyStruct"
458            })
459        ));
460    }
461
462    #[test]
463    fn protector_kind_classification() {
464        assert_eq!(
465            ProtectorKind::from_user_type(0x1000_0001),
466            ProtectorKind::Password
467        );
468        assert_eq!(
469            ProtectorKind::from_user_type(0x1000_0009),
470            ProtectorKind::Recovery
471        );
472        assert_eq!(
473            ProtectorKind::from_user_type(0x1000_000f),
474            ProtectorKind::Institutional
475        );
476        assert_eq!(
477            ProtectorKind::from_user_type(0xdead),
478            ProtectorKind::Unknown
479        );
480        assert_eq!(ProtectorKind::Password.label(), "password");
481        assert_eq!(ProtectorKind::Recovery.label(), "recovery");
482        assert_eq!(ProtectorKind::Institutional.label(), "institutional");
483        assert_eq!(ProtectorKind::Unknown.label(), "unknown");
484    }
485
486    #[test]
487    fn extracts_logical_volume_info() {
488        let meta = b"<dict ID=\"0\">\
489            <key>com.apple.corestorage.lv.familyUUID</key><string ID=\"1\">1F01CA34-5F6C-4123-AC0C-B0A256889DB2</string>\
490            <key>com.apple.corestorage.lv.name</key><string ID=\"6\">TestLV</string>\
491            <key>com.apple.corestorage.lv.size</key><integer size=\"64\" ID=\"7\">0xa000000</integer>\
492            <key>com.apple.corestorage.lv.uuid</key><string ID=\"8\">420AF122-CF73-4A30-8B0A-A593A65FBEF5</string>\
493            </dict>"
494            .to_vec();
495        let lv = LogicalVolumeInfo::extract(&meta).unwrap();
496        assert_eq!(lv.family_uuid, "1F01CA34-5F6C-4123-AC0C-B0A256889DB2");
497        assert_eq!(lv.size, 167_772_160);
498        assert_eq!(lv.name.as_deref(), Some("TestLV"));
499        assert_eq!(
500            lv.lv_identifier.as_deref(),
501            Some("420AF122-CF73-4A30-8B0A-A593A65FBEF5")
502        );
503    }
504
505    #[test]
506    fn missing_family_uuid_is_error() {
507        let meta = b"<dict><key>other</key></dict>".to_vec();
508        assert!(matches!(
509            LogicalVolumeInfo::extract(&meta),
510            Err(FileVaultError::MetadataStructureMissing {
511                what: "com.apple.corestorage.lv.familyUUID"
512            })
513        ));
514    }
515
516    #[test]
517    fn lv_size_absent_is_zero_not_fabricated() {
518        let meta = b"<key>com.apple.corestorage.lv.familyUUID</key><string ID=\"1\">1F01CA34-5F6C-4123-AC0C-B0A256889DB2</string>".to_vec();
519        let lv = LogicalVolumeInfo::extract(&meta).unwrap();
520        assert_eq!(lv.size, 0);
521        assert!(lv.name.is_none());
522    }
523
524    #[test]
525    fn oversized_base64_blob_is_rejected() {
526        // A data blob longer than the cap decodes to None (guards allocation).
527        let big = "A".repeat(MAX_BASE64_BLOB + 4);
528        let meta = format!("<key>PassphraseWrappedKEKStruct</key><data ID=\"4\">{big}</data>")
529            .into_bytes();
530        assert!(matches!(
531            EncryptionContext::extract(&meta),
532            Err(FileVaultError::MetadataStructureMissing { .. })
533        ));
534    }
535
536    #[test]
537    fn decimal_integer_values_parse() {
538        // UserType and lv.size given as decimals (not 0x hex) still parse.
539        let meta = b"<key>UserType</key><integer size=\"32\" ID=\"6\">268435457</integer>".to_vec();
540        assert_eq!(
541            extract_integer_from(&meta, "UserType", 0).unwrap().0,
542            268_435_457
543        );
544
545        let lv_meta = b"<dict>\
546            <key>com.apple.corestorage.lv.familyUUID</key><string ID=\"1\">1F01CA34-5F6C-4123-AC0C-B0A256889DB2</string>\
547            <key>com.apple.corestorage.lv.size</key><integer size=\"64\" ID=\"7\">167772160</integer>\
548            </dict>".to_vec();
549        let lv = LogicalVolumeInfo::extract(&lv_meta).unwrap();
550        assert_eq!(lv.size, 167_772_160);
551    }
552}