pass_fu/
cli.rs

1use crate::{
2    err::{assert_child_io, assert_child_status, Errors, LogicError, PassFuResult},
3    opt::{Command, Config, Opt, PassOpt, ShowMode, ShowOpt},
4};
5use std::{
6    ffi::OsString,
7    io::{self, BufRead, BufReader, Write},
8    iter::once,
9    path::{Component, PathBuf},
10    process::Stdio,
11};
12
13pub fn main() {
14    if let Err(err) = run(std::env::args_os(), std::env::vars_os()) {
15        eprintln!("Error {err}");
16        std::process::exit(err.code())
17    }
18}
19
20pub fn run(
21    args: impl IntoIterator<Item = OsString>,
22    env: impl IntoIterator<Item = (OsString, OsString)>,
23) -> PassFuResult<()> {
24    let Opt {
25        config, command, ..
26    } = Opt::parse(args, env)
27        .map_err(LogicError::new)
28        .ctx("parsing arguments")?;
29
30    match command {
31        Command::Show(opt) => search(config, opt),
32        Command::Other(opt) => pass_through(config, opt),
33    }
34}
35fn pass_through(cfg: Config, opt: PassOpt) -> PassFuResult<()> {
36    let status = std::process::Command::new(&cfg.pass)
37        .args(once(opt.command).chain(opt.rest))
38        .status();
39
40    assert_child_status(cfg.pass.to_str(), status)
41}
42
43fn search(cfg: Config, mut opt: ShowOpt) -> PassFuResult<()> {
44    opt.path = {
45        let paths = find(&cfg, &opt).ctx("finding paths")?;
46        pick(&cfg, paths).ctx("picking a path")?.map(|p| p.into())
47    };
48
49    const OTP_CODE: &str = "OTP code";
50    let (mut content, line) = if opt.line.is_empty() {
51        // we do not know exactly which part of the file do we need.
52        let content = fetch(&cfg, &opt).ctx("fetching content")?;
53
54        let choice = {
55            // ask if a single line or whole file
56            let mut choices = vec![];
57            for (i, l) in content.iter().enumerate() {
58                let mut l = l.as_str();
59                if l.trim().starts_with("otpauth://") {
60                    l = "OTP URI";
61                    choices.push(format!("{i}: {OTP_CODE}"));
62                } else if i == 0 {
63                    l = "the main password";
64                } else {
65                    l = "*******";
66                }
67                choices.push(format!("{i}: {l}"));
68            }
69
70            if let [single] = &mut choices[..] {
71                std::mem::take(single)
72            } else {
73                choices.insert(0, "All".to_owned());
74                pick(&cfg, choices)
75                    .ctx("picking a line")?
76                    .unwrap_or_default()
77            }
78        };
79
80        let (line, kind) = choice
81            .split_once(":")
82            .map(|(line, kind)| (line.parse::<usize>().ok(), kind))
83            .unwrap_or_default();
84
85        if kind.trim() == OTP_CODE {
86            let code = match line {
87                Some(l) => otp(&content[l])?,
88                None => otp(&content.join("\n"))?,
89            };
90            (vec![code], "0".to_owned())
91        } else {
92            (content, line.map(|l| l.to_string()).unwrap_or_default())
93        }
94    } else {
95        (vec![], opt.line)
96    };
97    opt.line = line;
98
99    match &opt.mode {
100        ShowMode::Clip | ShowMode::QrCode => {
101            // we have a copy or qr request on command line
102            // this will be taken care of by pass now
103            // might avoid loading the content
104            return send(&cfg, &opt);
105        }
106        _ => {
107            // we will handle it
108        }
109    }
110
111    if content.is_empty() {
112        content = fetch(&cfg, &opt).ctx("loading content2")?;
113    }
114    eprintln!("line {}", opt.line);
115    let secret = if opt.line.is_empty() {
116        content.join("\n")
117    } else {
118        let line = opt
119            .line
120            .parse::<usize>()
121            .map_err(LogicError::new)
122            .ctx(format!("parsing line number for {:?} option", opt.mode))?;
123        content.get(line).map(|l| l.to_owned()).unwrap_or_default()
124    };
125
126    match opt.mode {
127        ShowMode::Output => print!("{}", secret),
128        ShowMode::Clip => unreachable!("this is handled by pass"),
129        ShowMode::QrCode => unreachable!("this is handled by pass"),
130        ShowMode::Type => typeit(&cfg, &secret)?,
131    }
132
133    Ok(())
134}
135/// Find all relevant entries
136fn find(cfg: &Config, opt: &ShowOpt) -> PassFuResult<Vec<String>> {
137    let mut base = cfg.dir.clone();
138    if let Some(ref p) = opt.path {
139        // we consider it a base folder or an explicit file like pass
140        base.extend(p.components().filter(|c| !matches!(c, Component::RootDir)));
141        if !base.exists() {
142            // not an existing dir... is it a file?
143            let mut basefile = base.as_os_str().to_owned();
144            basefile.push(".gpg");
145            let basefile: PathBuf = basefile.into();
146            if basefile.exists() && (basefile.is_file() || basefile.is_symlink()) {
147                // it's a file, no finding needed!
148                return Ok(vec![p.to_str().unwrap_or_default().to_owned()]);
149            } else {
150                // it's not a dir nor file, this is probably going to fail.
151                eprintln!("base path does not exist {basefile:?}");
152            }
153        }
154    }
155
156    base =
157        assert_child_io(&cfg.find, base.canonicalize()).ctx(format!("searching dir {base:?}"))?;
158
159    // find "$PASSWORD_STORE_DIR" -iname '*.gpg'
160    let child = std::process::Command::new(&cfg.find)
161        .arg(base)
162        .args(["-iname", "*.gpg"])
163        .stdout(Stdio::piped())
164        .stderr(Stdio::inherit())
165        .spawn();
166    let mut child = assert_child_io(&cfg.find, child)?;
167    let mut paths = vec![];
168    for line in BufReader::new(child.stdout.as_mut().expect("find output as instructed")).lines() {
169        let path = assert_child_io(
170            &cfg.find,
171            PathBuf::from(assert_child_io(&cfg.find, line)?)
172                .with_extension("")
173                .strip_prefix(&cfg.dir)
174                .map(|path| path.to_str().unwrap_or_default().to_owned())
175                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)),
176        )?;
177        paths.push(path);
178    }
179
180    assert_child_status(&cfg.find, child.wait())?;
181    Ok(paths)
182}
183/// Let the user chose which entry to use
184fn pick(cfg: &Config, items: Vec<String>) -> PassFuResult<Option<String>> {
185    if items.len() == 0 {
186        // no entries, empty
187        return Ok(None);
188    }
189    if items.len() == 1 {
190        // single entry, found
191        return Ok(items.into_iter().next());
192    }
193
194    // multiple entries, invoke picker
195    let child = std::process::Command::new(&cfg.picker)
196        .args(&cfg.picker_args)
197        .stdin(Stdio::piped())
198        .stdout(Stdio::piped())
199        .stderr(Stdio::inherit())
200        .spawn();
201    let mut child = assert_child_io(&cfg.picker, child)?;
202
203    let picker = cfg.picker.clone();
204    let mut feed = child.stdin.take().expect("picker input as instructed");
205    std::thread::spawn(move || {
206        for path in items {
207            assert_child_io(&picker, feed.write_all(path.as_bytes())).unwrap();
208            assert_child_io(&picker, feed.write_all(b"\n")).unwrap();
209        }
210        assert_child_io(&picker, feed.flush()).unwrap();
211    });
212
213    let mut paths = vec![];
214    for path in BufReader::new(child.stdout.as_mut().expect("picker output as instructed")).lines()
215    {
216        paths.push(assert_child_io(&cfg.picker, path)?);
217    }
218
219    assert_child_status(&cfg.picker, child.wait())?;
220
221    Ok(match &mut paths[..] {
222        [] => Err(LogicError::new("No entries picked")),
223        [_, _, ..] => Err(LogicError::new("Too many entries picked")),
224        [single] => Ok(Some(std::mem::take(single))),
225    }?)
226}
227/// Fetch the pass secret
228fn fetch(cfg: &Config, opt: &ShowOpt) -> PassFuResult<Vec<String>> {
229    let child = std::process::Command::new(&cfg.pass)
230        .args(&opt.path)
231        .stdout(Stdio::piped())
232        .stderr(Stdio::inherit())
233        .stdin(Stdio::piped())
234        .spawn();
235    let mut child = assert_child_io(&cfg.pass, child)?;
236
237    let mut lines = vec![];
238
239    for line in BufReader::new(child.stdout.as_mut().expect("pass output as instructed")).lines() {
240        lines.push(assert_child_io(&cfg.pass, line)?);
241    }
242
243    assert_child_status(&cfg.pass, child.wait())?;
244    Ok(lines)
245}
246
247/// Generate OTP token
248fn otp(otpauth_url: &str) -> PassFuResult<String> {
249    let totp = totp_rs::TOTP::from_url(otpauth_url)?;
250    Ok(totp.generate_current()?)
251}
252/// Send final instructions to pass
253fn send(cfg: &Config, opt: &ShowOpt) -> PassFuResult<()> {
254    let option_fmt = |o: &str, v: &String| {
255        if v.is_empty() {
256            o.to_owned()
257        } else {
258            format!("{o}={v}")
259        }
260    };
261    let mode_arg = match opt.mode {
262        ShowMode::Output if opt.line.is_empty() => {
263            /*noop*/
264            None
265        }
266        ShowMode::Clip => Some(option_fmt("--clip", &opt.line)),
267        ShowMode::QrCode => Some(option_fmt("--qrcode", &opt.line)),
268        ref mode => unimplemented!("The original pass does not implement {mode:?}"),
269    };
270
271    let status = std::process::Command::new(&cfg.pass)
272        .args(mode_arg)
273        .args(&opt.path)
274        .status();
275
276    assert_child_status(&cfg.pass, status)
277}
278
279/// Send final instructions to pass
280fn typeit(cfg: &Config, secret: &str) -> PassFuResult<()> {
281    let child = std::process::Command::new(&cfg.typist)
282        .args(&cfg.typist_args)
283        .stdout(Stdio::inherit())
284        .stderr(Stdio::inherit())
285        .stdin(Stdio::piped())
286        .spawn();
287    let mut child = assert_child_io(&cfg.picker, child)?;
288    let feed = child.stdin.as_mut().expect("typist input as instructed");
289    assert_child_io(&cfg.typist, feed.write_all(secret.as_bytes())).unwrap();
290    assert_child_status(&cfg.pass, child.wait())
291}