use crate::subcommands::prompt;
use crate::{Result, TorClient};
use anyhow::{Context, anyhow};
use arti_client::{HsClientDescEncKey, HsId, InertTorClient, KeystoreSelector, TorClientConfig};
use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
use safelog::DisplayRedacted;
use tor_rtcompat::Runtime;
#[cfg(feature = "onion-service-cli-extra")]
use {
std::collections::{HashMap, hash_map::Entry},
tor_hsclient::HsClientDescEncKeypairSpecifier,
tor_hscrypto::pk::HsClientDescEncKeypair,
tor_keymgr::{CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, KeystoreId},
};
use std::fs::OpenOptions;
use std::io::{self, Write};
use std::str::FromStr;
#[derive(Parser, Debug)]
pub(crate) enum HscSubcommands {
#[command(subcommand)]
Hsc(HscSubcommand),
}
#[derive(Debug, Subcommand)]
pub(crate) enum HscSubcommand {
#[command(arg_required_else_help = true)]
GetKey(GetKeyArgs),
#[command(subcommand)]
Key(KeySubcommand),
#[cfg(feature = "onion-service-cli-extra")]
#[command(name = "ctor-migrate")]
CTorMigrate(CTorMigrateArgs),
}
#[derive(Debug, Subcommand)]
pub(crate) enum KeySubcommand {
#[command(arg_required_else_help = true)]
Get(GetKeyArgs),
#[command(arg_required_else_help = true)]
Rotate(RotateKeyArgs),
#[command(arg_required_else_help = true)]
Remove(RemoveKeyArgs),
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
enum KeyType {
#[default]
ServiceDiscovery,
}
#[derive(Debug, Clone, Args)]
pub(crate) struct GetKeyArgs {
#[command(flatten)]
common: CommonArgs,
#[command(flatten)]
keygen: KeygenArgs,
#[arg(
long,
default_value_t = GenerateKey::IfNeeded,
value_enum
)]
generate: GenerateKey,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
enum GenerateKey {
No,
#[default]
IfNeeded,
}
#[derive(Debug, Clone, Args)]
pub(crate) struct CommonArgs {
#[arg(
long,
default_value_t = KeyType::ServiceDiscovery,
value_enum
)]
key_type: KeyType,
#[arg(long, short, default_value_t = false)]
batch: bool,
}
#[derive(Debug, Clone, Args)]
pub(crate) struct KeygenArgs {
#[arg(long, name = "FILE")]
output: String,
#[arg(long)]
overwrite: bool,
}
#[derive(Debug, Clone, Args)]
pub(crate) struct RotateKeyArgs {
#[command(flatten)]
common: CommonArgs,
#[command(flatten)]
keygen: KeygenArgs,
}
#[derive(Debug, Clone, Args)]
pub(crate) struct RemoveKeyArgs {
#[command(flatten)]
common: CommonArgs,
}
#[derive(Debug, Clone, Args)]
#[cfg(feature = "onion-service-cli-extra")]
pub(crate) struct CTorMigrateArgs {
#[arg(long, short, default_value_t = false)]
batch: bool,
#[arg(long, short)]
from: KeystoreId,
}
pub(crate) fn run<R: Runtime>(
runtime: R,
hsc_matches: &ArgMatches,
config: &TorClientConfig,
) -> Result<()> {
use KeyType::*;
let subcommand =
HscSubcommand::from_arg_matches(hsc_matches).expect("Could not parse hsc subcommand");
let client = TorClient::with_runtime(runtime)
.config(config.clone())
.create_inert()?;
match subcommand {
HscSubcommand::GetKey(args) => {
eprintln!(
"warning: using deprecated command 'arti hsc key-get` (hint: use 'arti hsc key get' instead)"
);
match args.common.key_type {
ServiceDiscovery => prepare_service_discovery_key(&args, &client),
}
}
HscSubcommand::Key(subcommand) => run_key(subcommand, &client),
#[cfg(feature = "onion-service-cli-extra")]
HscSubcommand::CTorMigrate(args) => migrate_ctor_keys(&args, &client),
}
}
fn run_key(subcommand: KeySubcommand, client: &InertTorClient) -> Result<()> {
match subcommand {
KeySubcommand::Get(args) => prepare_service_discovery_key(&args, client),
KeySubcommand::Rotate(args) => rotate_service_discovery_key(&args, client),
KeySubcommand::Remove(args) => remove_service_discovery_key(&args, client),
}
}
fn prepare_service_discovery_key(args: &GetKeyArgs, client: &InertTorClient) -> Result<()> {
let addr = get_onion_address(&args.common)?;
let key = match args.generate {
GenerateKey::IfNeeded => {
client
.get_service_discovery_key(addr)?
.map(Ok)
.unwrap_or_else(|| {
client.generate_service_discovery_key(KeystoreSelector::Primary, addr)
})?
}
GenerateKey::No => match client.get_service_discovery_key(addr)? {
Some(key) => key,
None => {
return Err(anyhow!(
"Service discovery key not found. Rerun with --generate=if-needed to generate a new service discovery keypair"
));
}
},
};
display_service_discovery_key(&args.keygen, &key)
}
fn display_service_discovery_key(args: &KeygenArgs, key: &HsClientDescEncKey) -> Result<()> {
match args.output.as_str() {
"-" => write_public_key(io::stdout(), key)?,
filename => {
let res = OpenOptions::new()
.create(true)
.create_new(!args.overwrite)
.write(true)
.truncate(true)
.open(filename)
.and_then(|f| write_public_key(f, key));
if let Err(e) = res {
match e.kind() {
io::ErrorKind::AlreadyExists => {
return Err(anyhow!(
"{filename} already exists. Move it, or rerun with --overwrite to overwrite it"
));
}
_ => {
return Err(e)
.with_context(|| format!("could not write public key to {filename}"));
}
}
}
}
}
Ok(())
}
fn write_public_key(mut f: impl io::Write, key: &HsClientDescEncKey) -> io::Result<()> {
writeln!(f, "{}", key)?;
Ok(())
}
fn rotate_service_discovery_key(args: &RotateKeyArgs, client: &InertTorClient) -> Result<()> {
let addr = get_onion_address(&args.common)?;
let msg = format!(
"rotate client restricted discovery key for {}?",
addr.display_unredacted()
);
if !args.common.batch && !prompt(&msg)? {
return Ok(());
}
let key = client.rotate_service_discovery_key(KeystoreSelector::default(), addr)?;
display_service_discovery_key(&args.keygen, &key)
}
fn remove_service_discovery_key(args: &RemoveKeyArgs, client: &InertTorClient) -> Result<()> {
let addr = get_onion_address(&args.common)?;
let msg = format!(
"remove client restricted discovery key for {}?",
addr.display_unredacted()
);
if !args.common.batch && !prompt(&msg)? {
return Ok(());
}
let _key = client.remove_service_discovery_key(KeystoreSelector::default(), addr)?;
Ok(())
}
#[cfg(feature = "onion-service-cli-extra")]
fn migrate_ctor_keys(args: &CTorMigrateArgs, client: &InertTorClient) -> Result<()> {
let keymgr = client.keymgr()?;
let ctor_client_entries = read_ctor_keys(&keymgr.list_by_id(&args.from)?, args)?;
let arti_keystore_id = KeystoreId::from_str("arti")
.map_err(|_| anyhow!("Default arti keystore ID is not valid?!"))?;
for (hsid, entry) in ctor_client_entries {
if let Ok(Some(key)) = keymgr.get_entry::<HsClientDescEncKeypair>(&entry) {
let key_exists = keymgr
.get_from::<HsClientDescEncKeypair>(
&HsClientDescEncKeypairSpecifier::new(hsid),
&arti_keystore_id,
)?
.is_some();
let proceed = if args.batch || !key_exists {
true
} else {
let p = format!(
"Found key in the primary keystore for service {}, do you want to replace it? ",
hsid.display_redacted()
);
prompt(&p)?
};
if proceed {
let res = keymgr.insert(
key,
&HsClientDescEncKeypairSpecifier::new(hsid),
(&arti_keystore_id).into(),
true,
);
if let Err(e) = res {
eprintln!(
"Failed to insert key for service {}: {e}",
hsid.display_redacted()
);
}
}
}
}
Ok(())
}
fn get_onion_address(args: &CommonArgs) -> Result<HsId, anyhow::Error> {
let mut addr = String::new();
if !args.batch {
print!("Enter an onion address: ");
io::stdout().flush().map_err(|e| anyhow!(e))?;
};
io::stdin().read_line(&mut addr).map_err(|e| anyhow!(e))?;
HsId::from_str(addr.trim_end()).map_err(|e| anyhow!(e))
}
#[cfg(feature = "onion-service-cli-extra")]
fn read_ctor_keys<'a>(
entries: &[KeystoreEntryResult<KeystoreEntry<'a>>],
args: &CTorMigrateArgs,
) -> Result<HashMap<HsId, KeystoreEntry<'a>>> {
let mut ctor_client_entries = HashMap::new();
for entry in entries.iter().flatten() {
if let KeyPath::CTor(CTorPath::HsClientDescEncKeypair { hs_id }) = entry.key_path() {
match ctor_client_entries.entry(*hs_id) {
Entry::Occupied(_) => {
return Err(anyhow!(
"Invalid C Tor keystore (multiple keys exist for service {})",
hs_id.display_redacted()
));
}
Entry::Vacant(v) => {
v.insert(entry.clone());
}
}
};
}
if ctor_client_entries.is_empty() {
return Err(anyhow!(
"No CTor client keys found in keystore {}",
args.from,
));
}
Ok(ctor_client_entries)
}