Skip to main content

radicle_cli/commands/
auth.rs

1mod 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
110/// Try loading the identity's key into SSH Agent, falling back to verifying `RAD_PASSPHRASE` for
111/// use.
112pub 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    // If our key is encrypted, we try to authenticate with SSH Agent and
124    // register it; only if it is running.
125    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    // Try RAD_PASSPHRASE fallback.
155    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
171/// Register key with ssh-agent.
172pub 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}