radicle_cli/commands/
auth.rs1#![allow(clippy::or_fun_call)]
2mod args;
3
4use std::str::FromStr;
5
6use anyhow::{anyhow, Context};
7
8use radicle::crypto::ssh;
9use radicle::crypto::ssh::Passphrase;
10use radicle::node::Alias;
11use radicle::profile::env;
12use radicle::{profile, Profile};
13
14use crate::terminal as term;
15
16pub use args::Args;
17
18pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
19 match ctx.profile() {
20 Ok(profile) => authenticate(args, &profile),
21 Err(_) => init(args),
22 }
23}
24
25pub fn init(args: Args) -> anyhow::Result<()> {
26 term::headline("Initializing your radicle 👾 identity");
27
28 if let Ok(version) = radicle::git::version() {
29 if version < radicle::git::VERSION_REQUIRED {
30 term::warning(format!(
31 "Your Git version is unsupported, please upgrade to {} or later",
32 radicle::git::VERSION_REQUIRED,
33 ));
34 term::blank();
35 }
36 } else {
37 anyhow::bail!("A Git installation is required for Radicle to run.");
38 }
39
40 let alias: Alias = if let Some(alias) = args.alias {
41 alias
42 } else {
43 let user = env::var("USER").ok().and_then(|u| Alias::from_str(&u).ok());
44 let user = term::input(
45 "Enter your alias:",
46 user,
47 Some("This is your node alias. You can always change it later"),
48 )?;
49
50 user.ok_or_else(|| anyhow::anyhow!("An alias is required for Radicle to run."))?
51 };
52 let home = profile::home()?;
53 let passphrase = if args.stdin {
54 Some(term::passphrase_stdin()?)
55 } else {
56 term::passphrase_confirm("Enter a passphrase:", env::RAD_PASSPHRASE)?
57 };
58 let passphrase = passphrase.filter(|passphrase| !passphrase.trim().is_empty());
59 let spinner = term::spinner("Creating your Ed25519 keypair...");
60 let profile = Profile::init(home, alias, passphrase.clone(), env::seed())?;
61 let mut agent = true;
62 spinner.finish();
63
64 if let Some(passphrase) = passphrase {
65 match ssh::agent::Agent::connect() {
66 Ok(mut agent) => {
67 let mut spinner = term::spinner("Adding your radicle key to ssh-agent...");
68 if register(&mut agent, &profile, passphrase).is_ok() {
69 spinner.finish();
70 } else {
71 spinner.message("Could not register radicle key in ssh-agent.");
72 spinner.warn();
73 }
74 }
75 Err(e) if e.is_not_running() => {
76 agent = false;
77 }
78 Err(e) => Err(e).context("failed to connect to ssh-agent")?,
79 }
80 }
81
82 term::success!(
83 "Your Radicle DID is {}. This identifies your device. Run {} to show it at all times.",
84 term::format::highlight(profile.did()),
85 term::format::command("rad self")
86 );
87 term::success!("You're all set.");
88 term::blank();
89
90 if profile.config.cli.hints && !agent {
91 term::hint("install ssh-agent to have it fill in your passphrase for you when signing.");
92 term::blank();
93 }
94 term::info!(
95 "To create a Radicle repository, run {} from a Git repository with at least one commit.",
96 term::format::command("rad init")
97 );
98 term::info!(
99 "To clone a repository, run {}. For example, {} clones the Radicle 'heartwood' repository.",
100 term::format::command("rad clone <rid>"),
101 term::format::command("rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5")
102 );
103 term::info!(
104 "To get a list of all commands, run {}.",
105 term::format::command("rad"),
106 );
107
108 Ok(())
109}
110
111pub fn authenticate(args: Args, profile: &Profile) -> anyhow::Result<()> {
114 if !profile.keystore.is_encrypted()? {
115 term::success!("Authenticated as {}", term::format::tertiary(profile.id()));
116 return Ok(());
117 }
118 for (key, _) in &profile.config.node.extra {
119 term::warning(format!(
120 "unused or deprecated configuration attribute {key:?}"
121 ));
122 }
123
124 match ssh::agent::Agent::connect() {
127 Ok(mut agent) => {
128 if agent.request_identities()?.contains(&profile.public_key) {
129 term::success!("Radicle key already in ssh-agent");
130 return Ok(());
131 }
132 let passphrase = if let Some(phrase) = profile::env::passphrase() {
133 phrase
134 } else if args.stdin {
135 term::passphrase_stdin()?
136 } else if let Some(passphrase) =
137 term::io::passphrase(term::io::PassphraseValidator::new(profile.keystore.clone()))?
138 {
139 passphrase
140 } else {
141 anyhow::bail!(
142 "A passphrase is required to read your Radicle key. Unable to continue."
143 )
144 };
145 register(&mut agent, profile, passphrase)?;
146
147 term::success!("Radicle key added to {}", term::format::dim("ssh-agent"));
148
149 return Ok(());
150 }
151 Err(e) if e.is_not_running() => {}
152 Err(e) => Err(e)?,
153 };
154
155 if let Some(passphrase) = profile::env::passphrase() {
157 ssh::keystore::MemorySigner::load(&profile.keystore, Some(passphrase))
158 .map_err(|_| anyhow!("`{}` is invalid", env::RAD_PASSPHRASE))?;
159 return Ok(());
160 }
161
162 term::print(term::format::dim(
163 "Nothing to do, ssh-agent is not running.",
164 ));
165 term::print(term::format::dim(
166 "You will be prompted for a passphrase when necessary.",
167 ));
168
169 Ok(())
170}
171
172pub fn register(
174 agent: &mut ssh::agent::Agent,
175 profile: &Profile,
176 passphrase: Passphrase,
177) -> anyhow::Result<()> {
178 let secret = profile
179 .keystore
180 .secret_key(Some(passphrase))
181 .map_err(|e| {
182 if e.is_crypto_err() {
183 anyhow!("could not decrypt secret key: invalid passphrase")
184 } else {
185 e.into()
186 }
187 })?
188 .ok_or_else(|| anyhow!("Key not found in {:?}", profile.keystore.secret_key_path()))?;
189
190 agent.register(&secret)?;
191
192 Ok(())
193}