~core/sage

ref: 78315f80a4134c20eba8bd551c8f0191053ed90f sage/src/wrapper.rs -rw-r--r-- 5.9 KiB
78315f80 — core chore: please thy lord clippy 21 days ago
                                                                                
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
use crate::db::Database;
use crate::identity::IdKeyData;
use anyhow::bail;
use pico_args::Arguments;
use std::os::unix::prelude::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Command, exit};
use std::str::FromStr;
use tempfile::NamedTempFile;

#[derive(Debug)]
enum Mode {
    Encrypt,
    Decrypt,
}

pub fn wrapper(mut pargs: Arguments, db_path: &Path) -> anyhow::Result<()> {
    let verbose = pargs.contains(["-v", "--verbose"]);
    let contains_encrypt = pargs.contains(["-e", "--encrypt"]);
    let contains_decrypt = pargs.contains(["-d", "--decrypt"]);

    if contains_encrypt && contains_decrypt {
        bail!("cannot specify both --encrypt and --decrypt, pick one");
    }

    let mode = if contains_decrypt {
        Mode::Decrypt
    } else {
        Mode::Encrypt
    };
    let output: Option<PathBuf> = pargs.opt_value_from_str(["-o", "--output"])?;
    let armor = pargs.contains(["-a", "--armor"]);
    let passphrase = pargs.contains(["-p", "--passphrase"]);

    let mut recipients = vec![];
    loop {
        let maybe_r: Option<String> = pargs.opt_value_from_str(["-r", "--recipient"])?;

        if let Some(r) = maybe_r {
            recipients.push(r);
        } else {
            break;
        }
    }

    if let Ok(Some(f)) = pargs.opt_value_from_str::<[&str; 2], PathBuf>(["-R", "--recipients-file"])
    {
        recipients.extend(load_keyfile(&f)?);
    }

    let mut identities = vec![];
    loop {
        let maybe_i: Option<String> = pargs.opt_value_from_str(["-i", "--identity"])?;

        if let Some(i) = maybe_i {
            let Ok(path) = PathBuf::from_str(&i);
            if path.exists() {
                identities.extend(load_keyfile(&path)?);
            } else {
                identities.push(i);
            }
        } else {
            break;
        }
    }

    let input: Option<PathBuf> = pargs.opt_free_from_str()?;

    if verbose {
        println!(
            "resolved args: {:?} output={:?} armor?{:?} passphrase?{:?} recipients=>{:?} identities=>{:?} input={:?}",
            mode, output, armor, passphrase, recipients, identities, input
        );
    }

    // Resolve recipients and identities to the database
    let db = Database::load_or_create(db_path)?;

    // resolve recipients
    let resolved_recipients = recipients
        .iter()
        .map(|u| {
            // is this already a valid key?
            if u.starts_with("age1") {
                // regular ol' pubkey
                return u.clone();
            }
            // otherwise, resolve it in the database
            let results = db.fuzzy_search(u.clone());
            if results.is_empty() {
                eprintln!("no results for recipient {u}");
                exit(1);
            }
            if results.len() > 1 {
                eprintln!("recipient {u} is ambiguous, please narrow it down");
                exit(1);
            }
            let r = &results[0];

            if verbose {
                eprintln!(
                    "resolved rcpt {u} => {:?} (keyid {})",
                    r,
                    r.keys.keyid().unwrap()
                );
            }

            r.keys.pk().unwrap()
        })
        .collect::<Vec<_>>();
    // resolve identities
    let resolved_identities = identities.iter()
        .map(|u| {
            // is this already a valid key?
            if u.starts_with("AGE-SECRET-KEY") {
                // regular ol' sk
                return u.clone()
            }
            // otherwise, resolve it in the database
            let results = db.fuzzy_search(u.clone());
            if results.is_empty() {
                eprintln!("no results for identity {u}");
                exit(1);
            }
            if results.len() > 1 {
                eprintln!("identity {u} is ambiguous, please narrow it down");
                exit(1);
            }
            let r = &results[0];

            if verbose {
                eprintln!("resolved id {u} => {:?} (keyid {})", r, r.keys.keyid().unwrap());
            }

            match &r.keys {
                IdKeyData::Local(sk) => sk.clone(),
                IdKeyData::Peer(_) => {
                    eprintln!("resolved identity is a peer key, not a local key. perhaps revise your search terms?");
                    exit(1);
                }
            }
        })
        .collect::<Vec<_>>();

    if verbose {
        eprintln!("resolved recipients: {:#?}", resolved_recipients);
    }

    // write recipients to a temp file
    let r_tmpfile = NamedTempFile::new()?;
    std::fs::write(r_tmpfile.path(), resolved_recipients.join("\n"))?;
    let i_tmpfile = NamedTempFile::new()?;
    std::fs::write(i_tmpfile.path(), resolved_identities.join("\n"))?;

    // execute age
    let mut args = vec![];
    args.push(
        match mode {
            Mode::Encrypt => "--encrypt",
            Mode::Decrypt => "--decrypt",
        }
        .to_string(),
    );
    if let Some(o) = output {
        args.push("--output".to_string());
        args.push(o.display().to_string());
    }
    if armor {
        args.push("--armor".to_string());
    }
    if passphrase {
        args.push("--passphrase".to_string());
    }
    if !resolved_recipients.is_empty() {
        args.push("--recipients-file".to_string());
        args.push(r_tmpfile.path().display().to_string());
    }
    if !resolved_identities.is_empty() {
        args.push("--identity".to_string());
        args.push(i_tmpfile.path().display().to_string());
    }
    if let Some(i) = input {
        args.push(i.display().to_string());
    }

    if verbose {
        eprintln!("exec age {:?}", args);
    }

    Err(Command::new("age").args(args).exec())?;
    Ok(())
}

fn load_keyfile(path: &Path) -> anyhow::Result<Vec<String>> {
    let contents = std::fs::read_to_string(path)?;
    Ok(contents
        .lines()
        .filter(|u| !u.is_empty())
        .filter(|u| !u.starts_with("#"))
        .map(|u| u.to_string())
        .collect())
}