1use serde::{Deserialize, Serialize};
2use serde_with::formats::CommaSeparator;
3use serde_with::{serde_as, DefaultOnNull, StringWithSeparator};
4use std::fmt;
5use std::fs::File;
6use std::io::Read;
7use std::path::Path;
8use std::str::FromStr;
9
10#[derive(Serialize, Deserialize, Debug)]
11pub struct EtcDb {
12 pub users: Vec<EtcUser>,
13 pub shadow: Vec<EtcShadow>,
14 pub groups: Vec<EtcGroup>,
15}
16
17#[derive(Serialize, Deserialize, Debug, PartialEq)]
18pub struct EtcUser {
19 pub name: String,
20 pub password: String,
21 pub uid: u32,
22 pub gid: u32,
23 pub gecos: String,
24 pub homedir: String,
25 pub shell: String,
26}
27
28pub fn parse_etc_passwd(bytes: &[u8]) -> Result<Vec<EtcUser>, UnixIntegrationError> {
29 use csv::ReaderBuilder;
30 let mut rdr = ReaderBuilder::new()
31 .has_headers(false)
32 .delimiter(b':')
33 .from_reader(bytes);
34 rdr.deserialize()
35 .map(|result| result.map_err(|_e| UnixIntegrationError))
36 .collect::<Result<Vec<EtcUser>, UnixIntegrationError>>()
37}
38
39pub fn read_etc_passwd_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcUser>, UnixIntegrationError> {
40 let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
41
42 let mut contents = vec![];
43 file.read_to_end(&mut contents)
44 .map_err(|_| UnixIntegrationError)?;
45
46 parse_etc_passwd(contents.as_slice()).map_err(|_| UnixIntegrationError)
47}
48
49#[derive(PartialEq, Default)]
50pub enum CryptPw {
51 Sha256(String),
52 Sha512(String),
53 #[default]
54 Invalid,
55}
56
57impl fmt::Display for CryptPw {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 CryptPw::Invalid => write!(f, "x"),
61 CryptPw::Sha256(s) | CryptPw::Sha512(s) => write!(f, "{}", s),
62 }
63 }
64}
65
66impl fmt::Debug for CryptPw {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 CryptPw::Invalid => write!(f, "x"),
70 CryptPw::Sha256(_s) => write!(f, "crypt sha256"),
71 CryptPw::Sha512(_s) => write!(f, "crypt sha512"),
72 }
73 }
74}
75
76impl FromStr for CryptPw {
77 type Err = &'static str;
78
79 fn from_str(value: &str) -> Result<Self, Self::Err> {
80 if value.starts_with("$6$") {
81 Ok(CryptPw::Sha512(value.to_string()))
82 } else if value.starts_with("$5$") {
83 Ok(CryptPw::Sha256(value.to_string()))
84 } else {
85 Ok(CryptPw::Invalid)
86 }
87 }
88}
89
90impl CryptPw {
91 pub fn is_valid(&self) -> bool {
92 !matches!(self, CryptPw::Invalid)
93 }
94
95 pub fn check_pw(&self, cred: &str) -> bool {
96 match &self {
97 CryptPw::Sha256(crypt) => sha_crypt::sha256_check(cred, crypt.as_str()).is_ok(),
98 CryptPw::Sha512(crypt) => sha_crypt::sha512_check(cred, crypt.as_str()).is_ok(),
99 CryptPw::Invalid => false,
100 }
101 }
102}
103
104#[serde_as]
105#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
106pub struct EtcShadow {
107 pub name: String,
108 #[serde_as(as = "serde_with::DisplayFromStr")]
109 pub password: CryptPw,
110 pub epoch_change_days: Option<i64>,
113 #[serde_as(deserialize_as = "DefaultOnNull")]
115 pub days_min_password_age: i64,
116 pub days_max_password_age: Option<i64>,
117 #[serde_as(deserialize_as = "DefaultOnNull")]
119 pub days_warning_period: i64,
120 pub days_inactivity_period: Option<i64>,
123 pub epoch_expire_date: Option<i64>,
124 pub flag_reserved: Option<u32>,
125}
126
127pub fn parse_etc_shadow(bytes: &[u8]) -> Result<Vec<EtcShadow>, UnixIntegrationError> {
128 use csv::ReaderBuilder;
129 let mut rdr = ReaderBuilder::new()
130 .has_headers(false)
131 .delimiter(b':')
132 .from_reader(bytes);
133 rdr.deserialize()
134 .map(|result| {
135 result.map_err(|err| {
136 eprintln!("{:?}", err);
137 UnixIntegrationError
138 })
139 })
140 .collect::<Result<Vec<EtcShadow>, UnixIntegrationError>>()
141}
142
143pub fn read_etc_shadow_file<P: AsRef<Path>>(
144 path: P,
145) -> Result<Vec<EtcShadow>, UnixIntegrationError> {
146 let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
147
148 let mut contents = vec![];
149 file.read_to_end(&mut contents)
150 .map_err(|_| UnixIntegrationError)?;
151
152 parse_etc_shadow(contents.as_slice()).map_err(|_| UnixIntegrationError)
153}
154
155#[serde_as]
156#[derive(Serialize, Deserialize, Debug, PartialEq)]
157pub struct EtcGroup {
158 pub name: String,
159 pub password: String,
160 pub gid: u32,
161 #[serde_as(as = "StringWithSeparator::<CommaSeparator, String>")]
162 pub members: Vec<String>,
163}
164
165#[derive(Debug)]
166pub struct UnixIntegrationError;
167
168pub fn parse_etc_group(bytes: &[u8]) -> Result<Vec<EtcGroup>, UnixIntegrationError> {
169 use csv::ReaderBuilder;
170 let mut rdr = ReaderBuilder::new()
171 .has_headers(false)
172 .delimiter(b':')
173 .from_reader(bytes);
174 rdr.deserialize()
175 .map(|result| result.map_err(|_e| UnixIntegrationError))
176 .collect::<Result<Vec<EtcGroup>, UnixIntegrationError>>()
177}
178
179pub fn read_etc_group_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcGroup>, UnixIntegrationError> {
180 let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
181
182 let mut contents = vec![];
183 file.read_to_end(&mut contents)
184 .map_err(|_| UnixIntegrationError)?;
185
186 parse_etc_group(contents.as_slice()).map_err(|_| UnixIntegrationError)
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 const EXAMPLE_PASSWD: &str = r#"root:x:0:0:root:/root:/bin/bash
194systemd-timesync:x:498:498:systemd Time Synchronization:/:/usr/sbin/nologin
195nobody:x:65534:65534:nobody:/var/lib/nobody:/bin/bash
196"#;
197
198 #[test]
199 fn test_parse_passwd() {
200 let users =
201 parse_etc_passwd(EXAMPLE_PASSWD.as_bytes()).expect("Failed to parse passwd data");
202
203 assert_eq!(
204 users[0],
205 EtcUser {
206 name: "root".to_string(),
207 password: "x".to_string(),
208 uid: 0,
209 gid: 0,
210 gecos: "root".to_string(),
211 homedir: "/root".to_string(),
212 shell: "/bin/bash".to_string(),
213 }
214 );
215
216 assert_eq!(
217 users[1],
218 EtcUser {
219 name: "systemd-timesync".to_string(),
220 password: "x".to_string(),
221 uid: 498,
222 gid: 498,
223 gecos: "systemd Time Synchronization".to_string(),
224 homedir: "/".to_string(),
225 shell: "/usr/sbin/nologin".to_string(),
226 }
227 );
228
229 assert_eq!(
230 users[2],
231 EtcUser {
232 name: "nobody".to_string(),
233 password: "x".to_string(),
234 uid: 65534,
235 gid: 65534,
236 gecos: "nobody".to_string(),
237 homedir: "/var/lib/nobody".to_string(),
238 shell: "/bin/bash".to_string(),
239 }
240 );
241 }
242
243 const EXAMPLE_SHADOW: &str = r#"sshd:!:19978::::::
245tss:!:19980::::::
246admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//:19980:0:99999:7:::
247"#;
248
249 #[test]
250 fn test_parse_shadow() {
251 let shadow =
252 parse_etc_shadow(EXAMPLE_SHADOW.as_bytes()).expect("Failed to parse passwd data");
253
254 assert_eq!(
255 shadow[0],
256 EtcShadow {
257 name: "sshd".to_string(),
258 password: CryptPw::Invalid,
259 epoch_change_days: Some(19978),
260 days_min_password_age: 0,
261 days_max_password_age: None,
262 days_warning_period: 0,
263 days_inactivity_period: None,
264 epoch_expire_date: None,
265 flag_reserved: None
266 }
267 );
268
269 assert_eq!(
270 shadow[1],
271 EtcShadow {
272 name: "tss".to_string(),
273 password: CryptPw::Invalid,
274 epoch_change_days: Some(19980),
275 days_min_password_age: 0,
276 days_max_password_age: None,
277 days_warning_period: 0,
278 days_inactivity_period: None,
279 epoch_expire_date: None,
280 flag_reserved: None
281 }
282 );
283
284 assert_eq!(shadow[2], EtcShadow {
285 name: "admin".to_string(),
286 password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()),
287 epoch_change_days: Some(19980),
288 days_min_password_age: 0,
289 days_max_password_age: Some(99999),
290 days_warning_period: 7,
291 days_inactivity_period: None,
292 epoch_expire_date: None,
293 flag_reserved: None
294 });
295 }
296
297 const EXAMPLE_GROUP: &str = r#"root:x:0:
298wheel:x:481:admin,testuser
299"#;
300
301 #[test]
302 fn test_parse_group() {
303 let groups = parse_etc_group(EXAMPLE_GROUP.as_bytes()).expect("Failed to parse groups");
304
305 assert_eq!(
306 groups[0],
307 EtcGroup {
308 name: "root".to_string(),
309 password: "x".to_string(),
310 gid: 0,
311 members: vec![]
312 }
313 );
314
315 assert_eq!(
316 groups[1],
317 EtcGroup {
318 name: "wheel".to_string(),
319 password: "x".to_string(),
320 gid: 481,
321 members: vec!["admin".to_string(), "testuser".to_string(),]
322 }
323 );
324 }
325}