Skip to main content

usb_forensic/sources/
emdmgmt.rs

1//! Adapter: `peripheral-core` [`EmdVolume`]s (Windows `EMDMgmt` `ReadyBoost` cache) →
2//! USB-history [`Claim`]s.
3//!
4//! `EMDMgmt` caches, per volume, its label and 4-byte volume serial. The serial is the
5//! value a Shell Link's `DriveSerialNumber` stores, so keying the label and the serial by
6//! that serial lets a `.lnk` file-access (keyed by the same serial) reconcile onto the
7//! named volume — attributing "files opened" to a volume by *name*, even after the device
8//! is gone. A pure mapping over already-decoded records.
9
10use crate::{Attribute, Claim, DeviceKey, HistorySource, Provenance, SourceKind, Value};
11use peripheral_core::emdmgmt::EmdVolume;
12
13/// A [`HistorySource`] over decoded [`EmdVolume`]s.
14pub struct EmdMgmtSource<'a> {
15    volumes: &'a [EmdVolume],
16}
17
18impl<'a> EmdMgmtSource<'a> {
19    /// Wrap decoded `EMDMgmt` volumes (from `peripheral_core::emdmgmt::parse_emdmgmt`).
20    #[must_use]
21    pub fn new(volumes: &'a [EmdVolume]) -> Self {
22        Self { volumes }
23    }
24}
25
26/// Render a 4-byte volume serial as `dir`/`vol` show it (`XXXX-XXXX`, upper hex), the same
27/// canonical form the LNK adapter uses, so the same serial from either source matches.
28fn format_volume_serial(serial: u32) -> String {
29    format!("{:04X}-{:04X}", serial >> 16, serial & 0xFFFF)
30}
31
32impl HistorySource for EmdMgmtSource<'_> {
33    fn claims(&self) -> Vec<Claim> {
34        let mut out = Vec::new();
35        for vol in self.volumes {
36            let serial = format_volume_serial(vol.volume_serial);
37            let device = DeviceKey(serial.clone());
38            let locator = vol
39                .source
40                .key_path
41                .clone()
42                .unwrap_or_else(|| vol.source.file.clone());
43            // The volume serial, surfaced as the matchable join key.
44            out.push(Claim {
45                device: device.clone(),
46                attribute: Attribute::VolumeSerial,
47                value: Value::Text(serial),
48                provenance: Provenance {
49                    source: SourceKind::EmdMgmt,
50                    locator: locator.clone(),
51                },
52            });
53            // The label, when the volume had one (an unlabelled volume still has a serial).
54            if !vol.volume_label.is_empty() {
55                out.push(Claim {
56                    device,
57                    attribute: Attribute::VolumeName,
58                    value: Value::Text(vol.volume_label.clone()),
59                    provenance: Provenance {
60                        source: SourceKind::EmdMgmt,
61                        locator,
62                    },
63                });
64            }
65        }
66        out
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use peripheral_core::Provenance as PcProvenance;
74
75    fn vol(label: &str, serial: u32) -> EmdVolume {
76        EmdVolume {
77            volume_label: label.to_string(),
78            volume_serial: serial,
79            source: PcProvenance {
80                file: "SOFTWARE".to_string(),
81                line: 0,
82                key_path: Some("…\\EMDMgmt\\x".to_string()),
83            },
84        }
85    }
86
87    #[test]
88    fn labelled_volume_yields_serial_and_name_claims_keyed_by_serial() {
89        // 0x5C754D3E = 5C75-4D3E (CFReDS "Authorized USB").
90        let vols = [vol("Authorized USB", 0x5C75_4D3E)];
91        let claims = EmdMgmtSource::new(&vols).claims();
92        assert_eq!(claims.len(), 2);
93        assert_eq!(claims[0].device, DeviceKey("5C75-4D3E".to_string()));
94        assert_eq!(claims[0].attribute, Attribute::VolumeSerial);
95        assert_eq!(claims[0].value, Value::Text("5C75-4D3E".to_string()));
96        assert_eq!(claims[1].attribute, Attribute::VolumeName);
97        assert_eq!(claims[1].value, Value::Text("Authorized USB".to_string()));
98        assert_eq!(claims[1].provenance.source, SourceKind::EmdMgmt);
99    }
100
101    #[test]
102    fn unlabelled_volume_yields_only_the_serial_claim() {
103        let vols = [vol("", 1)];
104        let claims = EmdMgmtSource::new(&vols).claims();
105        assert_eq!(claims.len(), 1);
106        assert_eq!(claims[0].attribute, Attribute::VolumeSerial);
107        assert_eq!(claims[0].value, Value::Text("0000-0001".to_string()));
108    }
109
110    #[test]
111    fn locator_falls_back_to_the_file_without_a_key_path() {
112        let mut v = vol("X", 2);
113        v.source.key_path = None;
114        let vols = [v];
115        let claims = EmdMgmtSource::new(&vols).claims();
116        assert_eq!(claims[0].provenance.locator, "SOFTWARE");
117    }
118}