/ radicle-cli / src / commands / auth.rs
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  }