use std::ops::Deref;
use std::str::FromStr;
use anyhow::Result;
use arti_client::{InertTorClient, TorClient, TorClientConfig};
use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand};
use tor_keymgr::{KeyMgr, KeystoreEntry, KeystoreEntryResult, KeystoreId, UnrecognizedEntryError};
use tor_rtcompat::Runtime;
use crate::{ArtiConfig, subcommands::prompt};
#[cfg(feature = "onion-service-service")]
use tor_hsservice::OnionService;
const LINE_LEN: usize = 80;
#[derive(Debug, Parser)]
pub(crate) enum KeysSubcommands {
#[command(subcommand)]
Keys(KeysSubcommand),
}
#[derive(Subcommand, Debug, Clone)]
pub(crate) enum KeysSubcommand {
List(ListArgs),
ListKeystores,
CheckIntegrity(CheckIntegrityArgs),
}
#[derive(Debug, Clone, Args)]
pub(crate) struct ListArgs {
#[arg(short, long)]
keystore_id: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub(crate) struct CheckIntegrityArgs {
#[arg(short, long)]
keystore_id: Option<KeystoreId>,
#[arg(long, short, default_value_t = false)]
sweep: bool,
#[arg(long, short, default_value_t = false)]
batch: bool,
}
#[derive(Clone)]
struct InvalidKeystoreEntries<'a> {
keystore_id: KeystoreId,
entries: Vec<InvalidKeystoreEntry<'a>>,
}
#[derive(Clone)]
struct InvalidKeystoreEntry<'a> {
entry: KeystoreEntryResult<KeystoreEntry<'a>>,
error_msg: String,
}
pub(crate) fn run<R: Runtime>(
runtime: R,
keys_matches: &ArgMatches,
config: &ArtiConfig,
client_config: &TorClientConfig,
) -> Result<()> {
let subcommand =
KeysSubcommand::from_arg_matches(keys_matches).expect("Could not parse keys subcommand");
let rt = runtime.clone();
let client_builder = TorClient::with_runtime(runtime).config(client_config.clone());
match subcommand {
KeysSubcommand::List(args) => run_list_keys(&args, &client_builder.create_inert()?),
KeysSubcommand::ListKeystores => run_list_keystores(&client_builder.create_inert()?),
KeysSubcommand::CheckIntegrity(args) => run_check_integrity(
&args,
&rt.reenter_block_on(client_builder.create_bootstrapped())?,
config,
client_config,
),
}
}
fn display_entry(entry: &KeystoreEntry, keymgr: &KeyMgr) {
let raw_entry = entry.raw_entry();
match keymgr.describe(entry.key_path()) {
Some(e) => {
println!(" Keystore ID: {}", entry.keystore_id());
println!(" Role: {}", e.role());
println!(" Summary: {}", e.summary());
println!(" KeystoreItemType: {:?}", entry.key_type());
println!(" Location: {}", raw_entry.raw_id());
let extra_info = e.extra_info();
println!(" Extra info:");
for (key, value) in extra_info {
println!(" - {key}: {value}");
}
}
None => {
println!(" Unrecognized path {}", raw_entry.raw_id());
}
}
println!("\n {}", "-".repeat(LINE_LEN));
}
fn display_unrecognized_entry(entry: &UnrecognizedEntryError) {
let raw_entry = entry.entry();
println!(" Unrecognized entry");
#[allow(clippy::single_match)]
match raw_entry.raw_id() {
tor_keymgr::RawEntryId::Path(p) => {
println!(" Keystore ID: {}", raw_entry.keystore_id());
println!(" Location: {}", p.to_string_lossy());
println!(" Error: {}", entry.error());
}
other => {
panic!("Unhandled enum variant: {:?}", other);
}
}
println!("\n {}\n", "-".repeat(LINE_LEN));
}
fn run_list_keys(args: &ListArgs, client: &InertTorClient) -> Result<()> {
let keymgr = client.keymgr()?;
match &args.keystore_id {
Some(s) => {
let id = KeystoreId::from_str(s)?;
let empty_err_msg = format!("Currently there are no entries in the keystore {}.", s);
display_keystore_entries(
&keymgr.list_by_id(&id)?,
keymgr,
"Keystore entries",
&empty_err_msg,
);
}
None => {
display_keystore_entries(
&keymgr.list()?,
keymgr,
"Keystore entries",
"Currently there are no entries in any of the keystores.",
);
}
}
Ok(())
}
fn run_list_keystores(client: &InertTorClient) -> Result<()> {
let keymgr = client.keymgr()?;
let entries = keymgr.list_keystores();
if entries.is_empty() {
println!("Currently there are no keystores available.");
} else {
println!(" Keystores:\n");
for entry in entries {
println!(" - {:?}\n", entry.as_ref());
}
}
Ok(())
}
fn run_check_integrity<R: Runtime>(
args: &CheckIntegrityArgs,
client: &TorClient<R>,
config: &ArtiConfig,
client_config: &TorClientConfig,
) -> Result<()> {
let keymgr = client.keymgr()?;
let keystore_ids = match &args.keystore_id {
Some(id) => vec![id.to_owned()],
None => keymgr.list_keystores(),
};
let keystores: Vec<(_, Vec<KeystoreEntryResult<KeystoreEntry>>)> = keystore_ids
.into_iter()
.map(|id| keymgr.list_by_id(&id).map(|entries| (id, entries)))
.collect::<Result<Vec<_>, _>>()?;
let mut affected_keystores = Vec::new();
cfg_if::cfg_if! {
if #[cfg(feature = "onion-service-service")] {
let services = create_all_services(config, client_config)?;
let mut expired_entries: Vec<_> = get_expired_keys(&services, client)?;
}
}
for (id, entries) in keystores {
let mut invalid_entries = entries
.into_iter()
.filter_map(|entry| match entry {
Ok(e) => keymgr
.validate_entry_integrity(&e)
.map_err(|err| InvalidKeystoreEntry {
entry: Ok(e),
error_msg: err.to_string(),
})
.err(),
Err(err) => {
let error = err.error().to_string();
Some(InvalidKeystoreEntry {
entry: Err(err),
error_msg: error,
})
}
})
.collect::<Vec<_>>();
cfg_if::cfg_if! {
if #[cfg(feature = "onion-service-service")] {
expired_entries.retain(|expired_entry| {
match &expired_entry.entry {
Ok(entry) => {
if entry.keystore_id() == &id {
invalid_entries.push(expired_entry.clone());
return false;
}
}
Err(err) => {
eprintln!("WARNING: Unexpected invalid keystore entry encountered: {}", err);
}
}
true
})
}
}
if invalid_entries.is_empty() {
println!("{}: OK.\n", id);
continue;
}
affected_keystores.push(InvalidKeystoreEntries {
keystore_id: id,
entries: invalid_entries,
});
}
cfg_if::cfg_if! {
if #[cfg(feature = "onion-service-service")] {
if !expired_entries.is_empty() {
return Err(anyhow::anyhow!(
"Encountered an expired key that doesn't belong to a registered keystore."
));
}
}
}
display_invalid_keystore_entries(&affected_keystores);
maybe_remove_invalid_entries(args, &affected_keystores, keymgr)?;
Ok(())
}
fn display_invalid_keystore_entries(affected_keystores: &[InvalidKeystoreEntries]) {
if affected_keystores.is_empty() {
return;
}
print_check_integrity_incipit(affected_keystores);
for InvalidKeystoreEntries {
keystore_id,
entries,
} in affected_keystores
{
println!("\nInvalid keystore entries in keystore {}:\n", keystore_id);
for InvalidKeystoreEntry { entry, error_msg } in entries {
let raw_entry = match entry {
Ok(e) => e.raw_entry(),
Err(e) => e.entry().into(),
};
println!("{}", raw_entry.raw_id());
println!("\tError: {}", error_msg);
}
}
}
fn display_keystore_entries(
entries: &[KeystoreEntryResult<KeystoreEntry>],
keymgr: &KeyMgr,
header: &str,
empty_err_msg: &str,
) {
if entries.is_empty() {
println!("{empty_err_msg}");
return;
}
println!(" ===== {} =====\n\n", header);
for entry in entries {
match entry {
Ok(entry) => {
display_entry(entry, keymgr);
}
Err(entry) => {
display_unrecognized_entry(entry);
}
}
}
}
#[cfg(feature = "onion-service-service")]
fn create_all_services(
config: &ArtiConfig,
client_config: &TorClientConfig,
) -> Result<Vec<OnionService>> {
let mut services = Vec::new();
for (_, cfg) in config.onion_services.iter() {
services.push(
TorClient::<tor_rtcompat::PreferredRuntime>::create_onion_service(
client_config,
cfg.svc_cfg.clone(),
)?,
);
}
Ok(services)
}
#[cfg(feature = "onion-service-service")]
fn get_expired_keys<'a, R: Runtime>(
services: &'a Vec<OnionService>,
client: &TorClient<R>,
) -> Result<Vec<InvalidKeystoreEntry<'a>>> {
let netdir = client.dirmgr().timely_netdir()?;
let mut expired_keys = Vec::new();
for service in services {
expired_keys.append(
&mut service
.list_expired_keys(&netdir)?
.into_iter()
.map(|entry| InvalidKeystoreEntry {
entry: Ok(entry),
error_msg: "The entry is expired.".to_string(),
})
.collect(),
);
}
Ok(expired_keys)
}
fn maybe_remove_invalid_entries(
args: &CheckIntegrityArgs,
affected_keystores: &[InvalidKeystoreEntries],
keymgr: &KeyMgr,
) -> Result<()> {
if affected_keystores.is_empty() || !args.sweep {
return Ok(());
}
let should_remove = args.batch || prompt("Remove all invalid entries?")?;
if !should_remove {
return Ok(());
}
for InvalidKeystoreEntries {
keystore_id: _,
entries,
} in affected_keystores
{
for InvalidKeystoreEntry {
entry,
error_msg: _,
} in entries.iter()
{
let raw_entry = match entry {
Ok(e) => &e.raw_entry(),
Err(e) => e.entry().deref(),
};
if keymgr
.remove_unchecked(&raw_entry.raw_id().to_string(), raw_entry.keystore_id())
.is_err()
{
eprintln!("Failed to remove entry at location: {}", raw_entry.raw_id());
}
}
}
Ok(())
}
fn print_check_integrity_incipit(affected_keystores: &[InvalidKeystoreEntries]) {
let len = affected_keystores.len();
let mut incipit = "Found problems in keystore".to_string();
if len > 1 {
incipit.push('s');
}
incipit.push_str(": ");
let keystore_names: Vec<_> = affected_keystores
.iter()
.map(|x| x.keystore_id.to_string())
.collect();
incipit.push_str(&keystore_names.join(", "));
incipit.push('.');
println!("{}", incipit);
}