mod args; use std::str::FromStr; use anyhow::{Context, anyhow}; use radicle::crypto::ssh; use radicle::crypto::ssh::Passphrase; use radicle::node::Alias; use radicle::profile::env; use radicle::{Profile, profile}; use crate::terminal as term; pub use args::Args; pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { match ctx.profile() { Ok(profile) => authenticate(args, &profile), Err(_) => init(args), } } pub fn init(args: Args) -> anyhow::Result<()> { term::headline("Initializing your Radicle 👾 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!("A Git installation is required for Radicle to run."); } let alias: Alias = if let Some(alias) = args.alias { alias } else { let user = env::var("USER").ok().and_then(|u| Alias::from_str(&u).ok()); let user = term::input( "Enter your alias:", user, Some("This is your node alias. You can always change it later"), )?; user.ok_or_else(|| anyhow::anyhow!("An alias is required for Radicle to run."))? }; let home = profile::home()?; let passphrase = if args.stdin { Some(term::passphrase_stdin()?) } else { term::passphrase_confirm("Enter a passphrase:", env::RAD_PASSPHRASE)? }; let passphrase = passphrase.filter(|passphrase| !passphrase.trim().is_empty()); let spinner = term::spinner("Creating your Ed25519 keypair..."); let profile = Profile::init(home, alias, passphrase.clone(), env::seed())?; let mut agent = true; spinner.finish(); if let Some(passphrase) = passphrase { match ssh::agent::Agent::connect() { Ok(mut agent) => { let mut spinner = term::spinner("Adding your Radicle key to ssh-agent..."); if register(&mut agent, &profile, passphrase).is_ok() { spinner.finish(); } else { spinner.message("Could not register Radicle key in ssh-agent."); spinner.warn(); } } Err(e) if e.is_not_running() => { agent = false; } Err(e) => Err(e).context("failed to connect to ssh-agent")?, } } term::success!( "Your Radicle DID is {}. This identifies your device. Run {} to show it at all times.", term::format::highlight(profile.did()), term::format::command("rad self") ); term::success!("You're all set."); term::blank(); if profile.config.cli.hints && !agent { term::hint("install ssh-agent to have it fill in your passphrase for you when signing."); term::blank(); } term::info!( "To create a Radicle repository, run {} from a Git repository with at least one commit.", term::format::command("rad init") ); term::info!( "To clone a repository, run {}. For example, {} clones the Radicle 'heartwood' repository.", term::format::command("rad clone "), term::format::command("rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5") ); term::info!( "To get a list of all commands, run {}.", term::format::command("rad"), ); Ok(()) } /// Try loading the identity's key into SSH Agent, falling back to verifying `RAD_PASSPHRASE` for /// use. pub fn authenticate(args: Args, profile: &Profile) -> anyhow::Result<()> { if !profile.keystore.is_encrypted()? { term::success!("Authenticated as {}", term::format::tertiary(profile.id())); return Ok(()); } for (key, _) in &profile.config.node.extra { term::warning(format!( "unused or deprecated configuration attribute {key:?}" )); } // If our key is encrypted, we try to authenticate with SSH Agent and // register it; only if it is running. match ssh::agent::Agent::connect() { Ok(mut agent) => { if agent.request_identities()?.contains(&profile.public_key) { term::success!("Radicle key already in ssh-agent"); return Ok(()); } let passphrase = if let Some(phrase) = profile::env::passphrase() { phrase } else if args.stdin { term::passphrase_stdin()? } else if let Some(passphrase) = term::io::passphrase(term::io::PassphraseValidator::new(profile.keystore.clone()))? { passphrase } else { anyhow::bail!( "A passphrase is required to read your Radicle key. Unable to continue." ) }; register(&mut agent, profile, passphrase)?; term::success!("Radicle key added to {}", term::format::dim("ssh-agent")); return Ok(()); } Err(e) if e.is_not_running() => {} Err(e) => Err(e)?, }; // Try RAD_PASSPHRASE fallback. if let Some(passphrase) = profile::env::passphrase() { ssh::keystore::MemorySigner::load(&profile.keystore, Some(passphrase)) .map_err(|_| anyhow!("`{}` is invalid", env::RAD_PASSPHRASE))?; return Ok(()); } term::println(term::format::dim( "Nothing to do, ssh-agent is not running.", )); term::println(term::format::dim( "You will be prompted for a passphrase when necessary.", )); Ok(()) } /// Register key with ssh-agent. pub fn register( agent: &mut ssh::agent::Agent, profile: &Profile, passphrase: Passphrase, ) -> anyhow::Result<()> { let secret = profile .keystore .secret_key(Some(passphrase)) .map_err(|e| { if e.is_crypto_err() { anyhow!("could not decrypt secret key: invalid passphrase") } else { e.into() } })? .ok_or_else(|| anyhow!("Key not found in {:?}", profile.keystore.secret_key_path()))?; agent.register(&secret)?; Ok(()) }