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