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