entrust_core/
backend.rs

1pub mod age;
2pub mod gpg;
3
4use anyhow::anyhow;
5use std::fs::File;
6use std::io::{BufRead, BufReader, Read};
7use std::path::Path;
8use std::process::{ExitStatus, Output};
9
10#[derive(Clone, Copy, Debug)]
11pub enum Backend {
12    Age,
13    Gpg,
14}
15
16impl Backend {
17    pub fn encrypt(
18        &self,
19        mut content: impl Read,
20        store: &Path,
21        out_path: &Path,
22    ) -> anyhow::Result<()> {
23        match self {
24            Backend::Age => {
25                age::encrypt(&mut content, &self.recipient(store)?, out_path)?;
26            }
27            Backend::Gpg => {
28                gpg::encrypt(&mut content, &self.recipient(store)?, out_path)?;
29            }
30        }
31        Ok(())
32    }
33
34    pub fn decrypt(path: &Path) -> anyhow::Result<String> {
35        if is_age_encrypted(path)? {
36            age::decrypt(path)
37        } else {
38            gpg::decrypt(path)
39        }
40    }
41
42    pub fn display_name(&self) -> &'static str {
43        match self {
44            Backend::Age => "age",
45            Backend::Gpg => "gpg",
46        }
47    }
48
49    pub fn recipient_file_name(&self) -> &'static str {
50        match self {
51            Backend::Age => age::RECIPIENT_FILE_NAME,
52            Backend::Gpg => gpg::RECIPIENT_FILE_NAME,
53        }
54    }
55
56    pub fn needs_init(self, store: &Path) -> Option<Backend> {
57        if store.join(self.recipient_file_name()).exists() {
58            None
59        } else {
60            Some(self)
61        }
62    }
63
64    fn recipient(&self, dir: &Path) -> anyhow::Result<String> {
65        let recipient_file = dir.join(self.recipient_file_name());
66        read_first_line(&recipient_file)
67    }
68}
69
70fn is_age_encrypted(path: &Path) -> anyhow::Result<bool> {
71    let first_line = read_first_line(path)?;
72    Ok(
73        first_line.contains("BEGIN AGE ENCRYPTED FILE")
74            || first_line.contains("age-encryption.org"),
75    )
76}
77
78fn read_first_line(path: &Path) -> anyhow::Result<String> {
79    let file = File::open(path)?;
80    let first_line = BufReader::new(file)
81        .lines()
82        .next()
83        .ok_or(anyhow!("{path:?} is empty"))??;
84    Ok(first_line)
85}
86
87fn exit_status_to_result(exit_status: ExitStatus, bin_name: &str) -> anyhow::Result<()> {
88    if exit_status.success() {
89        Ok(())
90    } else {
91        Err(anyhow!(
92            "{bin_name} failed with exit code {}",
93            exit_status
94                .code()
95                .map(|c| c.to_string())
96                .unwrap_or_else(|| "<unknown>".to_string())
97        ))
98    }
99}
100
101fn output_to_result(mut output: Output) -> anyhow::Result<String> {
102    if output.status.success() {
103        while [Some(&b'\r'), Some(&b'\n')].contains(&output.stdout.last()) {
104            output.stdout.pop();
105        }
106        Ok(String::from_utf8(output.stdout)?)
107    } else {
108        Err(anyhow!(String::from_utf8(output.stderr)?))
109    }
110}