Skip to main content

windows_erg/security/
sid.rs

1//! SID (Security Identifier) primitives.
2
3use crate::Result;
4use crate::error::{Error, SecurityError, SidParseError};
5use std::fmt::Write;
6
7/// A validated Windows Security Identifier.
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub struct Sid {
10    raw: Vec<u8>,
11    string: String,
12}
13
14impl Sid {
15    /// Create a SID from binary data.
16    pub fn from_bytes(data: &[u8]) -> Result<Self> {
17        let (sid_string, consumed) = sid_to_string_with_size(data).ok_or_else(|| {
18            Error::Security(SecurityError::SidParse(SidParseError::new(
19                "binary_sid",
20                "SID data is malformed or truncated",
21            )))
22        })?;
23
24        if consumed != data.len() {
25            return Err(Error::Security(SecurityError::SidParse(
26                SidParseError::new("binary_sid", "SID buffer contains trailing bytes"),
27            )));
28        }
29
30        Ok(Self {
31            raw: data.to_vec(),
32            string: sid_string,
33        })
34    }
35
36    /// Parse a SID from string form (`S-1-...`).
37    pub fn parse(value: &str) -> Result<Self> {
38        let raw = sid_string_to_bytes(value)?;
39        Ok(Self {
40            raw,
41            string: value.to_string(),
42        })
43    }
44
45    /// Parse a SID from either canonical SID string (`S-1-...`) or SDDL trustee alias.
46    pub fn from_sddl_trustee(value: &str) -> Result<Self> {
47        if value.starts_with("S-") {
48            return Self::parse(value);
49        }
50
51        let sid_string = sddl_alias_to_sid(value).ok_or_else(|| {
52            Error::Security(SecurityError::SidParse(SidParseError::new(
53                value.to_string(),
54                "unrecognized SDDL trustee alias",
55            )))
56        })?;
57
58        Self::parse(sid_string)
59    }
60
61    /// Returns the canonical SID string.
62    pub fn as_str(&self) -> &str {
63        &self.string
64    }
65
66    /// Returns the binary SID representation.
67    pub fn as_bytes(&self) -> &[u8] {
68        &self.raw
69    }
70
71    /// Returns a well-known SDDL alias for this SID when one exists.
72    pub fn to_sddl_alias(&self) -> Option<&'static str> {
73        sid_to_sddl_alias(self.as_str())
74    }
75
76    /// Case-insensitive compare for SID strings.
77    pub fn eq_case_insensitive(&self, other: &str) -> bool {
78        self.string.eq_ignore_ascii_case(other)
79    }
80}
81
82fn sddl_alias_to_sid(value: &str) -> Option<&'static str> {
83    match value {
84        "SY" => Some("S-1-5-18"),     // LocalSystem
85        "BA" => Some("S-1-5-32-544"), // Builtin Administrators
86        "BU" => Some("S-1-5-32-545"), // Builtin Users
87        "BG" => Some("S-1-5-32-546"), // Builtin Guests
88        "PU" => Some("S-1-5-32-547"), // Power Users
89        "AO" => Some("S-1-5-32-548"), // Account Operators
90        "SO" => Some("S-1-5-32-549"), // Server Operators
91        "PO" => Some("S-1-5-32-550"), // Print Operators
92        "BO" => Some("S-1-5-32-551"), // Backup Operators
93        "RE" => Some("S-1-5-32-552"), // Replicator
94        "WD" => Some("S-1-1-0"),      // Everyone
95        "AU" => Some("S-1-5-11"),     // Authenticated Users
96        "AN" => Some("S-1-5-7"),      // Anonymous
97        "NU" => Some("S-1-5-2"),      // Network
98        "IU" => Some("S-1-5-4"),      // Interactive
99        "SU" => Some("S-1-5-6"),      // Service
100        "LS" => Some("S-1-5-19"),     // Local Service
101        "NS" => Some("S-1-5-20"),     // Network Service
102        "CO" => Some("S-1-3-0"),      // Creator Owner
103        "CG" => Some("S-1-3-1"),      // Creator Group
104        "OW" => Some("S-1-3-4"),      // Owner Rights
105        "AC" => Some("S-1-15-2-1"),   // All Application Packages
106        "S-1-5-80-0" => Some("S-1-5-80-0"),
107        _ => None,
108    }
109}
110
111fn sid_to_sddl_alias(value: &str) -> Option<&'static str> {
112    match value {
113        "S-1-5-18" => Some("SY"),
114        "S-1-5-32-544" => Some("BA"),
115        "S-1-5-32-545" => Some("BU"),
116        "S-1-5-32-546" => Some("BG"),
117        "S-1-5-32-547" => Some("PU"),
118        "S-1-5-32-548" => Some("AO"),
119        "S-1-5-32-549" => Some("SO"),
120        "S-1-5-32-550" => Some("PO"),
121        "S-1-5-32-551" => Some("BO"),
122        "S-1-5-32-552" => Some("RE"),
123        "S-1-1-0" => Some("WD"),
124        "S-1-5-11" => Some("AU"),
125        "S-1-5-7" => Some("AN"),
126        "S-1-5-2" => Some("NU"),
127        "S-1-5-4" => Some("IU"),
128        "S-1-5-6" => Some("SU"),
129        "S-1-5-19" => Some("LS"),
130        "S-1-5-20" => Some("NS"),
131        "S-1-3-0" => Some("CO"),
132        "S-1-3-1" => Some("CG"),
133        "S-1-3-4" => Some("OW"),
134        "S-1-15-2-1" => Some("AC"),
135        _ => None,
136    }
137}
138
139impl std::fmt::Display for Sid {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        write!(f, "{}", self.string)
142    }
143}
144
145fn sid_string_to_bytes(value: &str) -> Result<Vec<u8>> {
146    let parts: Vec<&str> = value.split('-').collect();
147    if parts.len() < 3 || !parts[0].eq_ignore_ascii_case("S") {
148        return Err(Error::Security(SecurityError::SidParse(
149            SidParseError::new(
150                value.to_string(),
151                "expected SID format S-Revision-IdentifierAuthority-SubAuthority...",
152            ),
153        )));
154    }
155
156    let revision = parts[1].parse::<u8>().map_err(|_| {
157        Error::Security(SecurityError::SidParse(SidParseError::new(
158            value.to_string(),
159            "invalid SID revision",
160        )))
161    })?;
162
163    let identifier_authority = parts[2].parse::<u64>().map_err(|_| {
164        Error::Security(SecurityError::SidParse(SidParseError::new(
165            value.to_string(),
166            "invalid identifier authority",
167        )))
168    })?;
169
170    if identifier_authority > 0x0000_FFFF_FFFF {
171        return Err(Error::Security(SecurityError::SidParse(
172            SidParseError::new(
173                value.to_string(),
174                "identifier authority must fit in 48 bits",
175            ),
176        )));
177    }
178
179    let sub_authority_count = parts.len().saturating_sub(3);
180    if sub_authority_count > u8::MAX as usize {
181        return Err(Error::Security(SecurityError::SidParse(
182            SidParseError::new(value.to_string(), "too many sub-authorities"),
183        )));
184    }
185
186    let mut out = Vec::with_capacity(8 + sub_authority_count * 4);
187    out.push(revision);
188    out.push(sub_authority_count as u8);
189
190    let authority_be = identifier_authority.to_be_bytes();
191    out.extend_from_slice(&authority_be[2..]);
192
193    for part in parts.iter().skip(3) {
194        let sub = part.parse::<u32>().map_err(|_| {
195            Error::Security(SecurityError::SidParse(SidParseError::new(
196                value.to_string(),
197                "invalid sub-authority value",
198            )))
199        })?;
200        out.extend_from_slice(&sub.to_le_bytes());
201    }
202
203    Ok(out)
204}
205
206fn sid_to_string_with_size(sid: &[u8]) -> Option<(String, usize)> {
207    if sid.len() < 8 {
208        return None;
209    }
210
211    let mut id = String::with_capacity(32);
212    let subauthority_count = sid[1] as usize;
213
214    let mut identifier_authority = (u16::from_be_bytes([sid[2], sid[3]]) as u64) << 32;
215    identifier_authority |= u32::from_be_bytes([sid[4], sid[5], sid[6], sid[7]]) as u64;
216
217    let _ = write!(&mut id, "S-{}-{}", sid[0], identifier_authority);
218
219    let mut start = 8usize;
220    for _ in 0..subauthority_count {
221        if start + 4 > sid.len() {
222            return None;
223        }
224
225        let authority =
226            u32::from_le_bytes([sid[start], sid[start + 1], sid[start + 2], sid[start + 3]]);
227        let _ = write!(&mut id, "-{}", authority);
228        start += 4;
229    }
230
231    Some((id, start))
232}
233
234#[cfg(test)]
235mod tests {
236    use super::Sid;
237
238    #[test]
239    fn sid_round_trip_string_binary() {
240        let sid = Sid::parse("S-1-5-32-544").expect("parse should succeed");
241        let sid2 = Sid::from_bytes(sid.as_bytes()).expect("binary parse should succeed");
242        assert_eq!(sid.as_str(), sid2.as_str());
243        assert_eq!(sid.as_bytes(), sid2.as_bytes());
244    }
245
246    #[test]
247    fn sid_parse_invalid_format() {
248        let err = Sid::parse("not-a-sid").expect_err("expected parse error");
249        assert!(err.to_string().contains("expected SID format"));
250    }
251
252    #[test]
253    fn sid_parse_from_sddl_alias() {
254        let sid = Sid::from_sddl_trustee("BA").expect("alias parse should succeed");
255        assert_eq!(sid.as_str(), "S-1-5-32-544");
256    }
257
258    #[test]
259    fn sid_to_sddl_alias_round_trip() {
260        let sid = Sid::parse("S-1-5-32-545").expect("parse should succeed");
261        assert_eq!(sid.to_sddl_alias(), Some("BU"));
262    }
263}