#[cfg(feature = "onion-service-cli-extra")]
use {
crate::subcommands::prompt,
std::str::FromStr,
tor_hscrypto::pk::HsIdKeypair,
tor_hsservice::HsIdKeypairSpecifier,
tor_keymgr::{KeyMgr, KeystoreEntry, KeystoreId},
};
use anyhow::anyhow;
use arti_client::{InertTorClient, TorClientConfig};
use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
use safelog::DisplayRedacted;
use tor_hsservice::{HsId, HsNickname, OnionService};
use tor_rtcompat::Runtime;
use crate::{ArtiConfig, Result, TorClient};
#[derive(Parser, Debug)]
pub(crate) enum HssSubcommands {
Hss(Hss),
}
#[derive(Debug, Parser)]
pub(crate) struct Hss {
#[command(flatten)]
common: CommonArgs,
#[command(subcommand)]
command: HssSubcommand,
}
#[derive(Subcommand, Debug, Clone)]
pub(crate) enum HssSubcommand {
OnionAddress(OnionAddressArgs),
#[command(hide = true)] OnionName(OnionAddressArgs),
#[cfg(feature = "onion-service-cli-extra")]
#[command(name = "ctor-migrate")]
CTorMigrate(CTorMigrateArgs),
}
#[derive(Debug, Clone, Args)]
pub(crate) struct OnionAddressArgs {
#[arg(
long,
default_value_t = GenerateKey::No,
value_enum
)]
generate: GenerateKey,
}
#[derive(Debug, Clone, Args)]
#[cfg(feature = "onion-service-cli-extra")]
pub(crate) struct CTorMigrateArgs {
#[arg(long, short, default_value_t = false)]
batch: bool,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
enum GenerateKey {
#[default]
No,
IfNeeded,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
enum KeyType {
OnionAddress,
}
#[derive(Debug, Clone, Args)]
pub(crate) struct CommonArgs {
#[arg(short, long)]
nickname: HsNickname,
}
pub(crate) fn run<R: Runtime>(
runtime: R,
hss_matches: &ArgMatches,
config: &ArtiConfig,
client_config: &TorClientConfig,
) -> Result<()> {
let hss = Hss::from_arg_matches(hss_matches).expect("Could not parse hss subcommand");
match hss.command {
HssSubcommand::OnionAddress(args) => {
run_onion_address(&hss.common, &args, config, client_config)
}
#[cfg(feature = "onion-service-cli-extra")]
HssSubcommand::CTorMigrate(args) => run_migrate(runtime, client_config, &args, &hss.common),
HssSubcommand::OnionName(args) => {
eprintln!(
"warning: using deprecated command 'onion-name', (hint: use 'onion-address' instead)"
);
run_onion_address(&hss.common, &args, config, client_config)
}
}
}
fn create_svc(
nickname: &HsNickname,
config: &ArtiConfig,
client_config: &TorClientConfig,
) -> Result<OnionService> {
let Some(svc_config) = config
.onion_services
.iter()
.find(|(n, _)| *n == nickname)
.map(|(_, cfg)| cfg.svc_cfg.clone())
else {
return Err(anyhow!("Service {nickname} is not configured"));
};
Ok(
TorClient::<tor_rtcompat::PreferredRuntime>::create_onion_service(
client_config,
svc_config,
)?,
)
}
fn display_onion_address(nickname: &HsNickname, hsid: Option<HsId>) -> Result<()> {
if let Some(onion) = hsid {
println!("{}", onion.display_unredacted());
} else {
return Err(anyhow!(
"Service {nickname} does not exist, or does not have an K_hsid yet"
));
}
Ok(())
}
fn onion_address(
args: &CommonArgs,
config: &ArtiConfig,
client_config: &TorClientConfig,
) -> Result<()> {
let onion_svc = create_svc(&args.nickname, config, client_config)?;
let hsid = onion_svc.onion_address();
display_onion_address(&args.nickname, hsid)?;
Ok(())
}
fn get_or_generate_onion_address(
args: &CommonArgs,
config: &ArtiConfig,
client_config: &TorClientConfig,
) -> Result<()> {
let svc = create_svc(&args.nickname, config, client_config)?;
let hsid = svc.onion_address();
match hsid {
Some(hsid) => display_onion_address(&args.nickname, Some(hsid)),
None => {
let selector = Default::default();
let hsid = svc.generate_identity_key(selector)?;
display_onion_address(&args.nickname, Some(hsid))
}
}
}
fn run_onion_address(
args: &CommonArgs,
get_key_args: &OnionAddressArgs,
config: &ArtiConfig,
client_config: &TorClientConfig,
) -> Result<()> {
match get_key_args.generate {
GenerateKey::No => onion_address(args, config, client_config),
GenerateKey::IfNeeded => get_or_generate_onion_address(args, config, client_config),
}
}
#[cfg(feature = "onion-service-cli-extra")]
fn run_migrate<R: Runtime>(
runtime: R,
client_config: &TorClientConfig,
migrate_args: &CTorMigrateArgs,
args: &CommonArgs,
) -> Result<()> {
let ctor_keystore_id = find_ctor_keystore(client_config, args)?;
let inert_client = TorClient::with_runtime(runtime)
.config(client_config.clone())
.create_inert()?;
migrate_ctor_keys(migrate_args, args, &inert_client, &ctor_keystore_id)
}
#[cfg(feature = "onion-service-cli-extra")]
fn migrate_ctor_keys(
migrate_args: &CTorMigrateArgs,
args: &CommonArgs,
client: &InertTorClient,
ctor_keystore_id: &KeystoreId,
) -> Result<()> {
let keymgr = client.keymgr()?;
let nickname = &args.nickname;
let id_key_spec = HsIdKeypairSpecifier::new(nickname.clone());
let ctor_id_key = keymgr
.get_from::<HsIdKeypair>(&id_key_spec, ctor_keystore_id)?
.ok_or_else(|| anyhow!("No identity key found in the provided C Tor keystore."))?;
let arti_pat = tor_keymgr::KeyPathPattern::Arti(format!("hss/{}/**/*", nickname));
let arti_entries = keymgr.list_matching(&arti_pat)?;
let arti_keystore_id = KeystoreId::from_str("arti")
.map_err(|_| anyhow!("Default arti keystore ID is not valid?!"))?;
let is_empty = arti_entries.is_empty();
if !is_empty {
let arti_id_entry_opt = arti_entries.iter().find(|k| {
keymgr
.describe(k.key_path())
.is_some_and(|info| info.role() == "ks_hs_id")
});
if let Some(arti_id_entry) = arti_id_entry_opt {
let arti_id_key: HsIdKeypair = match keymgr.get_entry(arti_id_entry)? {
Some(aik) => aik,
None => {
return Err(anyhow!(
"Identity key disappeared during migration (is another process using the keystore?)"
));
}
};
if arti_id_key.as_ref().public() == ctor_id_key.as_ref().public() {
return Err(anyhow!("Service {nickname} was already migrated."));
}
}
}
if is_empty || migrate_args.batch || prompt(&build_prompt(&arti_entries))? {
remove_arti_entries(keymgr, &arti_entries);
keymgr.insert(ctor_id_key, &id_key_spec, (&arti_keystore_id).into(), true)?;
} else {
println!("Aborted.");
}
Ok(())
}
#[cfg(feature = "onion-service-cli-extra")]
fn find_ctor_keystore(client_config: &TorClientConfig, args: &CommonArgs) -> Result<KeystoreId> {
let keystore_config = client_config.keystore();
let ctor_services = keystore_config.ctor().services();
if ctor_services.is_empty() {
return Err(anyhow!("No CTor keystore are configured."));
}
let Some((_, service_config)) = ctor_services
.iter()
.find(|(hs_nick, _)| *hs_nick == &args.nickname)
else {
return Err(anyhow!(
"The service identified using `--nickname {}` is not configured with any recognized CTor keystore.",
&args.nickname,
));
};
Ok(service_config.id().clone())
}
#[cfg(feature = "onion-service-cli-extra")]
fn remove_arti_entries(keymgr: &KeyMgr, arti_entries: &Vec<KeystoreEntry<'_>>) {
for entry in arti_entries {
if let Err(e) = keymgr.remove_entry(entry) {
eprintln!("Failed to remove entry {} ({e})", entry.key_path(),);
}
}
}
#[cfg(feature = "onion-service-cli-extra")]
fn build_prompt(entries: &Vec<KeystoreEntry<'_>>) -> String {
let mut p = "WARNING: the following keys will be deleted\n".to_string();
for k in entries.iter() {
p.push('\t');
p.push_str(&k.key_path().to_string());
p.push('\n');
}
p.push('\n');
p.push_str("Proceed anyway?");
p
}