read_secret/
lib.rs

1//! This rust library provides an easy way to read secrets from your environment and file.
2//! It also allows you to use external command like GPG to read keys.
3
4use std::{
5    env::{self, VarError},
6    error::Error,
7    fs::File,
8    io::{self, Read, Write},
9    path::Path,
10    process::{Child, Command, Stdio},
11};
12
13/// Type of Secret:
14pub enum SecretType {
15    /// environment variable name
16    Env(String),
17    /// file path relative to the project root or an absolute path
18    File(String),
19    /// secret string
20    String(String),
21}
22
23/// Method to decrypt a encoded secret
24pub enum DecryptMethod<'a> {
25    /// do nothing
26    None,
27    GPG,
28    /// Custom command used to decrypt a secret
29    Custom(&'a mut Command),
30}
31
32/// Provide an common entry to read secret
33pub fn read_secret(stype: SecretType, dm: &mut DecryptMethod) -> Result<String, Box<dyn Error>> {
34    let estr = match stype {
35        SecretType::Env(name) => read_env(&name).map_err(|e| Box::new(e) as Box<dyn Error>)?,
36        SecretType::File(path) => read_file(&path).map_err(|e| Box::new(e) as Box<dyn Error>)?,
37        SecretType::String(secret) => secret,
38    };
39    decrypt(estr, dm).map_err(|e| Box::new(e) as Box<dyn Error>)
40}
41
42/// Get a value of the given environment variable. If the given environment variable doesn't exist,
43/// env::VarError will be returned.
44pub fn read_env(env_name: &str) -> Result<String, VarError> {
45    env::var(env_name)
46}
47
48/// path relative to project root or an absolute path
49pub fn read_file(path: &str) -> io::Result<String> {
50    let path = Path::new(path);
51    let mut buf = String::new();
52    File::open(path)?.read_to_string(&mut buf)?;
53    Ok(buf)
54}
55
56/// Decrypt a encoded password using provided method
57fn decrypt(estr: String, dm: &mut DecryptMethod) -> io::Result<String> {
58    match dm {
59        DecryptMethod::None => Ok(estr),
60        DecryptMethod::GPG => {
61            let gpg = Command::new("gpg")
62                .args(["--no-tty", "-q", "-d", "-a"])
63                .stdin(Stdio::piped())
64                .stdout(Stdio::piped())
65                .spawn()
66                .expect("Failed to spawn `gpg`: please ensure you have it installed.");
67            let res = get_command_output(gpg, estr)?;
68            // remove the tailing new line character
69            let res = res.chars().take(res.len() - 1).collect();
70            Ok(res)
71        }
72        DecryptMethod::Custom(command) => {
73            let command = command
74                .stdin(Stdio::piped())
75                .stdout(Stdio::piped())
76                .spawn()
77                .expect("Failed to spawn command");
78            get_command_output(command, estr)
79        }
80    }
81}
82
83fn get_command_output(mut command: Child, input: String) -> io::Result<String> {
84    let mut stdin = (&mut command).stdin.take().expect("Failed to take stdin");
85    stdin.write_all(input.as_bytes())?;
86    drop(stdin);
87    let output = command.wait_with_output()?;
88    let res = String::from_utf8(output.stdout).expect("Cannot convert utf8 bytes into String.");
89    Ok(res)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    const LIB_VERSION: &str = "0.1.0";
96
97    #[test]
98    fn test_read_secret() -> Result<(), Box<dyn Error>> {
99        let mut dm = DecryptMethod::None;
100        let st = SecretType::Env("CARGO_PKG_VERSION".to_string());
101        let sr = read_secret(st, &mut dm)?;
102        assert_eq!(LIB_VERSION, sr);
103        Ok(())
104    }
105
106    #[test]
107    fn test_read_env() -> Result<(), VarError> {
108        // pass case
109        let s = read_env("CARGO_PKG_VERSION")?;
110        assert_eq!(LIB_VERSION, s);
111        // failed case
112        let failed = read_env("UNEXISTED_VALUE");
113        assert!(failed.is_err());
114        Ok(())
115    }
116
117    #[test]
118    fn test_read_file() -> io::Result<()> {
119        let sl = "El Psy Kongaroo";
120        let sr = read_file("/home/zarkli/projects/rust/read-secret/tests/pass_0")?;
121        assert_eq!(sl, sr);
122        let sr = read_file("tests/pass_0")?;
123        assert_eq!(sl, sr);
124        Ok(())
125    }
126
127    #[test]
128    fn test_decrypt() -> io::Result<()> {
129        let sl = "El Psy Kongaroo";
130        // DecryptMethod::None
131        let mut dm = DecryptMethod::None;
132        let sr = sl;
133        let sr = decrypt(sr.to_string(), &mut dm)?;
134        assert_eq!(sr, sl);
135
136        // DecryptMethod::GPG
137        let mut dm = DecryptMethod::GPG;
138        let sr = "-----BEGIN PGP MESSAGE-----
139
140hQGMAw4MNp4TmOFvAQwAuXN8xO+ca+Bz8bEFqnEB8cuxKYd0rCLa7UqN446DLnbj
1410g5IqyfhCgzNgbMN9LN3pYALwPrNEw6bSK6QoOn3ZtCOQKRSjH1WprRGUx3Fc+dO
142gDy8B79twcQyPFsy+3PbfDgQjxciNGuCXKBEp/cr+QFjAgX+wPTmoYv3xZGLHX5G
143tAsE9bB00AeyUdedDbn+V1YUW8mTZko4JtvXst3pRhaBHlina+MdaoFaQLzAhN0A
144jaVMrrm/L+WWAvrbdJvs8ew7QprENch2J0rXT5BY9tL8QRTnTLqrczQXtCLXMdk6
145nELkVDEvj/FloVKgGK10wj62eRgIp2eZOxY5GRkB6U8VEuDVzy9ryNWm8qiachsB
146GhibFWgjXOGxq/kEcmwZbzOC01KuqiGpI0MiGz3492detV2K2YGXsgbRZUwxb44B
147X0MFE43HySxRGdYZ7Q5E0HClTQedNw1YCo82DpELheGA9GOe1taQjX+gd98h5Fp0
148fmvb8an9JMgXwJDb0EHO0ksB6CaAfueLAR4sL8OpmUbeVg/kJv9fgvkHXvMMnFp+
149BafOEV3SWmeMynyfYk2g12wtph+Jm9EDq3PLokHAlfp0EYRqTSF0VDaHYdw=
150=LM7a
151-----END PGP MESSAGE-----";
152        let sr = decrypt(sr.to_string(), &mut dm)?;
153        assert_eq!(sl, sr);
154
155        // DecryptMethod::Custom
156        let mut binding = Command::new("wc");
157        let custom_command = binding.args(["-c"]);
158        let mut dm = DecryptMethod::Custom(custom_command);
159        let sr = sl;
160        let sr = decrypt(sr.to_string(), &mut dm)?;
161        assert_eq!("15\n", sr);
162        Ok(())
163    }
164}