auth.rs
1 #![allow(clippy::or_fun_call)] 2 use std::env; 3 use std::ffi::OsString; 4 use std::ops::Not as _; 5 use std::str::FromStr; 6 7 use anyhow::anyhow; 8 9 use radicle::crypto::ssh; 10 use radicle::crypto::ssh::Passphrase; 11 use radicle::node::Alias; 12 use radicle::profile::env::RAD_PASSPHRASE; 13 use radicle::{profile, Profile}; 14 15 use crate::terminal as term; 16 use crate::terminal::args::{Args, Error, Help}; 17 18 pub const HELP: Help = Help { 19 name: "auth", 20 description: "Manage identities and profiles", 21 version: env!("CARGO_PKG_VERSION"), 22 usage: r#" 23 Usage 24 25 rad auth [<option>...] 26 27 A passphrase may be given via the environment variable `RAD_PASSPHRASE` or 28 via the standard input stream if `--stdin` is used. Using either of these 29 methods disables the passphrase prompt. 30 31 Options 32 33 --alias When initializing an identity, sets the node alias 34 --stdin Read passphrase from stdin (default: false) 35 --help Print help 36 "#, 37 }; 38 39 #[derive(Debug)] 40 pub struct Options { 41 pub stdin: bool, 42 pub alias: Option<Alias>, 43 } 44 45 impl Args for Options { 46 fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> { 47 use lexopt::prelude::*; 48 49 let mut stdin = false; 50 let mut alias = None; 51 let mut parser = lexopt::Parser::from_args(args); 52 53 while let Some(arg) = parser.next()? { 54 match arg { 55 Long("alias") => { 56 let val = parser.value()?; 57 let val = term::args::alias(&val)?; 58 59 alias = Some(val); 60 } 61 Long("stdin") => { 62 stdin = true; 63 } 64 Long("help") | Short('h') => { 65 return Err(Error::Help.into()); 66 } 67 _ => return Err(anyhow::anyhow!(arg.unexpected())), 68 } 69 } 70 71 Ok((Options { alias, stdin }, vec![])) 72 } 73 } 74 75 pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { 76 match ctx.profile() { 77 Ok(profile) => authenticate(options, &profile), 78 Err(_) => init(options), 79 } 80 } 81 82 pub fn init(options: Options) -> anyhow::Result<()> { 83 term::headline("Initializing your radicle 👾 identity"); 84 85 if let Ok(version) = radicle::git::version() { 86 if version < radicle::git::VERSION_REQUIRED { 87 term::warning(format!( 88 "Your git version is unsupported, please upgrade to {} or later", 89 radicle::git::VERSION_REQUIRED, 90 )); 91 term::blank(); 92 } 93 } else { 94 anyhow::bail!("Error retrieving git version; please check your installation"); 95 } 96 97 let alias: Alias = if let Some(alias) = options.alias { 98 alias 99 } else { 100 let user = env::var("USER").ok().and_then(|u| Alias::from_str(&u).ok()); 101 term::input( 102 "Enter your alias:", 103 user, 104 Some("This is your node alias. You can always change it later"), 105 )? 106 }; 107 let home = profile::home()?; 108 let passphrase = if options.stdin { 109 term::passphrase_stdin() 110 } else { 111 term::passphrase_confirm("Enter a passphrase:", RAD_PASSPHRASE) 112 }?; 113 let passphrase = passphrase.trim().is_empty().not().then_some(passphrase); 114 let spinner = term::spinner("Creating your Ed25519 keypair..."); 115 let profile = Profile::init(home, alias, passphrase.clone())?; 116 spinner.finish(); 117 118 if let Some(passphrase) = passphrase { 119 match ssh::agent::Agent::connect() { 120 Ok(mut agent) => { 121 let mut spinner = term::spinner("Adding your radicle key to ssh-agent..."); 122 if register(&mut agent, &profile, passphrase).is_ok() { 123 spinner.finish(); 124 } else { 125 spinner.message("Could not register radicle key in ssh-agent."); 126 spinner.warn(); 127 } 128 } 129 Err(e) if e.is_not_running() => {} 130 Err(e) => Err(e)?, 131 } 132 } 133 134 term::success!( 135 "Your Radicle DID is {}. This identifies your device. Run {} to show it at all times.", 136 term::format::highlight(profile.did()), 137 term::format::command("rad self") 138 ); 139 term::success!("You're all set."); 140 term::blank(); 141 term::info!( 142 "To create a radicle project, run {} from a Git repository with at least one commit.", 143 term::format::command("rad init") 144 ); 145 term::info!( 146 "To clone a project, run {}. For example, {} clones the Radicle 'heartwood' project.", 147 term::format::command("rad clone <rid>"), 148 term::format::command("rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5") 149 ); 150 term::info!( 151 "To get a list of all commands, run {}.", 152 term::format::command("rad help"), 153 ); 154 155 Ok(()) 156 } 157 158 /// Try loading the identity's key into SSH Agent, falling back to verifying `RAD_PASSPHRASE` for 159 /// use. 160 pub fn authenticate(options: Options, profile: &Profile) -> anyhow::Result<()> { 161 if !profile.keystore.is_encrypted()? { 162 term::success!("Authenticated as {}", term::format::tertiary(profile.id())); 163 return Ok(()); 164 } 165 166 // If our key is encrypted, we try to authenticate with SSH Agent and 167 // register it; only if it is running. 168 match ssh::agent::Agent::connect() { 169 Ok(mut agent) => { 170 if agent.request_identities()?.contains(&profile.public_key) { 171 term::success!("Radicle key already in ssh-agent"); 172 return Ok(()); 173 } 174 175 let validator = term::io::PassphraseValidator::new(profile.keystore.clone()); 176 let passphrase = if options.stdin { 177 term::passphrase_stdin()? 178 } else { 179 term::passphrase(RAD_PASSPHRASE, validator)? 180 }; 181 register(&mut agent, profile, passphrase)?; 182 183 term::success!("Radicle key added to {}", term::format::dim("ssh-agent")); 184 185 return Ok(()); 186 } 187 Err(e) if e.is_not_running() => {} 188 Err(e) => Err(e)?, 189 }; 190 191 // Try RAD_PASSPHRASE fallback. 192 if let Some(passphrase) = profile::env::passphrase() { 193 ssh::keystore::MemorySigner::load(&profile.keystore, Some(passphrase)) 194 .map_err(|_| anyhow!("RAD_PASSPHRASE failed"))?; 195 return Ok(()); 196 }; 197 198 // ssh-agent is the de-facto solution. 199 anyhow::bail!("ssh-agent not running"); 200 } 201 202 /// Register key with ssh-agent. 203 pub fn register( 204 agent: &mut ssh::agent::Agent, 205 profile: &Profile, 206 passphrase: Passphrase, 207 ) -> anyhow::Result<()> { 208 let secret = profile 209 .keystore 210 .secret_key(Some(passphrase)) 211 .map_err(|e| { 212 if e.is_crypto_err() { 213 anyhow!("could not decrypt secret key: invalid passphrase") 214 } else { 215 e.into() 216 } 217 })? 218 .ok_or_else(|| anyhow!("Key not found in {:?}", profile.keystore.path()))?; 219 220 agent.register(&secret)?; 221 222 Ok(()) 223 }