1
//! The `hsc` subcommand.
2

            
3
use crate::{Result, TorClient};
4

            
5
use anyhow::{anyhow, Context};
6
use arti_client::{HsClientDescEncKey, HsId, InertTorClient, KeystoreSelector, TorClientConfig};
7
use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
8
use tor_rtcompat::Runtime;
9

            
10
use std::fs::OpenOptions;
11
use std::io;
12

            
13
/// The hsc subcommands the arti CLI will be augmented with.
14
#[derive(Parser, Debug)]
15
pub(crate) enum HscSubcommands {
16
    /// Run state management commands for an Arti hidden service client.
17
    #[command(subcommand)]
18
    Hsc(HscSubcommand),
19
}
20

            
21
#[derive(Debug, Subcommand)]
22
pub(crate) enum HscSubcommand {
23
    /// Prepare a service discovery key for connecting
24
    /// to a service running in restricted discovery mode.
25
    /// (Deprecated: use `arti hsc key get` instead)
26
    ///
27
    // TODO: use a clap deprecation attribute when clap supports it:
28
    // <https://github.com/clap-rs/clap/issues/3321>
29
    #[command(arg_required_else_help = true)]
30
    GetKey(GetKeyArgs),
31
    /// Key management subcommands.
32
    #[command(subcommand)]
33
    Key(KeySubcommand),
34
}
35

            
36
#[derive(Debug, Subcommand)]
37
pub(crate) enum KeySubcommand {
38
    /// Get or generate a hidden service client key
39
    /// Deprecated. Use key get instead.
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)]
54
enum 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)]
64
pub(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
48
        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)]
85
enum 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)]
95
pub(crate) struct CommonArgs {
96
    /// The type of key to rotate.
97
    #[arg(
98
        long,
99
48
        default_value_t = KeyType::ServiceDiscovery,
100
        value_enum
101
    )]
102
    key_type: KeyType,
103

            
104
    /// The .onion address of the hidden service
105
    #[arg(long)]
106
    onion_name: HsId,
107
}
108

            
109
/// The common arguments of the key subcommands.
110
#[derive(Debug, Clone, Args)]
111
pub(crate) struct KeygenArgs {
112
    /// Write the public key to FILE. Use - to write to stdout
113
    #[arg(long, name = "FILE")]
114
    output: String,
115

            
116
    /// Whether to overwrite the output file if it already exists
117
    #[arg(long)]
118
    overwrite: bool,
119
}
120

            
121
/// The arguments of the [`Rotate`](KeySubcommand::Rotate) subcommand.
122
#[derive(Debug, Clone, Args)]
123
pub(crate) struct RotateKeyArgs {
124
    /// Arguments shared by all hsc subcommands.
125
    #[command(flatten)]
126
    common: CommonArgs,
127

            
128
    /// Arguments for configuring keygen.
129
    #[command(flatten)]
130
    keygen: KeygenArgs,
131

            
132
    /// Do not prompt before overwriting the key.
133
    #[arg(long, short)]
134
    force: bool,
135
}
136

            
137
/// The arguments of the [`Remove`](KeySubcommand::Remove) subcommand.
138
#[derive(Debug, Clone, Args)]
139
pub(crate) struct RemoveKeyArgs {
140
    /// Arguments shared by all hsc subcommands.
141
    #[command(flatten)]
142
    common: CommonArgs,
143

            
144
    /// Do not prompt before removing the key.
145
    #[arg(long, short)]
146
    force: bool,
147
}
148

            
149
/// Run the `hsc` subcommand.
150
12
pub(crate) fn run<R: Runtime>(
151
12
    runtime: R,
152
12
    hsc_matches: &ArgMatches,
153
12
    config: &TorClientConfig,
154
12
) -> Result<()> {
155
    use KeyType::*;
156

            
157
12
    let subcommand =
158
12
        HscSubcommand::from_arg_matches(hsc_matches).expect("Could not parse hsc subcommand");
159
12
    let client = TorClient::with_runtime(runtime)
160
12
        .config(config.clone())
161
12
        .create_inert()?;
162

            
163
12
    match subcommand {
164
        HscSubcommand::GetKey(args) => {
165
            eprintln!(
166
                "warning: using deprecated command 'arti hsc key-get` (hint: use 'arti hsc key get' instead)"
167
            );
168
            match args.common.key_type {
169
                ServiceDiscovery => prepare_service_discovery_key(&args, &client),
170
            }
171
        }
172
12
        HscSubcommand::Key(subcommand) => run_key(subcommand, &client),
173
    }
174
12
}
175

            
176
/// Run the `hsc key` subcommand
177
12
fn run_key(subcommand: KeySubcommand, client: &InertTorClient) -> Result<()> {
178
12
    match subcommand {
179
12
        KeySubcommand::Get(args) => prepare_service_discovery_key(&args, client),
180
        KeySubcommand::Rotate(args) => rotate_service_discovery_key(&args, client),
181
        KeySubcommand::Remove(args) => remove_service_discovery_key(&args, client),
182
    }
183
12
}
184

            
185
/// Run the `hsc prepare-stealth-mode-key` subcommand.
186
12
fn prepare_service_discovery_key(args: &GetKeyArgs, client: &InertTorClient) -> Result<()> {
187
12
    let key = match args.generate {
188
        GenerateKey::IfNeeded => {
189
            // TODO: consider using get_or_generate in generate_service_discovery_key
190
6
            client
191
6
                .get_service_discovery_key(args.common.onion_name)?
192
6
                .map(Ok)
193
6
                .unwrap_or_else(|| {
194
                    client.generate_service_discovery_key(
195
                        KeystoreSelector::Primary,
196
                        args.common.onion_name,
197
                    )
198
6
                })?
199
        }
200
6
        GenerateKey::No => match client.get_service_discovery_key(args.common.onion_name)? {
201
            Some(key) => key,
202
            None => {
203
6
                return Err(anyhow!(
204
6
                        "Service discovery key not found. Rerun with --generate=if-needed to generate a new service discovery keypair"
205
6
                    ));
206
            }
207
        },
208
    };
209

            
210
6
    display_service_discovery_key(&args.keygen, &key)
211
12
}
212

            
213
/// Display the public part of a service discovery key.
214
//
215
// TODO: have a more principled implementation for displaying messages, etc.
216
// For example, it would be nice to centralize the logic for writing to stdout/file,
217
// and to add a flag for choosing the output format (human-readable or json)
218
6
fn display_service_discovery_key(args: &KeygenArgs, key: &HsClientDescEncKey) -> Result<()> {
219
6
    // Output the public key to the specified file, or to stdout.
220
6
    match args.output.as_str() {
221
6
        "-" => write_public_key(io::stdout(), key)?,
222
        filename => {
223
            let res = OpenOptions::new()
224
                .create(true)
225
                .create_new(!args.overwrite)
226
                .write(true)
227
                .truncate(true)
228
                .open(filename)
229
                .and_then(|f| write_public_key(f, key));
230

            
231
            if let Err(e) = res {
232
                match e.kind() {
233
                    io::ErrorKind::AlreadyExists => {
234
                        return Err(anyhow!("{filename} already exists. Move it, or rerun with --overwrite to overwrite it"));
235
                    }
236
                    _ => {
237
                        // TODO maybe handle some other ErrorKinds
238
                        return Err(e)
239
                            .with_context(|| format!("could not write public key to {filename}"));
240
                    }
241
                }
242
            }
243
        }
244
    }
245

            
246
6
    Ok(())
247
6
}
248

            
249
/// Write the public part of `key` to `f`.
250
6
fn write_public_key(mut f: impl io::Write, key: &HsClientDescEncKey) -> io::Result<()> {
251
6
    write!(f, "{}", key)?;
252
6
    Ok(())
253
6
}
254

            
255
/// Run the `hsc rotate-key` subcommand.
256
fn rotate_service_discovery_key(args: &RotateKeyArgs, client: &InertTorClient) -> Result<()> {
257
    if !args.force {
258
        let msg = format!(
259
            "rotate client restricted discovery key for {}?",
260
            args.common.onion_name
261
        );
262
        if !prompt(&msg)? {
263
            return Ok(());
264
        }
265
    }
266

            
267
    let key =
268
        client.rotate_service_discovery_key(KeystoreSelector::default(), args.common.onion_name)?;
269

            
270
    display_service_discovery_key(&args.keygen, &key)
271
}
272

            
273
/// Run the `hsc remove-key` subcommand.
274
fn remove_service_discovery_key(args: &RemoveKeyArgs, client: &InertTorClient) -> Result<()> {
275
    if !args.force {
276
        let msg = format!(
277
            "remove client restricted discovery key for {}?",
278
            args.common.onion_name
279
        );
280
        if !prompt(&msg)? {
281
            return Ok(());
282
        }
283
    }
284

            
285
    let _key =
286
        client.remove_service_discovery_key(KeystoreSelector::default(), args.common.onion_name)?;
287

            
288
    Ok(())
289
}
290

            
291
/// Prompt the user to confirm by typing yes or no.
292
///
293
/// Loops until the user confirms or declines,
294
/// returning true if they confirmed.
295
fn prompt(msg: &str) -> Result<bool> {
296
    /// The accept message.
297
    const YES: &str = "YES";
298
    /// The decline message.
299
    const NO: &str = "no";
300

            
301
    let msg = format!("{msg} (type {YES} or {NO})");
302
    loop {
303
        let proceed = dialoguer::Input::<String>::new()
304
            .with_prompt(&msg)
305
            .interact_text()?;
306

            
307
        let proceed: &str = proceed.as_ref();
308
        if proceed == YES {
309
            return Ok(true);
310
        }
311

            
312
        match proceed.to_lowercase().as_str() {
313
            NO | "n" => return Ok(false),
314
            _ => {
315
                continue;
316
            }
317
        }
318
    }
319
}