bootc_sysusers/nameservice/
shadow.rs

1//! Helpers for [shadowed password file](https://man7.org/linux/man-pages/man5/shadow.5.html).
2// Copyright (C) 2021 Oracle and/or its affiliates.
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5use anyhow::{Context, Result, anyhow};
6use std::io::{BufRead, Write};
7
8/// Entry from shadow file.
9// Field names taken from (presumably glibc's) /usr/include/shadow.h, descriptions adapted
10// from the [shadow(3) manual page](https://man7.org/linux/man-pages/man3/shadow.3.html).
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub(crate) struct ShadowEntry {
13    /// user login name
14    pub(crate) namp: String,
15    /// encrypted password
16    pub(crate) pwdp: String,
17    /// days (from Jan 1, 1970) since password was last changed
18    pub(crate) lstchg: Option<u32>,
19    /// days before which password may not be changed
20    pub(crate) min: Option<u32>,
21    /// days after which password must be changed
22    pub(crate) max: Option<u32>,
23    /// days before password is to expire that user is warned of pending password expiration
24    pub(crate) warn: Option<u32>,
25    /// days after password expires that account is considered inactive and disabled
26    pub(crate) inact: Option<u32>,
27    /// date (in days since Jan 1, 1970) when account will be disabled
28    pub(crate) expire: Option<u32>,
29    /// reserved for future use
30    pub(crate) flag: String,
31}
32
33fn u32_or_none(value: &str) -> Result<Option<u32>, std::num::ParseIntError> {
34    if value.is_empty() {
35        Ok(None)
36    } else {
37        Ok(Some(value.parse()?))
38    }
39}
40
41fn number_or_empty(value: Option<u32>) -> String {
42    if let Some(number) = value {
43        format!("{number}")
44    } else {
45        "".to_string()
46    }
47}
48
49impl ShadowEntry {
50    /// Parse a single shadow entry.
51    pub fn parse_line(s: impl AsRef<str>) -> Option<Self> {
52        let mut parts = s.as_ref().splitn(9, ':');
53
54        let entry = Self {
55            namp: parts.next()?.to_string(),
56            pwdp: parts.next()?.to_string(),
57            lstchg: u32_or_none(parts.next()?).ok()?,
58            min: u32_or_none(parts.next()?).ok()?,
59            max: u32_or_none(parts.next()?).ok()?,
60            warn: u32_or_none(parts.next()?).ok()?,
61            inact: u32_or_none(parts.next()?).ok()?,
62            expire: u32_or_none(parts.next()?).ok()?,
63            flag: parts.next()?.to_string(),
64        };
65        Some(entry)
66    }
67
68    /// Serialize entry to writer, as a shadow line.
69    pub fn to_writer(&self, writer: &mut impl Write) -> Result<()> {
70        std::writeln!(
71            writer,
72            "{}:{}:{}:{}:{}:{}:{}:{}:{}",
73            self.namp,
74            self.pwdp,
75            number_or_empty(self.lstchg),
76            number_or_empty(self.min),
77            number_or_empty(self.max),
78            number_or_empty(self.warn),
79            number_or_empty(self.inact),
80            number_or_empty(self.expire),
81            self.flag
82        )
83        .with_context(|| "failed to write shadow entry")
84    }
85}
86
87pub(crate) fn parse_shadow_content(content: impl BufRead) -> Result<Vec<ShadowEntry>> {
88    let mut entries = vec![];
89    for (line_num, line) in content.lines().enumerate() {
90        let input =
91            line.with_context(|| format!("failed to read shadow entry at line {line_num}"))?;
92
93        // Skip empty and comment lines
94        if input.is_empty() || input.starts_with('#') {
95            continue;
96        }
97
98        let entry = ShadowEntry::parse_line(&input).ok_or_else(|| {
99            anyhow!(
100                "failed to parse shadow entry at line {}, content: {}",
101                line_num,
102                &input
103            )
104        })?;
105        entries.push(entry);
106    }
107    Ok(entries)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use std::io::Cursor;
114
115    fn salted() -> String {
116        // Hex encoded hashed password to avoid tripping leak detectors
117        let obfuscated = "24362473616c7453616c7453414c544e61436c247865354c5a47466c656b35334350467765327069495065536947414e6f596f696e5544755157307179645879766f594b566d4c3257524c71445a48586b626e706f4148714c3079616c6939344e526355527445616f51";
118        String::from_utf8(hex::decode(obfuscated).unwrap()).unwrap()
119    }
120
121    fn mock_shadow_entry() -> ShadowEntry {
122        ShadowEntry {
123            namp: "salty".to_string(),
124            pwdp: salted(),
125            lstchg: Some(18912),
126            min: Some(0),
127            max: Some(99999),
128            warn: Some(7),
129            inact: None,
130            expire: None,
131            flag: "".to_string(),
132        }
133    }
134
135    #[test]
136    fn test_parse_lines() {
137        let salted = salted();
138        let content = format!(
139            r#"
140root:*:18912:0:99999:7:::
141daemon:*:18474:0:99999:7:::
142
143salty:{salted}:18912:0:99999:7:::
144
145# Dummy comment
146systemd-coredump:!!:::::::
147systemd-resolve:!!:::::::
148rngd:!!:::::::
149"#
150        );
151
152        let input = Cursor::new(content);
153        let entries = parse_shadow_content(input).unwrap();
154        assert_eq!(entries.len(), 6);
155        assert_eq!(entries[2], mock_shadow_entry());
156    }
157
158    #[test]
159    fn test_write_entry() {
160        let entry = mock_shadow_entry();
161        let expected = format!("salty:{}:18912:0:99999:7:::\n", salted());
162        let mut buf = Vec::new();
163        entry.to_writer(&mut buf).unwrap();
164        assert_eq!(&buf, expected.as_bytes());
165    }
166}