Skip to main content

windows_sddl/
sid.rs

1//! SID / GUID types per MS-DTYP. No FFI — pure binary + string handling, so this works
2//! cross-platform against raw bytes (LDAP `objectSid`, registry hives, backup formats).
3
4use std::fmt;
5
6/// Windows Security Identifier. Stored canonically; `Display` yields `S-1-5-...`.
7#[derive(Clone, PartialEq, Eq, Hash)]
8pub struct Sid {
9    pub revision: u8,
10    /// 6-byte identifier authority (big-endian on the wire).
11    pub identifier_authority: u64,
12    pub sub_authorities: Vec<u32>,
13}
14
15impl Sid {
16    /// Parse the binary (`objectSid` / `SID_AND_ATTRIBUTES`) representation.
17    pub fn from_bytes(b: &[u8]) -> Option<Sid> {
18        if b.len() < 8 {
19            return None;
20        }
21        let revision = b[0];
22        let count = b[1] as usize;
23        let mut authority: u64 = 0;
24        for &byte in &b[2..8] {
25            authority = (authority << 8) | byte as u64; // big-endian
26        }
27        if b.len() < 8 + count * 4 {
28            return None;
29        }
30        let mut subs = Vec::with_capacity(count);
31        for i in 0..count {
32            let off = 8 + i * 4;
33            subs.push(u32::from_le_bytes([
34                b[off],
35                b[off + 1],
36                b[off + 2],
37                b[off + 3],
38            ]));
39        }
40        Some(Sid {
41            revision,
42            identifier_authority: authority,
43            sub_authorities: subs,
44        })
45    }
46
47    /// Serialize to the binary (`objectSid`) form.
48    pub fn to_bytes(&self) -> Vec<u8> {
49        let mut b = vec![self.revision, self.sub_authorities.len() as u8];
50        let a = self.identifier_authority;
51        b.extend_from_slice(&[
52            (a >> 40) as u8,
53            (a >> 32) as u8,
54            (a >> 24) as u8,
55            (a >> 16) as u8,
56            (a >> 8) as u8,
57            a as u8,
58        ]);
59        for s in &self.sub_authorities {
60            b.extend_from_slice(&s.to_le_bytes());
61        }
62        b
63    }
64
65    /// Parse the string form `S-1-5-21-...-513`.
66    pub fn parse(s: &str) -> Option<Sid> {
67        let mut it = s.split('-');
68        if it.next()? != "S" {
69            return None;
70        }
71        let revision = it.next()?.parse().ok()?;
72        let identifier_authority = it.next()?.parse().ok()?;
73        let sub_authorities = it.map(|p| p.parse().ok()).collect::<Option<Vec<u32>>>()?;
74        Some(Sid {
75            revision,
76            identifier_authority,
77            sub_authorities,
78        })
79    }
80
81    /// Last sub-authority — the RID.
82    pub fn rid(&self) -> Option<u32> {
83        self.sub_authorities.last().copied()
84    }
85
86    /// True for built-in / well-known SIDs that are never domain-specific (e.g. `S-1-5-32-544`).
87    pub fn is_well_known(&self) -> bool {
88        matches!(self.identifier_authority, 1 | 3) // WORLD, CREATOR
89            || (self.identifier_authority == 5 && self.sub_authorities.first() == Some(&32))
90    }
91}
92
93impl fmt::Display for Sid {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(f, "S-{}-{}", self.revision, self.identifier_authority)?;
96        for s in &self.sub_authorities {
97            write!(f, "-{s}")?;
98        }
99        Ok(())
100    }
101}
102
103impl fmt::Debug for Sid {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        write!(f, "{self}")
106    }
107}
108
109/// 16-byte GUID stored in the mixed-endian on-wire layout, normalized to a comparable byte
110/// array. Rendered `aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee`.
111#[derive(Clone, Copy, PartialEq, Eq, Hash)]
112pub struct Guid(pub [u8; 16]);
113
114impl Guid {
115    pub fn from_bytes(b: &[u8]) -> Option<Guid> {
116        Some(Guid(b.get(..16)?.try_into().ok()?))
117    }
118
119    /// Parse `1131f6aa-9c07-11d1-f79f-00c04fc2dcd2` (braces optional).
120    pub fn parse(s: &str) -> Option<Guid> {
121        let s = s.trim_matches(|c| c == '{' || c == '}');
122        let hex: String = s.chars().filter(|c| *c != '-').collect();
123        if hex.len() != 32 {
124            return None;
125        }
126        let raw: Vec<u8> = (0..16)
127            .map(|i| u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok())
128            .collect::<Option<_>>()?;
129        // string form is big-endian for the first 3 groups; store normalized for comparison.
130        let mut b = [0u8; 16];
131        b[0..4].copy_from_slice(&[raw[3], raw[2], raw[1], raw[0]]);
132        b[4..6].copy_from_slice(&[raw[5], raw[4]]);
133        b[6..8].copy_from_slice(&[raw[7], raw[6]]);
134        b[8..16].copy_from_slice(&raw[8..16]);
135        Some(Guid(b))
136    }
137}
138
139impl fmt::Display for Guid {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        let b = &self.0;
142        write!(
143            f,
144            "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
145            b[3], b[2], b[1], b[0], b[5], b[4], b[7], b[6],
146            b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]
147        )
148    }
149}
150
151impl fmt::Debug for Guid {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        write!(f, "{self}")
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn sid_roundtrip_string_and_bytes() {
163        let s = Sid::parse("S-1-5-21-1-2-3-1104").unwrap();
164        assert_eq!(s.rid(), Some(1104));
165        assert_eq!(s.to_string(), "S-1-5-21-1-2-3-1104");
166        let bytes = s.to_bytes();
167        assert_eq!(Sid::from_bytes(&bytes).unwrap(), s);
168    }
169
170    #[test]
171    fn guid_parse_display_roundtrip() {
172        let g = Guid::parse("1131f6aa-9c07-11d1-f79f-00c04fc2dcd2").unwrap();
173        assert_eq!(g.to_string(), "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2");
174    }
175}