Skip to main content

linuxutils_misc/
mcookie.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{
7    fs::File,
8    io::{self, Read},
9    path::PathBuf,
10    process::ExitCode,
11};
12
13/// Generate magic cookies for xauth.
14///
15/// Generates a 128-bit random hexadecimal number for use with the X authority
16/// system.
17#[derive(Parser)]
18#[command(name = "mcookie", version, about)]
19pub struct Args {
20    /// Use this file as an additional source of randomness.
21    /// When file is '-', characters are read from standard input.
22    #[arg(short, long)]
23    file: Option<PathBuf>,
24
25    /// Read from file only this number of bytes.
26    #[arg(short, long)]
27    max_size: Option<u64>,
28
29    /// Inform where randomness originated.
30    #[arg(short, long)]
31    verbose: bool,
32}
33
34pub fn run(args: Args) -> ExitCode {
35    let result = if let Some(ref path) = args.file {
36        if path.as_os_str() == "-" {
37            let mut stdin = io::stdin().lock();
38            mcookie(Some(&mut stdin), args.max_size, args.verbose)
39        } else {
40            match File::open(path) {
41                Ok(mut f) => mcookie(Some(&mut f), args.max_size, args.verbose),
42                Err(e) => {
43                    eprintln!("mcookie: {}: {e}", path.display());
44                    return ExitCode::FAILURE;
45                }
46            }
47        }
48    } else {
49        mcookie(None, args.max_size, args.verbose)
50    };
51
52    match result {
53        Ok(cookie) => {
54            println!("{cookie}");
55            ExitCode::SUCCESS
56        }
57        Err(e) => {
58            eprintln!("mcookie: {e}");
59            ExitCode::FAILURE
60        }
61    }
62}
63
64/// Generate a 128-bit random hexadecimal cookie.
65///
66/// If `extra_file` is provided, its contents (up to `max_size` bytes) are
67/// mixed into the randomness by XORing with the random bytes.
68pub fn mcookie(
69    extra_file: Option<&mut dyn Read>,
70    max_size: Option<u64>,
71    verbose: bool,
72) -> io::Result<String> {
73    let mut bytes = [0u8; 16];
74
75    // Primary source: getrandom / /dev/urandom
76    getrandom(&mut bytes)?;
77    if verbose {
78        eprintln!("Got 16 bytes from getrandom/urandom");
79    }
80
81    // Optional extra file source
82    if let Some(reader) = extra_file {
83        let mut extra = Vec::new();
84        match max_size {
85            Some(max) => {
86                reader.take(max).read_to_end(&mut extra)?;
87            }
88            None => {
89                reader.read_to_end(&mut extra)?;
90            }
91        }
92        if verbose {
93            eprintln!("Got {} bytes from file", extra.len());
94        }
95        for (i, &b) in extra.iter().enumerate() {
96            bytes[i % 16] ^= b;
97        }
98    }
99
100    Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
101}
102
103fn getrandom(buf: &mut [u8]) -> io::Result<()> {
104    let mut f = File::open("/dev/urandom")?;
105    f.read_exact(buf)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn generates_32_hex_chars() {
114        let cookie = mcookie(None, None, false).unwrap();
115        assert_eq!(cookie.len(), 32);
116        assert!(cookie.chars().all(|c| c.is_ascii_hexdigit()));
117    }
118
119    #[test]
120    fn two_cookies_differ() {
121        let a = mcookie(None, None, false).unwrap();
122        let b = mcookie(None, None, false).unwrap();
123        assert_ne!(a, b);
124    }
125
126    #[test]
127    fn extra_file_mixed_in() {
128        let mut extra = &b"extra randomness here"[..];
129        let cookie = mcookie(Some(&mut extra), None, false).unwrap();
130        assert_eq!(cookie.len(), 32);
131        assert!(cookie.chars().all(|c| c.is_ascii_hexdigit()));
132    }
133
134    #[test]
135    fn max_size_limits_read() {
136        let mut extra = &b"extra randomness here"[..];
137        let cookie = mcookie(Some(&mut extra), Some(4), false).unwrap();
138        assert_eq!(cookie.len(), 32);
139        assert!(cookie.chars().all(|c| c.is_ascii_hexdigit()));
140    }
141}