use serde::{Deserialize, Serialize};
use serde_with::formats::CommaSeparator;
use serde_with::{serde_as, DefaultOnNull, StringWithSeparator};
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct EtcUser {
pub name: String,
pub password: String,
pub uid: u32,
pub gid: u32,
pub gecos: String,
pub homedir: String,
pub shell: String,
}
pub fn parse_etc_passwd(bytes: &[u8]) -> Result<Vec<EtcUser>, UnixIntegrationError> {
use csv::ReaderBuilder;
let mut rdr = ReaderBuilder::new()
.has_headers(false)
.delimiter(b':')
.from_reader(bytes);
rdr.deserialize()
.map(|result| result.map_err(|_e| UnixIntegrationError))
.collect::<Result<Vec<EtcUser>, UnixIntegrationError>>()
}
pub fn read_etc_passwd_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcUser>, UnixIntegrationError> {
let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
let mut contents = vec![];
file.read_to_end(&mut contents)
.map_err(|_| UnixIntegrationError)?;
parse_etc_passwd(contents.as_slice()).map_err(|_| UnixIntegrationError)
}
#[derive(Debug, PartialEq, Default)]
pub enum CryptPw {
Sha256(String),
Sha512(String),
#[default]
Invalid,
}
impl fmt::Display for CryptPw {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CryptPw::Invalid => write!(f, "x"),
CryptPw::Sha256(s) | CryptPw::Sha512(s) => write!(f, "{}", s),
}
}
}
impl FromStr for CryptPw {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.starts_with("$6$") {
Ok(CryptPw::Sha512(value.to_string()))
} else if value.starts_with("$5$") {
Ok(CryptPw::Sha256(value.to_string()))
} else {
Ok(CryptPw::Invalid)
}
}
}
impl CryptPw {
pub fn is_valid(&self) -> bool {
!matches!(self, CryptPw::Invalid)
}
pub fn check_pw(&self, cred: &str) -> bool {
match &self {
CryptPw::Sha256(crypt) => sha_crypt::sha256_check(cred, crypt.as_str()).is_ok(),
CryptPw::Sha512(crypt) => sha_crypt::sha512_check(cred, crypt.as_str()).is_ok(),
CryptPw::Invalid => false,
}
}
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
pub struct EtcShadow {
pub name: String,
#[serde_as(as = "serde_with::DisplayFromStr")]
pub password: CryptPw,
pub epoch_change_days: Option<i64>,
#[serde_as(deserialize_as = "DefaultOnNull")]
pub days_min_password_age: i64,
pub days_max_password_age: Option<i64>,
#[serde_as(deserialize_as = "DefaultOnNull")]
pub days_warning_period: i64,
pub days_inactivity_period: Option<i64>,
pub epoch_expire_date: Option<i64>,
pub flag_reserved: Option<u32>,
}
pub fn parse_etc_shadow(bytes: &[u8]) -> Result<Vec<EtcShadow>, UnixIntegrationError> {
use csv::ReaderBuilder;
let mut rdr = ReaderBuilder::new()
.has_headers(false)
.delimiter(b':')
.from_reader(bytes);
rdr.deserialize()
.map(|result| {
result.map_err(|err| {
eprintln!("{:?}", err);
UnixIntegrationError
})
})
.collect::<Result<Vec<EtcShadow>, UnixIntegrationError>>()
}
pub fn read_etc_shadow_file<P: AsRef<Path>>(
path: P,
) -> Result<Vec<EtcShadow>, UnixIntegrationError> {
let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
let mut contents = vec![];
file.read_to_end(&mut contents)
.map_err(|_| UnixIntegrationError)?;
parse_etc_shadow(contents.as_slice()).map_err(|_| UnixIntegrationError)
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct EtcGroup {
pub name: String,
pub password: String,
pub gid: u32,
#[serde_as(as = "StringWithSeparator::<CommaSeparator, String>")]
pub members: Vec<String>,
}
#[derive(Debug)]
pub struct UnixIntegrationError;
pub fn parse_etc_group(bytes: &[u8]) -> Result<Vec<EtcGroup>, UnixIntegrationError> {
use csv::ReaderBuilder;
let mut rdr = ReaderBuilder::new()
.has_headers(false)
.delimiter(b':')
.from_reader(bytes);
rdr.deserialize()
.map(|result| result.map_err(|_e| UnixIntegrationError))
.collect::<Result<Vec<EtcGroup>, UnixIntegrationError>>()
}
pub fn read_etc_group_file<P: AsRef<Path>>(path: P) -> Result<Vec<EtcGroup>, UnixIntegrationError> {
let mut file = File::open(path.as_ref()).map_err(|_| UnixIntegrationError)?;
let mut contents = vec![];
file.read_to_end(&mut contents)
.map_err(|_| UnixIntegrationError)?;
parse_etc_group(contents.as_slice()).map_err(|_| UnixIntegrationError)
}
#[cfg(test)]
mod tests {
use super::*;
const EXAMPLE_PASSWD: &str = r#"root:x:0:0:root:/root:/bin/bash
systemd-timesync:x:498:498:systemd Time Synchronization:/:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/var/lib/nobody:/bin/bash
"#;
#[test]
fn test_parse_passwd() {
let users =
parse_etc_passwd(EXAMPLE_PASSWD.as_bytes()).expect("Failed to parse passwd data");
assert_eq!(
users[0],
EtcUser {
name: "root".to_string(),
password: "x".to_string(),
uid: 0,
gid: 0,
gecos: "root".to_string(),
homedir: "/root".to_string(),
shell: "/bin/bash".to_string(),
}
);
assert_eq!(
users[1],
EtcUser {
name: "systemd-timesync".to_string(),
password: "x".to_string(),
uid: 498,
gid: 498,
gecos: "systemd Time Synchronization".to_string(),
homedir: "/".to_string(),
shell: "/usr/sbin/nologin".to_string(),
}
);
assert_eq!(
users[2],
EtcUser {
name: "nobody".to_string(),
password: "x".to_string(),
uid: 65534,
gid: 65534,
gecos: "nobody".to_string(),
homedir: "/var/lib/nobody".to_string(),
shell: "/bin/bash".to_string(),
}
);
}
const EXAMPLE_SHADOW: &str = r#"sshd:!:19978::::::
tss:!:19980::::::
admin:$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//:19980:0:99999:7:::
"#;
#[test]
fn test_parse_shadow() {
let shadow =
parse_etc_shadow(EXAMPLE_SHADOW.as_bytes()).expect("Failed to parse passwd data");
assert_eq!(
shadow[0],
EtcShadow {
name: "sshd".to_string(),
password: CryptPw::Invalid,
epoch_change_days: Some(19978),
days_min_password_age: 0,
days_max_password_age: None,
days_warning_period: 0,
days_inactivity_period: None,
epoch_expire_date: None,
flag_reserved: None
}
);
assert_eq!(
shadow[1],
EtcShadow {
name: "tss".to_string(),
password: CryptPw::Invalid,
epoch_change_days: Some(19980),
days_min_password_age: 0,
days_max_password_age: None,
days_warning_period: 0,
days_inactivity_period: None,
epoch_expire_date: None,
flag_reserved: None
}
);
assert_eq!(shadow[2], EtcShadow {
name: "admin".to_string(),
password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()),
epoch_change_days: Some(19980),
days_min_password_age: 0,
days_max_password_age: Some(99999),
days_warning_period: 7,
days_inactivity_period: None,
epoch_expire_date: None,
flag_reserved: None
});
}
const EXAMPLE_GROUP: &str = r#"root:x:0:
wheel:x:481:admin,testuser
"#;
#[test]
fn test_parse_group() {
let groups = parse_etc_group(EXAMPLE_GROUP.as_bytes()).expect("Failed to parse groups");
assert_eq!(
groups[0],
EtcGroup {
name: "root".to_string(),
password: "x".to_string(),
gid: 0,
members: vec![]
}
);
assert_eq!(
groups[1],
EtcGroup {
name: "wheel".to_string(),
password: "x".to_string(),
gid: 481,
members: vec!["admin".to_string(), "testuser".to_string(),]
}
);
}
}