arti/subcommands/
hsc.rs

1//! The `hsc` subcommand.
2
3use crate::{Result, TorClient};
4
5use anyhow::{anyhow, Context};
6use arti_client::{HsClientDescEncKey, HsId, InertTorClient, KeystoreSelector, TorClientConfig};
7use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
8use tor_rtcompat::Runtime;
9
10use std::fs::OpenOptions;
11use std::io::{self, Write};
12use std::str::FromStr;
13
14/// The hsc subcommands the arti CLI will be augmented with.
15#[derive(Parser, Debug)]
16pub(crate) enum HscSubcommands {
17    /// Run state management commands for an Arti hidden service client.
18    #[command(subcommand)]
19    Hsc(HscSubcommand),
20}
21
22#[derive(Debug, Subcommand)]
23pub(crate) enum HscSubcommand {
24    /// Prepare a service discovery key for connecting
25    /// to a service running in restricted discovery mode.
26    /// (Deprecated: use `arti hsc key get` instead)
27    ///
28    // TODO: use a clap deprecation attribute when clap supports it:
29    // <https://github.com/clap-rs/clap/issues/3321>
30    #[command(arg_required_else_help = true)]
31    GetKey(GetKeyArgs),
32    /// Key management subcommands.
33    #[command(subcommand)]
34    Key(KeySubcommand),
35}
36
37#[derive(Debug, Subcommand)]
38pub(crate) enum KeySubcommand {
39    /// Get or generate a hidden service client key
40    #[command(arg_required_else_help = true)]
41    Get(GetKeyArgs),
42
43    /// Rotate a hidden service client key
44    #[command(arg_required_else_help = true)]
45    Rotate(RotateKeyArgs),
46
47    /// Remove a hidden service client key
48    #[command(arg_required_else_help = true)]
49    Remove(RemoveKeyArgs),
50}
51
52/// A type of key
53#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
54enum KeyType {
55    /// A service discovery key for connecting to a service
56    /// running in restricted discovery mode.
57    #[default]
58    ServiceDiscovery,
59}
60
61/// The arguments of the [`GetKey`](HscSubcommand::GetKey)
62/// subcommand.
63#[derive(Debug, Clone, Args)]
64pub(crate) struct GetKeyArgs {
65    /// Arguments shared by all hsc subcommands.
66    #[command(flatten)]
67    common: CommonArgs,
68
69    /// Arguments for configuring keygen.
70    #[command(flatten)]
71    keygen: KeygenArgs,
72
73    /// Whether to generate the key if it is missing
74    #[arg(
75        long,
76        default_value_t = GenerateKey::IfNeeded,
77        value_enum
78    )]
79    generate: GenerateKey,
80    // TODO: add an option for selecting the keystore to generate the keypair in
81}
82
83/// Whether to generate the key if missing.
84#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
85enum GenerateKey {
86    /// Do not generate the key.
87    No,
88    /// Generate the key if it's missing.
89    #[default]
90    IfNeeded,
91}
92
93/// The common arguments of the key subcommands.
94#[derive(Debug, Clone, Args)]
95pub(crate) struct CommonArgs {
96    /// The type of the key.
97    #[arg(
98        long,
99        default_value_t = KeyType::ServiceDiscovery,
100        value_enum
101    )]
102    key_type: KeyType,
103
104    /// With this flag active no prompt will be shown
105    /// and no confirmation will be asked
106    #[arg(long, short, default_value_t = false)]
107    batch: bool,
108}
109
110/// The common arguments of the key subcommands.
111#[derive(Debug, Clone, Args)]
112pub(crate) struct KeygenArgs {
113    /// Write the public key to FILE. Use - to write to stdout
114    #[arg(long, name = "FILE")]
115    output: String,
116
117    /// Whether to overwrite the output file if it already exists
118    #[arg(long)]
119    overwrite: bool,
120}
121
122/// The arguments of the [`Rotate`](KeySubcommand::Rotate) subcommand.
123#[derive(Debug, Clone, Args)]
124pub(crate) struct RotateKeyArgs {
125    /// Arguments shared by all hsc subcommands.
126    #[command(flatten)]
127    common: CommonArgs,
128
129    /// Arguments for configuring keygen.
130    #[command(flatten)]
131    keygen: KeygenArgs,
132}
133
134/// The arguments of the [`Remove`](KeySubcommand::Remove) subcommand.
135#[derive(Debug, Clone, Args)]
136pub(crate) struct RemoveKeyArgs {
137    /// Arguments shared by all hsc subcommands.
138    #[command(flatten)]
139    common: CommonArgs,
140}
141
142/// Run the `hsc` subcommand.
143pub(crate) fn run<R: Runtime>(
144    runtime: R,
145    hsc_matches: &ArgMatches,
146    config: &TorClientConfig,
147) -> Result<()> {
148    use KeyType::*;
149
150    let subcommand =
151        HscSubcommand::from_arg_matches(hsc_matches).expect("Could not parse hsc subcommand");
152    let client = TorClient::with_runtime(runtime)
153        .config(config.clone())
154        .create_inert()?;
155
156    match subcommand {
157        HscSubcommand::GetKey(args) => {
158            eprintln!(
159                "warning: using deprecated command 'arti hsc key-get` (hint: use 'arti hsc key get' instead)"
160            );
161            match args.common.key_type {
162                ServiceDiscovery => prepare_service_discovery_key(&args, &client),
163            }
164        }
165        HscSubcommand::Key(subcommand) => run_key(subcommand, &client),
166    }
167}
168
169/// Run the `hsc key` subcommand
170fn run_key(subcommand: KeySubcommand, client: &InertTorClient) -> Result<()> {
171    match subcommand {
172        KeySubcommand::Get(args) => prepare_service_discovery_key(&args, client),
173        KeySubcommand::Rotate(args) => rotate_service_discovery_key(&args, client),
174        KeySubcommand::Remove(args) => remove_service_discovery_key(&args, client),
175    }
176}
177
178/// Run the `hsc prepare-stealth-mode-key` subcommand.
179fn prepare_service_discovery_key(args: &GetKeyArgs, client: &InertTorClient) -> Result<()> {
180    let addr = get_onion_address(&args.common)?;
181    let key = match args.generate {
182        GenerateKey::IfNeeded => {
183            // TODO: consider using get_or_generate in generate_service_discovery_key
184            client
185                .get_service_discovery_key(addr)?
186                .map(Ok)
187                .unwrap_or_else(|| {
188                    client.generate_service_discovery_key(KeystoreSelector::Primary, addr)
189                })?
190        }
191        GenerateKey::No => match client.get_service_discovery_key(addr)? {
192            Some(key) => key,
193            None => {
194                return Err(anyhow!(
195                        "Service discovery key not found. Rerun with --generate=if-needed to generate a new service discovery keypair"
196                    ));
197            }
198        },
199    };
200
201    display_service_discovery_key(&args.keygen, &key)
202}
203
204/// Display the public part of a service discovery key.
205//
206// TODO: have a more principled implementation for displaying messages, etc.
207// For example, it would be nice to centralize the logic for writing to stdout/file,
208// and to add a flag for choosing the output format (human-readable or json)
209fn display_service_discovery_key(args: &KeygenArgs, key: &HsClientDescEncKey) -> Result<()> {
210    // Output the public key to the specified file, or to stdout.
211    match args.output.as_str() {
212        "-" => write_public_key(io::stdout(), key)?,
213        filename => {
214            let res = OpenOptions::new()
215                .create(true)
216                .create_new(!args.overwrite)
217                .write(true)
218                .truncate(true)
219                .open(filename)
220                .and_then(|f| write_public_key(f, key));
221
222            if let Err(e) = res {
223                match e.kind() {
224                    io::ErrorKind::AlreadyExists => {
225                        return Err(anyhow!("{filename} already exists. Move it, or rerun with --overwrite to overwrite it"));
226                    }
227                    _ => {
228                        return Err(e)
229                            .with_context(|| format!("could not write public key to {filename}"));
230                    }
231                }
232            }
233        }
234    }
235
236    Ok(())
237}
238
239/// Write the public part of `key` to `f`.
240fn write_public_key(mut f: impl io::Write, key: &HsClientDescEncKey) -> io::Result<()> {
241    writeln!(f, "{}", key)?;
242    Ok(())
243}
244
245/// Run the `hsc rotate-key` subcommand.
246fn rotate_service_discovery_key(args: &RotateKeyArgs, client: &InertTorClient) -> Result<()> {
247    let addr = get_onion_address(&args.common)?;
248    let msg = format!("rotate client restricted discovery key for {}?", addr);
249    if !prompt(&msg, &args.common)? {
250        return Ok(());
251    }
252
253    let key = client.rotate_service_discovery_key(KeystoreSelector::default(), addr)?;
254
255    display_service_discovery_key(&args.keygen, &key)
256}
257
258/// Run the `hsc remove-key` subcommand.
259fn remove_service_discovery_key(args: &RemoveKeyArgs, client: &InertTorClient) -> Result<()> {
260    let addr = get_onion_address(&args.common)?;
261    let msg = format!("remove client restricted discovery key for {}?", addr);
262    if !prompt(&msg, &args.common)? {
263        return Ok(());
264    }
265
266    let _key = client.remove_service_discovery_key(KeystoreSelector::default(), addr)?;
267
268    Ok(())
269}
270
271/// Prompt the user to confirm by typing yes or no.
272///
273/// Loops until the user confirms or declines,
274/// returning true if they confirmed.
275///
276/// If `args.batch` is `true` no confirmation will be asked.
277fn prompt(msg: &str, args: &CommonArgs) -> Result<bool> {
278    if args.batch {
279        return Ok(true);
280    }
281
282    /// The accept message.
283    const YES: &str = "YES";
284    /// The decline message.
285    const NO: &str = "no";
286
287    let mut proceed = String::new();
288
289    print!("{} (type {YES} or {NO}): ", msg);
290    io::stdout().flush().map_err(|e| anyhow!(e))?;
291    loop {
292        io::stdin()
293            .read_line(&mut proceed)
294            .map_err(|e| anyhow!(e))?;
295
296        if proceed.trim_end() == YES {
297            return Ok(true);
298        }
299
300        match proceed.trim_end().to_lowercase().as_str() {
301            NO | "n" => return Ok(false),
302            _ => {
303                proceed.clear();
304                continue;
305            }
306        }
307    }
308}
309
310/// Prompt the user for an onion address.
311fn get_onion_address(args: &CommonArgs) -> Result<HsId, anyhow::Error> {
312    let mut addr = String::new();
313    if !args.batch {
314        print!("Enter an onion address: ");
315        io::stdout().flush().map_err(|e| anyhow!(e))?;
316    };
317    io::stdin().read_line(&mut addr).map_err(|e| anyhow!(e))?;
318
319    HsId::from_str(addr.trim_end()).map_err(|e| anyhow!(e))
320}