Skip to content
This repository has been archived by the owner on May 11, 2023. It is now read-only.

auth: Read passphrase from env var or stdin #237

Merged
merged 4 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 24 additions & 44 deletions auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ use std::str::FromStr;

use anyhow::Context as _;
use radicle_common::signer::ToSigner;
use zeroize::Zeroizing;

use librad::crypto::keystore::pinentry::SecUtf8;
use librad::profile::ProfileId;

use radicle_common::args::{Args, Error, Help};
Expand All @@ -22,15 +20,19 @@ Usage

rad auth [--init | --active] [<options>...] [<profile>]

If `--init` is used, name and passphrase may be given via the `--name`
and `--passphrase` option. Using these disables the respective input prompt.
A passphrase may be given via the environment variable `RAD_PASSPHRASE` or
via the standard input stream if `--stdin` is used. Using one of these
methods disables the passphrase prompt.

If `--init` is used, a name may be given via the `--name` option. Using
this disables the input prompt.

Options

--init Initialize a new identity
--active Authenticate with the currently active profile
--stdin Read passphrase from stdin (default: false)
--name <name> Use given name (default: none)
--passphrase <phrase> Use given passphrase (default: none)
--help Print help
"#,
};
Expand All @@ -39,8 +41,8 @@ Options
pub struct Options {
pub init: bool,
pub active: bool,
pub stdin: bool,
pub name: Option<String>,
pub passphrase: Option<String>,
pub profile: Option<ProfileId>,
}

Expand All @@ -50,8 +52,8 @@ impl Args for Options {

let mut init = false;
let mut active = false;
let mut stdin = false;
let mut name = None;
let mut passphrase = None;
let mut profile = None;
let mut parser = lexopt::Parser::from_args(args);

Expand All @@ -63,6 +65,9 @@ impl Args for Options {
Long("active") => {
active = true;
}
Long("stdin") => {
stdin = true;
}
Long("name") if init && name.is_none() => {
let val = parser
.value()?
Expand All @@ -72,22 +77,6 @@ impl Args for Options {

name = Some(val);
}
Long("passphrase") if init && passphrase.is_none() => {
let val = parser
.value()?
.to_str()
.ok_or(anyhow::anyhow!(
"invalid passphrase specified with `--passphrase`"
))?
.to_owned();

term::warning(
"Passing a plain-text passphrase is considered insecure. \
Please only use for testing purposes.",
);

passphrase = Some(val);
}
Long("help") => {
return Err(Error::Help.into());
}
Expand All @@ -107,8 +96,8 @@ impl Args for Options {
Options {
init,
active,
stdin,
name,
passphrase,
profile,
},
vec![],
Expand Down Expand Up @@ -136,6 +125,7 @@ pub fn init(options: Options) -> anyhow::Result<()> {
term::headline("Initializing your 🌱 profile and identity");

let sock = keys::ssh_auth_sock();
let home = profile::home();

if git::check_version().is_err() {
term::warning(&format!(
Expand All @@ -150,22 +140,18 @@ pub fn init(options: Options) -> anyhow::Result<()> {
.name
.unwrap_or_else(|| term::text_input("Name", None).unwrap()),
)?;
let passphrase = options
.passphrase
.map_or_else(term::secret_input_with_confirmation, |passphrase| {
SecUtf8::from(passphrase)
});
let pwhash = keys::pwhash(passphrase.clone());
let home = profile::home();

let passphrase = term::read_passphrase(options.stdin, true)?;
let secret = keys::pwhash(passphrase.clone());

let mut spinner = term::spinner("Creating your 🌱 Ed25519 keypair...");
let (profile, peer_id) = profile::create(home, pwhash.clone())?;
let (profile, peer_id) = profile::create(home, secret.clone())?;

let signer = if let Ok(sock) = sock {
spinner.finish();
spinner = term::spinner("Adding to ssh-agent...");

keys::add(&profile, pwhash, sock.clone())?;
keys::add(&profile, secret, sock.clone())?;
let signer = sock.to_signer(&profile)?;

spinner.finish();
Expand Down Expand Up @@ -274,17 +260,11 @@ pub fn authenticate(

// TODO: We should show the spinner on the passphrase prompt,
// otherwise it seems like the passphrase is valid even if it isn't.
let secret_input: SecUtf8 = if atty::is(atty::Stream::Stdin) {
term::secret_input()
} else {
let mut input: Zeroizing<String> = Zeroizing::new(Default::default());
std::io::stdin().read_line(&mut input)?;
SecUtf8::from(input.trim_end())
};
let pass = keys::pwhash(secret_input);
let spinner = term::spinner("Unlocking...");
let passphrase = term::read_passphrase(options.stdin, false)?;
let secret = keys::pwhash(passphrase);

keys::add(profile, pass, sock).context("invalid passphrase supplied")?;
let spinner = term::spinner("Unlocking...");
keys::add(profile, secret, sock).context("invalid passphrase supplied")?;
spinner.finish();

term::success!("Radicle key added to ssh-agent");
Expand Down Expand Up @@ -316,8 +296,8 @@ mod tests {
Options {
active: false,
init: true,
stdin: false,
name: Some(name.to_owned()),
passphrase: Some(test::USER_PASS.to_owned()),
profile: None,
}
}
Expand Down
1 change: 1 addition & 0 deletions common/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod setup {

pub fn lnk_home() -> Result<(), BoxedError> {
env::set_var(LNK_HOME, env::current_dir()?.join("lnk_home"));
env::set_var(keys::RAD_PASSPHRASE, USER_PASS);
Ok(())
}

Expand Down
4 changes: 2 additions & 2 deletions scripts/contributor-workflow-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ mkdir --parents tmp/root
banner "MAINTAINER"
###################

rad auth --init --name cloudhead --passphrase cloudhead
echo cloudhead | rad auth --init --name cloudhead --stdin
MAINTAINER=$(rad self --profile)

# Create git repo
Expand Down Expand Up @@ -100,7 +100,7 @@ banner "CONTRIBUTOR"
mkdir --parents $BASE/tmp/contributor
cd $BASE/tmp/contributor

rad auth --init --name scooby --passphrase scooby
echo scooby | rad auth --init --name scooby --stdin
rad clone $PROJECT --seed $SEED_ADDR --no-confirm

CONTRIBUTOR=$(rad self --profile)
Expand Down
1 change: 1 addition & 0 deletions terminal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ anyhow = "1.0"
dialoguer = "0.10.0"
indicatif = "0.16.2"
console = "0.15"
zeroize = "1.1"
librad = { version = "0" }

[dependencies.radicle-common]
Expand Down
53 changes: 43 additions & 10 deletions terminal/src/io.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::fmt;
use std::str::FromStr;

use zeroize::Zeroizing;

use librad::crypto::keystore::pinentry::SecUtf8;
use librad::crypto::BoxedSigner;
use librad::profile::Profile;
Expand Down Expand Up @@ -277,16 +279,6 @@ pub fn secret_input() -> SecUtf8 {
secret_input_with_prompt("Passphrase")
}

pub fn secret_key(profile: &Profile) -> Result<keys::signer::ZeroizingSecretKey, anyhow::Error> {
let passphrase = secret_input();
let spinner = spinner("Unsealing key..."); // Nb. Spinner ends when dropped.
let key = keys::load_secret_key(profile, passphrase)?;

spinner.finish();

Ok(key)
}

// TODO: This prompt shows success just for entering a password,
// even if the password is later found out to be wrong.
// We should handle this differently.
Expand All @@ -310,6 +302,47 @@ pub fn secret_input_with_confirmation() -> SecUtf8 {
)
}

pub fn secret_stdin() -> Result<SecUtf8, anyhow::Error> {
let mut input: Zeroizing<String> = Zeroizing::new(Default::default());
std::io::stdin().read_line(&mut input)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't stdin().read_line() read into SecUtf8 type directly, bypassing Zeroizing? SecUtf8 does everything Zeroizing does and more, so maybe we could ditch the zeroize dependency altogether?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmh, it seems that it should work with the latest version of secstr: https://docs.rs/secstr/latest/secstr/struct.SecUtf8.html#method.unsecure_mut.

Unfortunately, we're on 0.3.2 though and it's not supported it that version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened a PR in radicle-keystore that updates secstr to 0.5.x: radicle-dev/radicle-keystore#35

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks! I Didn't realise it's going to require a version update 😬

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, we can fix this in a later PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked here: #240


Ok(SecUtf8::from(input.trim_end()))
}

pub fn read_passphrase(stdin: bool, confirm: bool) -> Result<SecUtf8, anyhow::Error> {
let passphrase = match read_passphrase_from_env_var() {
Ok(input) => input,
_ => {
if stdin {
secret_stdin()?
} else if confirm {
secret_input_with_confirmation()
} else {
secret_input()
}
}
};

Ok(passphrase)
}

pub fn read_passphrase_from_env_var() -> Result<SecUtf8, anyhow::Error> {
let env_var = std::env::var(keys::RAD_PASSPHRASE)?;
let input: Zeroizing<String> = Zeroizing::new(env_var);

Ok(SecUtf8::from(input.trim_end()))
}

pub fn secret_key(profile: &Profile) -> Result<keys::signer::ZeroizingSecretKey, anyhow::Error> {
let passphrase = secret_input();
let spinner = spinner("Unsealing key..."); // Nb. Spinner ends when dropped.
let key = keys::load_secret_key(profile, passphrase)?;

spinner.finish();

Ok(key)
}

pub fn select<'a, T>(options: &'a [T], active: &'a T) -> Option<&'a T>
where
T: fmt::Display + Eq + PartialEq,
Expand Down