cli: Support for pass(1) as credential storage

`pass(1)`[1] is a small CLI-based password manager. When passing
`--pass-path edu/kit/uXXXX` to `KIT-ILIAS-Downloader`, it now attempts
to retrieve the password from `pass(1)`.

It is assumed that `pass(1)` is available in the `$PATH` of the process. If
that's not the case, it errors out with an error like this:

    Error: credentials input failed

    Caused by:
        pass not found in $PATH!

It's also taken care of the case where the value `--pass-path` is not
present in the store, the error will look like this:

    Error: credentials input failed

    Caused by:
        `pass` failed with non-zero exit code 1: Error: edu/kit/uXXXX is not in the password store.

Closes #32

[1] https://www.passwordstore.org/
This commit is contained in:
Maximilian Bosch 2022-07-10 12:14:32 +02:00
parent f21c0249ed
commit 3a83840f15
No known key found for this signature in database
GPG Key ID: 091DBF4D1FC46B8E

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
use std::io::{Error, ErrorKind};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicBool, AtomicUsize}; use std::sync::atomic::{AtomicBool, AtomicUsize};
use anyhow::anyhow; use anyhow::anyhow;
@ -72,6 +74,10 @@ pub struct Opt {
#[structopt(short = "P", long)] #[structopt(short = "P", long)]
pub password: Option<String>, pub password: Option<String>,
/// Path inside `pass(1)` to the password for shibboleth.
#[structopt(long)]
pub pass_path: Option<String>,
/// ILIAS page to download /// ILIAS page to download
#[structopt(long)] #[structopt(long)]
pub sync_url: Option<String>, pub sync_url: Option<String>,
@ -170,6 +176,35 @@ pub fn ask_user_pass(opt: &Opt) -> Result<(String, String)> {
should_store = true; should_store = true;
} }
} }
} else if let Some(pass_path) = &opt.pass_path {
let pw_out = Command::new("pass")
.arg("show")
.arg(pass_path)
.output()
.map_err(|x| {
if x.kind() == ErrorKind::NotFound {
Error::new(ErrorKind::NotFound, "pass not found in $PATH!")
} else {
x
}
})?;
if !pw_out.status.success() {
return Err(Error::new(
ErrorKind::Other,
format!(
"`pass` failed with non-zero exit code {}: {}",
pw_out.status.code()
.expect("Failed retrieving pass exit code!"),
String::from_utf8(pw_out.stderr)
.expect("Failed decoding stderr of pass!")
)
))?
} else {
pass = String::from_utf8(pw_out.stdout).map(|x| {
x.trim_end().to_string()
}).expect("utf-8 decode of `pass(1)`-output failed");
should_store = false;
}
} else { } else {
pass = rpassword::prompt_password("Password: ").context("password prompt")?; pass = rpassword::prompt_password("Password: ").context("password prompt")?;
should_store = true; should_store = true;