1#[cfg(feature = "onion-service-cli-extra")]
4use {
5 crate::subcommands::prompt,
6 std::str::FromStr,
7 tor_hscrypto::pk::HsIdKeypair,
8 tor_hsservice::HsIdKeypairSpecifier,
9 tor_keymgr::{KeyMgr, KeystoreEntry, KeystoreId},
10};
11
12use anyhow::anyhow;
13use arti_client::{InertTorClient, TorClientConfig};
14use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
15use safelog::DisplayRedacted;
16use tor_hsservice::{HsId, HsNickname, OnionService};
17use tor_rtcompat::Runtime;
18
19use crate::{ArtiConfig, Result, TorClient};
20
21#[derive(Parser, Debug)]
23pub(crate) enum HssSubcommands {
24 Hss(Hss),
26}
27
28#[derive(Debug, Parser)]
30pub(crate) struct Hss {
31 #[command(flatten)]
33 common: CommonArgs,
34
35 #[command(subcommand)]
37 command: HssSubcommand,
38}
39
40#[derive(Subcommand, Debug, Clone)]
42pub(crate) enum HssSubcommand {
43 OnionAddress(OnionAddressArgs),
45
46 #[command(hide = true)] OnionName(OnionAddressArgs),
49
50 #[cfg(feature = "onion-service-cli-extra")]
65 #[command(name = "ctor-migrate")]
66 CTorMigrate(CTorMigrateArgs),
67}
68
69#[derive(Debug, Clone, Args)]
71pub(crate) struct OnionAddressArgs {
72 #[arg(
74 long,
75 default_value_t = GenerateKey::No,
76 value_enum
77 )]
78 generate: GenerateKey,
79}
80
81#[derive(Debug, Clone, Args)]
83#[cfg(feature = "onion-service-cli-extra")]
84pub(crate) struct CTorMigrateArgs {
85 #[arg(long, short, default_value_t = false)]
88 batch: bool,
89}
90
91#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
93enum GenerateKey {
94 #[default]
96 No,
97 IfNeeded,
99}
100
101#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
103enum KeyType {
104 OnionAddress,
106}
107
108#[derive(Debug, Clone, Args)]
110pub(crate) struct CommonArgs {
111 #[arg(short, long)]
113 nickname: HsNickname,
114}
115
116pub(crate) fn run<R: Runtime>(
118 runtime: R,
119 hss_matches: &ArgMatches,
120 config: &ArtiConfig,
121 client_config: &TorClientConfig,
122) -> Result<()> {
123 let hss = Hss::from_arg_matches(hss_matches).expect("Could not parse hss subcommand");
124
125 match hss.command {
126 HssSubcommand::OnionAddress(args) => {
127 run_onion_address(&hss.common, &args, config, client_config)
128 }
129 #[cfg(feature = "onion-service-cli-extra")]
130 HssSubcommand::CTorMigrate(args) => run_migrate(runtime, client_config, &args, &hss.common),
131 HssSubcommand::OnionName(args) => {
132 eprintln!(
133 "warning: using deprecated command 'onion-name', (hint: use 'onion-address' instead)"
134 );
135 run_onion_address(&hss.common, &args, config, client_config)
136 }
137 }
138}
139
140fn create_svc(
142 nickname: &HsNickname,
143 config: &ArtiConfig,
144 client_config: &TorClientConfig,
145) -> Result<OnionService> {
146 let Some(svc_config) = config
147 .onion_services
148 .iter()
149 .find(|(n, _)| *n == nickname)
150 .map(|(_, cfg)| cfg.svc_cfg.clone())
151 else {
152 return Err(anyhow!("Service {nickname} is not configured"));
153 };
154
155 Ok(
162 TorClient::<tor_rtcompat::PreferredRuntime>::create_onion_service(
163 client_config,
164 svc_config,
165 )?,
166 )
167}
168
169fn display_onion_address(nickname: &HsNickname, hsid: Option<HsId>) -> Result<()> {
171 if let Some(onion) = hsid {
174 println!("{}", onion.display_unredacted());
175 } else {
176 return Err(anyhow!(
177 "Service {nickname} does not exist, or does not have an K_hsid yet"
178 ));
179 }
180
181 Ok(())
182}
183
184fn onion_address(
186 args: &CommonArgs,
187 config: &ArtiConfig,
188 client_config: &TorClientConfig,
189) -> Result<()> {
190 let onion_svc = create_svc(&args.nickname, config, client_config)?;
191 let hsid = onion_svc.onion_address();
192 display_onion_address(&args.nickname, hsid)?;
193
194 Ok(())
195}
196
197fn get_or_generate_onion_address(
199 args: &CommonArgs,
200 config: &ArtiConfig,
201 client_config: &TorClientConfig,
202) -> Result<()> {
203 let svc = create_svc(&args.nickname, config, client_config)?;
204 let hsid = svc.onion_address();
205 match hsid {
206 Some(hsid) => display_onion_address(&args.nickname, Some(hsid)),
207 None => {
208 let selector = Default::default();
209 let hsid = svc.generate_identity_key(selector)?;
210 display_onion_address(&args.nickname, Some(hsid))
211 }
212 }
213}
214
215fn run_onion_address(
217 args: &CommonArgs,
218 get_key_args: &OnionAddressArgs,
219 config: &ArtiConfig,
220 client_config: &TorClientConfig,
221) -> Result<()> {
222 match get_key_args.generate {
223 GenerateKey::No => onion_address(args, config, client_config),
224 GenerateKey::IfNeeded => get_or_generate_onion_address(args, config, client_config),
225 }
226}
227
228#[cfg(feature = "onion-service-cli-extra")]
230fn run_migrate<R: Runtime>(
231 runtime: R,
232 client_config: &TorClientConfig,
233 migrate_args: &CTorMigrateArgs,
234 args: &CommonArgs,
235) -> Result<()> {
236 let ctor_keystore_id = find_ctor_keystore(client_config, args)?;
237
238 let inert_client = TorClient::with_runtime(runtime)
239 .config(client_config.clone())
240 .create_inert()?;
241
242 migrate_ctor_keys(migrate_args, args, &inert_client, &ctor_keystore_id)
243}
244
245#[cfg(feature = "onion-service-cli-extra")]
257fn migrate_ctor_keys(
258 migrate_args: &CTorMigrateArgs,
259 args: &CommonArgs,
260 client: &InertTorClient,
261 ctor_keystore_id: &KeystoreId,
262) -> Result<()> {
263 let keymgr = client.keymgr()?;
264 let nickname = &args.nickname;
265 let id_key_spec = HsIdKeypairSpecifier::new(nickname.clone());
266 let ctor_id_key = keymgr
268 .get_from::<HsIdKeypair>(&id_key_spec, ctor_keystore_id)?
269 .ok_or_else(|| anyhow!("No identity key found in the provided C Tor keystore."))?;
270
271 let arti_pat = tor_keymgr::KeyPathPattern::Arti(format!("hss/{}/**/*", nickname));
272 let arti_entries = keymgr.list_matching(&arti_pat)?;
273
274 let arti_keystore_id = KeystoreId::from_str("arti")
277 .map_err(|_| anyhow!("Default arti keystore ID is not valid?!"))?;
278
279 let is_empty = arti_entries.is_empty();
280
281 if !is_empty {
282 let arti_id_entry_opt = arti_entries.iter().find(|k| {
283 keymgr
288 .describe(k.key_path())
289 .is_some_and(|info| info.role() == "ks_hs_id")
290 });
291 if let Some(arti_id_entry) = arti_id_entry_opt {
292 let arti_id_key: HsIdKeypair = match keymgr.get_entry(arti_id_entry)? {
293 Some(aik) => aik,
294 None => {
295 return Err(anyhow!(
296 "Identity key disappeared during migration (is another process using the keystore?)"
297 ));
298 }
299 };
300 if arti_id_key.as_ref().public() == ctor_id_key.as_ref().public() {
301 return Err(anyhow!("Service {nickname} was already migrated."));
302 }
303 }
304 }
305
306 if is_empty || migrate_args.batch || prompt(&build_prompt(&arti_entries))? {
307 remove_arti_entries(keymgr, &arti_entries);
308 keymgr.insert(ctor_id_key, &id_key_spec, (&arti_keystore_id).into(), true)?;
309 } else {
310 println!("Aborted.");
311 }
312
313 Ok(())
314}
315
316#[cfg(feature = "onion-service-cli-extra")]
322fn find_ctor_keystore(client_config: &TorClientConfig, args: &CommonArgs) -> Result<KeystoreId> {
323 let keystore_config = client_config.keystore();
324 let ctor_services = keystore_config.ctor().services();
325 if ctor_services.is_empty() {
326 return Err(anyhow!("No CTor keystore are configured."));
327 }
328
329 let Some((_, service_config)) = ctor_services
330 .iter()
331 .find(|(hs_nick, _)| *hs_nick == &args.nickname)
332 else {
333 return Err(anyhow!(
334 "The service identified using `--nickname {}` is not configured with any recognized CTor keystore.",
335 &args.nickname,
336 ));
337 };
338
339 Ok(service_config.id().clone())
340}
341
342#[cfg(feature = "onion-service-cli-extra")]
346fn remove_arti_entries(keymgr: &KeyMgr, arti_entries: &Vec<KeystoreEntry<'_>>) {
347 for entry in arti_entries {
348 if let Err(e) = keymgr.remove_entry(entry) {
349 eprintln!("Failed to remove entry {} ({e})", entry.key_path(),);
350 }
351 }
352}
353
354#[cfg(feature = "onion-service-cli-extra")]
357fn build_prompt(entries: &Vec<KeystoreEntry<'_>>) -> String {
358 let mut p = "WARNING: the following keys will be deleted\n".to_string();
359 for k in entries.iter() {
360 p.push('\t');
361 p.push_str(&k.key_path().to_string());
362 p.push('\n');
363 }
364 p.push('\n');
365 p.push_str("Proceed anyway?");
366 p
367}