1use 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#[cfg(feature = "onion-service-cli-extra")]
12use {
13 std::collections::{HashMap, hash_map::Entry},
14 tor_hsclient::HsClientDescEncKeypairSpecifier,
15 tor_hscrypto::pk::HsClientDescEncKeypair,
16 tor_keymgr::{CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, KeystoreId},
17};
18
19use std::fs::OpenOptions;
20use std::io::{self, Write};
21use std::str::FromStr;
22
23#[derive(Parser, Debug)]
25pub(crate) enum HscSubcommands {
26 #[command(subcommand)]
28 Hsc(HscSubcommand),
29}
30
31#[derive(Debug, Subcommand)]
33pub(crate) enum HscSubcommand {
34 #[command(arg_required_else_help = true)]
41 GetKey(GetKeyArgs),
42 #[command(subcommand)]
44 Key(KeySubcommand),
45
46 #[cfg(feature = "onion-service-cli-extra")]
49 #[command(name = "ctor-migrate")]
50 CTorMigrate(CTorMigrateArgs),
51}
52
53#[derive(Debug, Subcommand)]
55pub(crate) enum KeySubcommand {
56 #[command(arg_required_else_help = true)]
58 Get(GetKeyArgs),
59
60 #[command(arg_required_else_help = true)]
62 Rotate(RotateKeyArgs),
63
64 #[command(arg_required_else_help = true)]
66 Remove(RemoveKeyArgs),
67}
68
69#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
71enum KeyType {
72 #[default]
75 ServiceDiscovery,
76}
77
78#[derive(Debug, Clone, Args)]
81pub(crate) struct GetKeyArgs {
82 #[command(flatten)]
84 common: CommonArgs,
85
86 #[command(flatten)]
88 keygen: KeygenArgs,
89
90 #[arg(
92 long,
93 default_value_t = GenerateKey::IfNeeded,
94 value_enum
95 )]
96 generate: GenerateKey,
97 }
99
100#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
102enum GenerateKey {
103 No,
105 #[default]
107 IfNeeded,
108}
109
110#[derive(Debug, Clone, Args)]
112pub(crate) struct CommonArgs {
113 #[arg(
115 long,
116 default_value_t = KeyType::ServiceDiscovery,
117 value_enum
118 )]
119 key_type: KeyType,
120
121 #[arg(long, short, default_value_t = false)]
124 batch: bool,
125}
126
127#[derive(Debug, Clone, Args)]
129pub(crate) struct KeygenArgs {
130 #[arg(long, name = "FILE")]
132 output: String,
133
134 #[arg(long)]
136 overwrite: bool,
137}
138
139#[derive(Debug, Clone, Args)]
141pub(crate) struct RotateKeyArgs {
142 #[command(flatten)]
144 common: CommonArgs,
145
146 #[command(flatten)]
148 keygen: KeygenArgs,
149}
150
151#[derive(Debug, Clone, Args)]
153pub(crate) struct RemoveKeyArgs {
154 #[command(flatten)]
156 common: CommonArgs,
157}
158
159#[derive(Debug, Clone, Args)]
161#[cfg(feature = "onion-service-cli-extra")]
162pub(crate) struct CTorMigrateArgs {
163 #[arg(long, short, default_value_t = false)]
166 batch: bool,
167
168 #[arg(long, short)]
172 from: KeystoreId,
173}
174
175pub(crate) fn run<R: Runtime>(
177 runtime: R,
178 hsc_matches: &ArgMatches,
179 config: &TorClientConfig,
180) -> Result<()> {
181 use KeyType::*;
182
183 let subcommand =
184 HscSubcommand::from_arg_matches(hsc_matches).expect("Could not parse hsc subcommand");
185 let client = TorClient::with_runtime(runtime)
186 .config(config.clone())
187 .create_inert()?;
188
189 match subcommand {
190 HscSubcommand::GetKey(args) => {
191 eprintln!(
192 "warning: using deprecated command 'arti hsc key-get` (hint: use 'arti hsc key get' instead)"
193 );
194 match args.common.key_type {
195 ServiceDiscovery => prepare_service_discovery_key(&args, &client),
196 }
197 }
198 HscSubcommand::Key(subcommand) => run_key(subcommand, &client),
199 #[cfg(feature = "onion-service-cli-extra")]
200 HscSubcommand::CTorMigrate(args) => migrate_ctor_keys(&args, &client),
201 }
202}
203
204fn run_key(subcommand: KeySubcommand, client: &InertTorClient) -> Result<()> {
206 match subcommand {
207 KeySubcommand::Get(args) => prepare_service_discovery_key(&args, client),
208 KeySubcommand::Rotate(args) => rotate_service_discovery_key(&args, client),
209 KeySubcommand::Remove(args) => remove_service_discovery_key(&args, client),
210 }
211}
212
213fn prepare_service_discovery_key(args: &GetKeyArgs, client: &InertTorClient) -> Result<()> {
215 let addr = get_onion_address(&args.common)?;
216 let key = match args.generate {
217 GenerateKey::IfNeeded => {
218 client
220 .get_service_discovery_key(addr)?
221 .map(Ok)
222 .unwrap_or_else(|| {
223 client.generate_service_discovery_key(KeystoreSelector::Primary, addr)
224 })?
225 }
226 GenerateKey::No => match client.get_service_discovery_key(addr)? {
227 Some(key) => key,
228 None => {
229 return Err(anyhow!(
230 "Service discovery key not found. Rerun with --generate=if-needed to generate a new service discovery keypair"
231 ));
232 }
233 },
234 };
235
236 display_service_discovery_key(&args.keygen, &key)
237}
238
239fn display_service_discovery_key(args: &KeygenArgs, key: &HsClientDescEncKey) -> Result<()> {
245 match args.output.as_str() {
247 "-" => write_public_key(io::stdout(), key)?,
248 filename => {
249 let res = OpenOptions::new()
250 .create(true)
251 .create_new(!args.overwrite)
252 .write(true)
253 .truncate(true)
254 .open(filename)
255 .and_then(|f| write_public_key(f, key));
256
257 if let Err(e) = res {
258 match e.kind() {
259 io::ErrorKind::AlreadyExists => {
260 return Err(anyhow!(
261 "{filename} already exists. Move it, or rerun with --overwrite to overwrite it"
262 ));
263 }
264 _ => {
265 return Err(e)
266 .with_context(|| format!("could not write public key to {filename}"));
267 }
268 }
269 }
270 }
271 }
272
273 Ok(())
274}
275
276fn write_public_key(mut f: impl io::Write, key: &HsClientDescEncKey) -> io::Result<()> {
278 writeln!(f, "{}", key)?;
279 Ok(())
280}
281
282fn rotate_service_discovery_key(args: &RotateKeyArgs, client: &InertTorClient) -> Result<()> {
284 let addr = get_onion_address(&args.common)?;
285 let msg = format!(
286 "rotate client restricted discovery key for {}?",
287 addr.display_unredacted()
288 );
289 if !args.common.batch && !prompt(&msg)? {
290 return Ok(());
291 }
292
293 let key = client.rotate_service_discovery_key(KeystoreSelector::default(), addr)?;
294
295 display_service_discovery_key(&args.keygen, &key)
296}
297
298fn remove_service_discovery_key(args: &RemoveKeyArgs, client: &InertTorClient) -> Result<()> {
300 let addr = get_onion_address(&args.common)?;
301 let msg = format!(
302 "remove client restricted discovery key for {}?",
303 addr.display_unredacted()
304 );
305 if !args.common.batch && !prompt(&msg)? {
306 return Ok(());
307 }
308
309 let _key = client.remove_service_discovery_key(KeystoreSelector::default(), addr)?;
310
311 Ok(())
312}
313
314#[cfg(feature = "onion-service-cli-extra")]
316fn migrate_ctor_keys(args: &CTorMigrateArgs, client: &InertTorClient) -> Result<()> {
317 let keymgr = client.keymgr()?;
318 let ctor_client_entries = read_ctor_keys(&keymgr.list_by_id(&args.from)?, args)?;
319
320 let arti_keystore_id = KeystoreId::from_str("arti")
323 .map_err(|_| anyhow!("Default arti keystore ID is not valid?!"))?;
324 for (hsid, entry) in ctor_client_entries {
325 if let Ok(Some(key)) = keymgr.get_entry::<HsClientDescEncKeypair>(&entry) {
326 let key_exists = keymgr
327 .get_from::<HsClientDescEncKeypair>(
328 &HsClientDescEncKeypairSpecifier::new(hsid),
329 &arti_keystore_id,
330 )?
331 .is_some();
332 let proceed = if args.batch || !key_exists {
333 true
334 } else {
335 let p = format!(
336 "Found key in the primary keystore for service {}, do you want to replace it? ",
337 hsid.display_redacted()
338 );
339 prompt(&p)?
340 };
341 if proceed {
342 let res = keymgr.insert(
343 key,
344 &HsClientDescEncKeypairSpecifier::new(hsid),
345 (&arti_keystore_id).into(),
346 true,
347 );
348 if let Err(e) = res {
349 eprintln!(
350 "Failed to insert key for service {}: {e}",
351 hsid.display_redacted()
352 );
353 }
354 }
355 }
356 }
357
358 Ok(())
359}
360
361fn get_onion_address(args: &CommonArgs) -> Result<HsId, anyhow::Error> {
363 let mut addr = String::new();
364 if !args.batch {
365 print!("Enter an onion address: ");
366 io::stdout().flush().map_err(|e| anyhow!(e))?;
367 };
368 io::stdin().read_line(&mut addr).map_err(|e| anyhow!(e))?;
369
370 HsId::from_str(addr.trim_end()).map_err(|e| anyhow!(e))
371}
372
373#[cfg(feature = "onion-service-cli-extra")]
383fn read_ctor_keys<'a>(
384 entries: &[KeystoreEntryResult<KeystoreEntry<'a>>],
385 args: &CTorMigrateArgs,
386) -> Result<HashMap<HsId, KeystoreEntry<'a>>> {
387 let mut ctor_client_entries = HashMap::new();
388 for entry in entries.iter().flatten() {
389 if let KeyPath::CTor(CTorPath::HsClientDescEncKeypair { hs_id }) = entry.key_path() {
390 match ctor_client_entries.entry(*hs_id) {
391 Entry::Occupied(_) => {
392 return Err(anyhow!(
393 "Invalid C Tor keystore (multiple keys exist for service {})",
394 hs_id.display_redacted()
395 ));
396 }
397 Entry::Vacant(v) => {
398 v.insert(entry.clone());
399 }
400 }
401 };
402 }
403
404 if ctor_client_entries.is_empty() {
405 return Err(anyhow!(
406 "No CTor client keys found in keystore {}",
407 args.from,
408 ));
409 }
410
411 Ok(ctor_client_entries)
412}