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