1use 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#[derive(Parser, Debug)]
16pub(crate) enum HscSubcommands {
17 #[command(subcommand)]
19 Hsc(HscSubcommand),
20}
21
22#[derive(Debug, Subcommand)]
23pub(crate) enum HscSubcommand {
24 #[command(arg_required_else_help = true)]
31 GetKey(GetKeyArgs),
32 #[command(subcommand)]
34 Key(KeySubcommand),
35}
36
37#[derive(Debug, Subcommand)]
38pub(crate) enum KeySubcommand {
39 #[command(arg_required_else_help = true)]
41 Get(GetKeyArgs),
42
43 #[command(arg_required_else_help = true)]
45 Rotate(RotateKeyArgs),
46
47 #[command(arg_required_else_help = true)]
49 Remove(RemoveKeyArgs),
50}
51
52#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
54enum KeyType {
55 #[default]
58 ServiceDiscovery,
59}
60
61#[derive(Debug, Clone, Args)]
64pub(crate) struct GetKeyArgs {
65 #[command(flatten)]
67 common: CommonArgs,
68
69 #[command(flatten)]
71 keygen: KeygenArgs,
72
73 #[arg(
75 long,
76 default_value_t = GenerateKey::IfNeeded,
77 value_enum
78 )]
79 generate: GenerateKey,
80 }
82
83#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
85enum GenerateKey {
86 No,
88 #[default]
90 IfNeeded,
91}
92
93#[derive(Debug, Clone, Args)]
95pub(crate) struct CommonArgs {
96 #[arg(
98 long,
99 default_value_t = KeyType::ServiceDiscovery,
100 value_enum
101 )]
102 key_type: KeyType,
103
104 #[arg(long, short, default_value_t = false)]
107 batch: bool,
108}
109
110#[derive(Debug, Clone, Args)]
112pub(crate) struct KeygenArgs {
113 #[arg(long, name = "FILE")]
115 output: String,
116
117 #[arg(long)]
119 overwrite: bool,
120}
121
122#[derive(Debug, Clone, Args)]
124pub(crate) struct RotateKeyArgs {
125 #[command(flatten)]
127 common: CommonArgs,
128
129 #[command(flatten)]
131 keygen: KeygenArgs,
132}
133
134#[derive(Debug, Clone, Args)]
136pub(crate) struct RemoveKeyArgs {
137 #[command(flatten)]
139 common: CommonArgs,
140}
141
142pub(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
169fn 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
178fn 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 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
204fn display_service_discovery_key(args: &KeygenArgs, key: &HsClientDescEncKey) -> Result<()> {
210 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
239fn write_public_key(mut f: impl io::Write, key: &HsClientDescEncKey) -> io::Result<()> {
241 writeln!(f, "{}", key)?;
242 Ok(())
243}
244
245fn 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
258fn 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
271fn prompt(msg: &str, args: &CommonArgs) -> Result<bool> {
278 if args.batch {
279 return Ok(true);
280 }
281
282 const YES: &str = "YES";
284 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
310fn 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}