entrust_core/
backend.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
pub mod age;
pub mod gpg;

use anyhow::anyhow;
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::path::Path;
use std::process::{ExitStatus, Output};

#[derive(Clone, Copy, Debug)]
pub enum Backend {
    Age,
    Gpg,
}

impl Backend {
    pub fn encrypt(
        &self,
        mut content: impl Read,
        store: &Path,
        out_path: &Path,
    ) -> anyhow::Result<()> {
        match self {
            Backend::Age => {
                age::encrypt(&mut content, &self.recipient(store)?, out_path)?;
            }
            Backend::Gpg => {
                gpg::encrypt(&mut content, &self.recipient(store)?, out_path)?;
            }
        }
        Ok(())
    }

    pub fn decrypt(path: &Path) -> anyhow::Result<String> {
        if is_age_encrypted(path)? {
            age::decrypt(path)
        } else {
            gpg::decrypt(path)
        }
    }

    pub fn display_name(&self) -> &'static str {
        match self {
            Backend::Age => "age",
            Backend::Gpg => "gpg",
        }
    }

    pub fn recipient_file_name(&self) -> &'static str {
        match self {
            Backend::Age => age::RECIPIENT_FILE_NAME,
            Backend::Gpg => gpg::RECIPIENT_FILE_NAME,
        }
    }

    pub fn needs_init(self, store: &Path) -> Option<Backend> {
        if store.join(self.recipient_file_name()).exists() {
            None
        } else {
            Some(self)
        }
    }

    fn recipient(&self, dir: &Path) -> anyhow::Result<String> {
        let recipient_file = dir.join(self.recipient_file_name());
        read_first_line(&recipient_file)
    }
}

fn is_age_encrypted(path: &Path) -> anyhow::Result<bool> {
    let first_line = read_first_line(path)?;
    Ok(
        first_line.contains("BEGIN AGE ENCRYPTED FILE")
            || first_line.contains("age-encryption.org"),
    )
}

fn read_first_line(path: &Path) -> anyhow::Result<String> {
    let file = File::open(path)?;
    let first_line = BufReader::new(file)
        .lines()
        .next()
        .ok_or(anyhow!("{path:?} is empty"))??;
    Ok(first_line)
}

fn exit_status_to_result(exit_status: ExitStatus, bin_name: &str) -> anyhow::Result<()> {
    if exit_status.success() {
        Ok(())
    } else {
        Err(anyhow!(
            "{bin_name} failed with exit code {}",
            exit_status
                .code()
                .map(|c| c.to_string())
                .unwrap_or_else(|| "<unknown>".to_string())
        ))
    }
}

fn output_to_result(mut output: Output) -> anyhow::Result<String> {
    if output.status.success() {
        while [Some(&b'\r'), Some(&b'\n')].contains(&output.stdout.last()) {
            output.stdout.pop();
        }
        Ok(String::from_utf8(output.stdout)?)
    } else {
        Err(anyhow!(String::from_utf8(output.stderr)?))
    }
}