use std::fs;
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;
use std::str::FromStr as _;
use std::sync::Arc;
use crate::keystore::ctor::CTorKeystore;
use crate::keystore::ctor::err::{CTorKeystoreError, MalformedClientKeyError};
use crate::keystore::fs_utils::{FilesystemAction, FilesystemError, RelKeyPath, checked_op};
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
use crate::raw::{RawEntryId, RawKeystoreEntry};
use crate::{
CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, KeystoreId, Result,
UnrecognizedEntryError,
};
use fs_mistrust::Mistrust;
use itertools::Itertools as _;
use tor_basic_utils::PathExt;
use tor_error::debug_report;
use tor_hscrypto::pk::{HsClientDescEncKeypair, HsId};
use tor_key_forge::{KeyType, KeystoreItemType};
use tor_llcrypto::pk::curve25519;
use tracing::debug;
pub struct CTorClientKeystore(CTorKeystore);
impl CTorClientKeystore {
pub fn from_path_and_mistrust(
keystore_dir: impl AsRef<Path>,
mistrust: &Mistrust,
id: KeystoreId,
) -> Result<Self> {
CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id).map(Self)
}
}
macro_rules! hsid_if_supported {
($spec:expr, $ret:expr, $key_type:expr) => {{
let Some(ctor_path) = $spec.ctor_path() else {
return $ret;
};
let CTorPath::HsClientDescEncKeypair { hs_id } = ctor_path else {
return $ret;
};
if *$key_type != KeyType::X25519StaticKeypair.into() {
return Err(CTorKeystoreError::InvalidKeystoreItemType {
item_type: $key_type.clone(),
item: "client restricted discovery key".into(),
}
.into());
}
hs_id
}};
}
impl CTorClientKeystore {
fn list_entries(&self, dir: &RelKeyPath) -> Result<fs::ReadDir> {
let entries = checked_op!(read_directory, dir)
.map_err(|e| FilesystemError::FsMistrust {
action: FilesystemAction::Read,
path: dir.rel_path_unchecked().into(),
err: e.into(),
})
.map_err(CTorKeystoreError::Filesystem)?;
Ok(entries)
}
}
const KEY_EXTENSION: &str = "auth_private";
impl CTorClientKeystore {
fn read_key(&self, key_path: &Path) -> StdResult<Option<String>, CTorKeystoreError> {
let key_path = self.0.rel_path(key_path.into());
let content = match checked_op!(read_to_string, key_path) {
Err(fs_mistrust::Error::NotFound(_)) => {
return Ok(None);
}
res => res
.map_err(|err| FilesystemError::FsMistrust {
action: FilesystemAction::Read,
path: key_path.rel_path_unchecked().into(),
err: err.into(),
})
.map_err(CTorKeystoreError::Filesystem)?,
};
Ok(Some(content))
}
fn list_keys(
&self,
) -> Result<
impl Iterator<Item = StdResult<(HsId, HsClientDescEncKeypair), CTorKeystoreError>> + '_,
> {
use CTorKeystoreError::*;
let dir = self.0.rel_path(PathBuf::from("."));
Ok(self.list_entries(&dir)?.filter_map(|entry| {
let entry = entry
.map_err(|e| {
debug!("cannot access key entry: {e}");
})
.ok()?;
let file_name = entry.file_name();
let path: &Path = file_name.as_ref();
let Some(KEY_EXTENSION) = path.extension().and_then(|e| e.to_str()) else {
return Some(Err(MalformedKey {
path: entry.path(),
err: MalformedClientKeyError::InvalidFormat.into(),
}));
};
let content = match self.read_key(path) {
Ok(c) => c,
Err(e) => {
debug_report!(&e, "failed to read {}", path.display_lossy());
return Some(Err(e));
}
}?;
Some(
parse_client_keypair(content.trim()).map_err(|e| MalformedKey {
path: path.into(),
err: e.into(),
}),
)
}))
}
}
fn parse_client_keypair(
key: impl AsRef<str>,
) -> StdResult<(HsId, HsClientDescEncKeypair), MalformedClientKeyError> {
let key = key.as_ref();
let (hsid, auth_type, key_type, encoded_key) = key
.split(':')
.collect_tuple()
.ok_or(MalformedClientKeyError::InvalidFormat)?;
if auth_type != "descriptor" {
return Err(MalformedClientKeyError::InvalidAuthType(auth_type.into()));
}
if key_type != "x25519" {
return Err(MalformedClientKeyError::InvalidKeyType(key_type.into()));
}
let encoded_key = encoded_key.to_uppercase();
let x25519_sk = data_encoding::BASE32_NOPAD.decode(encoded_key.as_bytes())?;
let x25519_sk: [u8; 32] = x25519_sk
.try_into()
.map_err(|_| MalformedClientKeyError::InvalidKeyMaterial)?;
let secret = curve25519::StaticSecret::from(x25519_sk);
let public = (&secret).into();
let x25519_keypair = curve25519::StaticKeypair { secret, public };
let hsid = HsId::from_str(&format!("{hsid}.onion"))?;
Ok((hsid, x25519_keypair.into()))
}
impl Keystore for CTorClientKeystore {
fn id(&self) -> &KeystoreId {
&self.0.id
}
fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
self.get(key_spec, item_type).map(|k| k.is_some())
}
fn get(
&self,
key_spec: &dyn KeySpecifier,
item_type: &KeystoreItemType,
) -> Result<Option<ErasedKey>> {
let want_hsid = hsid_if_supported!(key_spec, Ok(None), item_type);
Ok(self
.list_keys()?
.find_map(|entry| {
if let Ok((hsid, key)) = entry {
(hsid == want_hsid).then(|| key.into())
} else {
None
}
})
.map(|k: curve25519::StaticKeypair| Box::new(k) as ErasedKey))
}
#[cfg(feature = "onion-service-cli-extra")]
fn raw_entry_id(&self, raw_id: &str) -> Result<RawEntryId> {
Ok(RawEntryId::Path(PathBuf::from(raw_id.to_string())))
}
fn insert(&self, _key: &dyn EncodableItem, _key_spec: &dyn KeySpecifier) -> Result<()> {
Err(CTorKeystoreError::NotSupported { action: "insert" }.into())
}
fn remove(
&self,
_key_spec: &dyn KeySpecifier,
_item_type: &KeystoreItemType,
) -> Result<Option<()>> {
Err(CTorKeystoreError::NotSupported { action: "remove" }.into())
}
#[cfg(feature = "onion-service-cli-extra")]
fn remove_unchecked(&self, _entry_id: &RawEntryId) -> Result<()> {
Err(CTorKeystoreError::NotSupported {
action: "remove_unchecked",
}
.into())
}
fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
use CTorKeystoreError::*;
let keys = self
.list_keys()?
.filter_map(|entry| match entry {
Ok((hs_id, _)) => {
let key_path: KeyPath = CTorPath::HsClientDescEncKeypair { hs_id }.into();
let key_type: KeystoreItemType = KeyType::X25519StaticKeypair.into();
let raw_id = RawEntryId::Path(key_path.ctor()?.to_string().into());
Some(Ok(KeystoreEntry::new(
key_path,
key_type,
self.id(),
raw_id,
)))
}
Err(e) => match e {
MalformedKey { ref path, err: _ } => {
let raw_id = RawEntryId::Path(path.clone());
let entry = RawKeystoreEntry::new(raw_id, self.id().clone()).into();
Some(Err(UnrecognizedEntryError::new(entry, Arc::new(e))))
}
InvalidKeystoreItemType { .. } => None,
Filesystem(_) => None,
NotSupported { .. } => None,
Bug(_) => None,
},
})
.collect();
Ok(keys)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use std::fs;
use tempfile::{TempDir, tempdir};
use crate::test_utils::{DummyKey, TestCTorSpecifier, assert_found};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
const ALICE_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/alice.auth_private");
const BOB_AUTH_PRIVATE_INVALID: &str = include_str!("../../../testdata/bob.auth_private");
const CAROL_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/carol.auth_private");
const DAN_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/dan.auth_private");
const HSID: &str = "mnyizjj7m3hpcr7i5afph3zt7maa65johyu2ruis6z7cmnjmaj3h6tad.onion";
fn init_keystore(id: &str) -> (CTorClientKeystore, TempDir) {
let keystore_dir = tempdir().unwrap();
#[cfg(unix)]
fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
let id = KeystoreId::from_str(id).unwrap();
let keystore =
CTorClientKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default(), id)
.unwrap();
let keys: &[(&str, &str)] = &[
("alice.auth_private", ALICE_AUTH_PRIVATE_VALID),
("bob.auth_private", BOB_AUTH_PRIVATE_INVALID),
(
"alice-truncated.auth_private",
&ALICE_AUTH_PRIVATE_VALID[..100],
),
("carol.auth", CAROL_AUTH_PRIVATE_VALID),
("dan.auth_private", DAN_AUTH_PRIVATE_VALID),
];
for (name, key) in keys {
fs::write(keystore_dir.path().join(name), key).unwrap();
}
(keystore, keystore_dir)
}
#[test]
fn get() {
let (keystore, _keystore_dir) = init_keystore("foo");
let path = CTorPath::HsClientDescEncKeypair {
hs_id: HsId::from_str(HSID).unwrap(),
};
assert_found!(
keystore,
&TestCTorSpecifier(path.clone()),
&KeyType::X25519StaticKeypair,
false
);
for hsid in &[ALICE_AUTH_PRIVATE_VALID, DAN_AUTH_PRIVATE_VALID] {
let onion = hsid.split(":").next().unwrap();
let hsid = HsId::from_str(&format!("{onion}.onion")).unwrap();
let path = CTorPath::HsClientDescEncKeypair {
hs_id: hsid.clone(),
};
assert_found!(
keystore,
&TestCTorSpecifier(path.clone()),
&KeyType::X25519StaticKeypair,
true
);
}
let keys: Vec<_> = keystore
.list()
.unwrap()
.into_iter()
.filter(|e| e.is_ok())
.collect();
assert_eq!(keys.len(), 2);
assert!(keys.iter().all(|entry| {
entry.as_ref().unwrap().key_type() == &KeyType::X25519StaticKeypair.into()
}));
}
#[test]
fn unsupported_operation() {
let (keystore, _keystore_dir) = init_keystore("foo");
let path = CTorPath::HsClientDescEncKeypair {
hs_id: HsId::from_str(HSID).unwrap(),
};
let err = keystore
.remove(
&TestCTorSpecifier(path.clone()),
&KeyType::X25519StaticKeypair.into(),
)
.unwrap_err();
assert_eq!(err.to_string(), "Operation not supported: remove");
let err = keystore
.insert(&DummyKey, &TestCTorSpecifier(path))
.unwrap_err();
assert_eq!(err.to_string(), "Operation not supported: insert");
}
#[test]
fn wrong_keytype() {
let (keystore, _keystore_dir) = init_keystore("foo");
let path = CTorPath::HsClientDescEncKeypair {
hs_id: HsId::from_str(HSID).unwrap(),
};
let err = keystore
.get(
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519PublicKey.into(),
)
.map(|_| ())
.unwrap_err();
assert_eq!(
err.to_string(),
"Invalid item type Ed25519PublicKey for client restricted discovery key"
);
}
#[test]
fn list() {
let (keystore, _keystore_dir) = init_keystore("foo");
let mut recognized = 0;
let mut unrecognized = 0;
for e in keystore.list().unwrap() {
if e.is_ok() {
recognized += 1;
} else {
unrecognized += 1;
}
}
assert_eq!(recognized, 2);
assert_eq!(unrecognized, 3);
}
}