arti/subcommands/
hsc.rs

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