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;
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
120/// Try loading the identity's key into SSH Agent, falling back to verifying `RAD_PASSPHRASE` for
121/// use.
122pub 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    // If our key is encrypted, we try to authenticate with SSH Agent and
134    // register it; only if it is running.
135    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    // Try RAD_PASSPHRASE fallback.
165    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
181/// Register key with ssh-agent.
182pub 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}