Skip to main content

ntfs_core/
standard_information.rs

1//! `$STANDARD_INFORMATION` (type `0x10`) — the core file metadata: the four
2//! MACE timestamps, DOS attribute flags, and (NTFS 3.0+) the security id and
3//! the `$UsnJrnl` update sequence number.
4//!
5//! These are the timestamps an attacker most easily forges. Comparing them
6//! against the `$FILE_NAME` set (see [`crate::file_name`]) is the classic
7//! timestomping check — wired in at the Tier-2 forensic layer.
8
9use crate::bytes::{arr, le_u32, le_u64};
10use crate::error::{NtfsError, Result};
11use crate::time::Filetime;
12
13/// Minimum `$STANDARD_INFORMATION` content (NTFS 1.2: four timestamps + flags +
14/// version fields).
15const SI_MIN: usize = 0x30;
16/// Content length at which NTFS 3.0+ fields (owner/security/quota/usn) appear.
17const SI_V3: usize = 0x48;
18
19/// Windows `FILE_ATTRIBUTE_*` flags (shared with `$FILE_NAME`), from the
20/// KNOWLEDGE layer. Re-exported under the historical `file_attr` name.
21pub use forensicnomicon::ntfs::file_attributes as file_attr;
22
23/// Parsed `$STANDARD_INFORMATION` value.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct StandardInformation {
26    /// Creation time.
27    pub created: Filetime,
28    /// File content modification time ("M").
29    pub modified: Filetime,
30    /// MFT record modification time ("C" / entry-changed).
31    pub mft_modified: Filetime,
32    /// Last access time ("A").
33    pub accessed: Filetime,
34    /// `FILE_ATTRIBUTE_*` flags.
35    pub file_attributes: u32,
36    /// Security id (NTFS 3.0+), else `None`.
37    pub security_id: Option<u32>,
38    /// `$UsnJrnl` update sequence number (NTFS 3.0+), else `None`.
39    pub usn: Option<u64>,
40}
41
42impl StandardInformation {
43    /// `true` if the hidden attribute is set.
44    #[must_use]
45    pub fn is_hidden(&self) -> bool {
46        self.file_attributes & file_attr::HIDDEN != 0
47    }
48
49    /// `true` if the system attribute is set.
50    #[must_use]
51    pub fn is_system(&self) -> bool {
52        self.file_attributes & file_attr::SYSTEM != 0
53    }
54
55    /// `true` if the read-only attribute is set.
56    #[must_use]
57    pub fn is_read_only(&self) -> bool {
58        self.file_attributes & file_attr::READONLY != 0
59    }
60
61    /// Parse a `$STANDARD_INFORMATION` value from its resident content bytes.
62    ///
63    /// # Errors
64    ///
65    /// [`NtfsError::TooShort`] when `content` is smaller than the minimum.
66    pub fn parse(content: &[u8]) -> Result<StandardInformation> {
67        if content.len() < SI_MIN {
68            return Err(NtfsError::TooShort {
69                what: "$STANDARD_INFORMATION",
70                need: SI_MIN,
71                got: content.len(),
72            });
73        }
74        let ft = |o: usize| Filetime::from_le(&arr(content, o));
75        let file_attributes = le_u32(content, 0x20);
76
77        let (security_id, usn) = if content.len() >= SI_V3 {
78            (Some(le_u32(content, 0x34)), Some(le_u64(content, 0x40)))
79        } else {
80            (None, None)
81        };
82
83        Ok(StandardInformation {
84            created: ft(0x00),
85            modified: ft(0x08),
86            mft_modified: ft(0x10),
87            accessed: ft(0x18),
88            file_attributes,
89            security_id,
90            usn,
91        })
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    fn make_si(
100        created: u64,
101        modified: u64,
102        mft_modified: u64,
103        accessed: u64,
104        attrs: u32,
105        v3: Option<(u32, u64)>, // (security_id, usn)
106    ) -> Vec<u8> {
107        let len = if v3.is_some() { SI_V3 } else { SI_MIN };
108        let mut c = vec![0u8; len];
109        c[0x00..0x08].copy_from_slice(&created.to_le_bytes());
110        c[0x08..0x10].copy_from_slice(&modified.to_le_bytes());
111        c[0x10..0x18].copy_from_slice(&mft_modified.to_le_bytes());
112        c[0x18..0x20].copy_from_slice(&accessed.to_le_bytes());
113        c[0x20..0x24].copy_from_slice(&attrs.to_le_bytes());
114        if let Some((sid, usn)) = v3 {
115            c[0x34..0x38].copy_from_slice(&sid.to_le_bytes());
116            c[0x40..0x48].copy_from_slice(&usn.to_le_bytes());
117        }
118        c
119    }
120
121    #[test]
122    fn parses_ntfs12_standard_information() {
123        let c = make_si(0x10, 0x20, 0x30, 0x40, file_attr::ARCHIVE, None);
124        let si = StandardInformation::parse(&c).unwrap();
125        assert_eq!(si.created, Filetime(0x10));
126        assert_eq!(si.modified, Filetime(0x20));
127        assert_eq!(si.mft_modified, Filetime(0x30));
128        assert_eq!(si.accessed, Filetime(0x40));
129        assert_eq!(si.file_attributes, file_attr::ARCHIVE);
130        assert_eq!(si.security_id, None);
131        assert_eq!(si.usn, None);
132    }
133
134    #[test]
135    fn parses_ntfs30_security_and_usn() {
136        let c = make_si(1, 2, 3, 4, 0, Some((0x101, 0xDEAD_BEEF)));
137        let si = StandardInformation::parse(&c).unwrap();
138        assert_eq!(si.security_id, Some(0x101));
139        assert_eq!(si.usn, Some(0xDEAD_BEEF));
140    }
141
142    #[test]
143    fn flag_predicates() {
144        let c = make_si(0, 0, 0, 0, file_attr::HIDDEN | file_attr::SYSTEM, None);
145        let si = StandardInformation::parse(&c).unwrap();
146        assert!(si.is_hidden());
147        assert!(si.is_system());
148        assert!(!si.is_read_only());
149    }
150
151    #[test]
152    fn rejects_too_short() {
153        let c = vec![0u8; 0x20];
154        assert!(matches!(
155            StandardInformation::parse(&c),
156            Err(NtfsError::TooShort { .. })
157        ));
158    }
159}