#![allow(clippy::or_fun_call)] use std::ffi::OsString; use anyhow::anyhow; use radicle::crypto::ssh; use radicle::{profile, Profile}; use crate::terminal as term; use crate::terminal::args::{Args, Error, Help}; pub const HELP: Help = Help { name: "auth", description: "Manage identities and profiles", version: env!("CARGO_PKG_VERSION"), usage: r#" Usage rad auth [...] 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. Options --stdin Read passphrase from stdin (default: false) --help Print help "#, }; #[derive(Debug)] pub struct Options { pub stdin: bool, } impl Args for Options { fn from_args(args: Vec) -> anyhow::Result<(Self, Vec)> { use lexopt::prelude::*; let mut stdin = false; let mut parser = lexopt::Parser::from_args(args); while let Some(arg) = parser.next()? { match arg { Long("stdin") => { stdin = true; } Long("help") => { return Err(Error::Help.into()); } _ => return Err(anyhow::anyhow!(arg.unexpected())), } } Ok((Options { stdin }, vec![])) } } pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { match ctx.profile() { Ok(profile) => authenticate(&profile, options), Err(_) => init(options), } } pub fn init(options: Options) -> anyhow::Result<()> { term::headline("Initializing your 🌱 profile and identity"); if let Ok(version) = radicle::git::version() { if version < radicle::git::VERSION_REQUIRED { term::warning(&format!( "Your git version is unsupported, please upgrade to {} or later", radicle::git::VERSION_REQUIRED, )); term::blank(); } } else { anyhow::bail!("Error retrieving git version; please check your installation"); } let home = profile::home()?; let passphrase = if options.stdin { term::passphrase_stdin() } else { term::passphrase_confirm() }?; let spinner = term::spinner("Creating your 🌱 Ed25519 keypair..."); let profile = Profile::init(home, passphrase)?; spinner.finish(); term::success!( "Your Radicle ID is {}. This identifies your device.", term::format::highlight(profile.did()) ); term::blank(); term::tip!( "To create a radicle project, run {} from a git repository.", term::format::secondary("`rad init`") ); Ok(()) } pub fn authenticate(profile: &Profile, options: Options) -> anyhow::Result<()> { let agent = ssh::agent::Agent::connect()?; // TODO: Only show this if we're not authenticated. term::headline(&format!( "🌱 Authenticating as {}", term::format::Identity::new(profile).styled() )); let profile = &profile; if !agent.signer(profile.public_key).is_ready()? { term::warning("Adding your radicle key to ssh-agent..."); // TODO: We should show the spinner on the passphrase prompt, // otherwise it seems like the passphrase is valid even if it isn't. let passphrase = if options.stdin { term::passphrase_stdin() } else { term::passphrase() }?; let spinner = term::spinner("Unlocking..."); let mut agent = ssh::agent::Agent::connect()?; let secret = profile .keystore .secret_key(passphrase)? .ok_or_else(|| anyhow!("Key not found in {:?}", profile.keystore.path()))?; agent.register(&secret)?; spinner.finish(); term::success!("Radicle key added to ssh-agent"); } else { term::success!("Signing key already in ssh-agent"); }; Ok(()) }