use crate::keystore::ctor::CTorKeystore;
use crate::keystore::ctor::err::{CTorKeystoreError, MalformedServiceKeyError};
use crate::keystore::fs_utils::{FilesystemAction, FilesystemError, checked_op};
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore, KeystoreId};
use crate::raw::{RawEntryId, RawKeystoreEntry};
use crate::{
CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, Result, UnrecognizedEntryError,
};
use fs_mistrust::Mistrust;
use tor_basic_utils::PathExt as _;
use tor_error::internal;
use tor_key_forge::{KeyType, KeystoreItemType};
use tor_llcrypto::pk::ed25519;
use tor_persist::hsnickname::HsNickname;
use std::io;
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;
#[allow(unused_imports)]
use std::str::FromStr;
use std::sync::Arc;
use itertools::Itertools;
use walkdir::WalkDir;
pub struct CTorServiceKeystore {
keystore: CTorKeystore,
nickname: HsNickname,
}
impl CTorServiceKeystore {
pub fn from_path_and_mistrust(
keystore_dir: impl AsRef<Path>,
mistrust: &Mistrust,
id: KeystoreId,
nickname: HsNickname,
) -> Result<Self> {
let keystore = CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id)?;
Ok(Self { keystore, nickname })
}
}
macro_rules! rel_path_if_supported {
($self:expr, $spec:expr, $ret:expr, $item_type:expr) => {{
use CTorPath::*;
use KeystoreItemType::*;
let Some(ctor_path) = $spec.ctor_path() else {
return $ret;
};
let nickname = match &ctor_path {
HsClientDescEncKeypair { .. } => return $ret,
HsIdKeypair { nickname } | HsIdPublicKey { nickname } => nickname,
};
if nickname != &$self.nickname {
return $ret;
};
let relpath = $self
.keystore
.rel_path(PathBuf::from(ctor_path.to_string()));
match ($item_type, &ctor_path) {
(Key(KeyType::Ed25519ExpandedKeypair), HsIdKeypair { .. })
| (Key(KeyType::Ed25519PublicKey), HsIdPublicKey { .. }) => Ok(()),
_ => Err(CTorKeystoreError::InvalidKeystoreItemType {
item_type: $item_type.clone(),
item: format!("key {}", relpath.rel_path_unchecked().display_lossy()),
}),
}?;
relpath
}};
}
impl Keystore for CTorServiceKeystore {
fn id(&self) -> &KeystoreId {
&self.keystore.id
}
fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
let path = rel_path_if_supported!(self, key_spec, Ok(false), item_type);
let meta = match checked_op!(metadata, path) {
Ok(meta) => meta,
Err(fs_mistrust::Error::NotFound(_)) => return Ok(false),
Err(e) => {
return Err(FilesystemError::FsMistrust {
action: FilesystemAction::Read,
path: path.rel_path_unchecked().into(),
err: e.into(),
})
.map_err(|e| CTorKeystoreError::Filesystem(e).into());
}
};
if meta.is_file() {
Ok(true)
} else {
Err(
CTorKeystoreError::Filesystem(FilesystemError::NotARegularFile(
path.rel_path_unchecked().into(),
))
.into(),
)
}
}
fn get(
&self,
key_spec: &dyn KeySpecifier,
item_type: &KeystoreItemType,
) -> Result<Option<ErasedKey>> {
use KeystoreItemType::*;
let path = rel_path_if_supported!(self, key_spec, Ok(None), item_type);
let key = match checked_op!(read, path) {
Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
res => res
.map_err(|err| FilesystemError::FsMistrust {
action: FilesystemAction::Read,
path: path.rel_path_unchecked().into(),
err: err.into(),
})
.map_err(CTorKeystoreError::Filesystem)?,
};
let parse_err = |err: MalformedServiceKeyError| CTorKeystoreError::MalformedKey {
path: path.rel_path_unchecked().into(),
err: err.into(),
};
let parsed_key: ErasedKey = match item_type {
Key(KeyType::Ed25519ExpandedKeypair) => parse_ed25519_keypair(&key)
.map_err(parse_err)
.map(Box::new)?,
Key(KeyType::Ed25519PublicKey) => parse_ed25519_public(&key)
.map_err(parse_err)
.map(Box::new)?,
_ => {
return Err(
internal!("item type was not validated by rel_path_if_supported?!").into(),
);
}
};
Ok(Some(parsed_key))
}
#[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>>> {
let all_keys = [
(
CTorPath::HsIdPublicKey {
nickname: self.nickname.clone(),
},
KeyType::Ed25519PublicKey,
),
(
CTorPath::HsIdKeypair {
nickname: self.nickname.clone(),
},
KeyType::Ed25519ExpandedKeypair,
),
];
let valid_rel_paths = all_keys
.into_iter()
.map(|(ctor_path, key_type)| {
let path = rel_path_if_supported!(
self,
ctor_path,
Err(internal!("Failed to build {ctor_path:?} path?!").into()),
KeystoreItemType::Key(key_type.clone())
);
Ok((ctor_path, key_type, path))
})
.collect::<Result<Vec<_>>>()?;
let keystore_path = self.keystore.keystore_dir.as_path();
WalkDir::new(keystore_path)
.into_iter()
.map(|entry| {
let entry = entry
.map_err(|e| {
let msg = e.to_string();
FilesystemError::Io {
action: FilesystemAction::Read,
path: keystore_path.into(),
err: e
.into_io_error()
.unwrap_or_else(|| io::Error::other(msg.clone()))
.into(),
}
})
.map_err(CTorKeystoreError::Filesystem)?;
let path = entry.path();
if entry.file_type().is_dir() {
return Ok(None);
}
let path = path.strip_prefix(keystore_path).map_err(|_| {
tor_error::internal!(
"found key {} outside of keystore_dir {}?!",
path.display_lossy(),
keystore_path.display_lossy()
)
})?;
if let Some(parent) = path.parent() {
self.keystore
.keystore_dir
.read_directory(parent)
.map_err(|e| FilesystemError::FsMistrust {
action: FilesystemAction::Read,
path: parent.into(),
err: e.into(),
})
.map_err(CTorKeystoreError::Filesystem)?;
}
let maybe_path =
valid_rel_paths
.iter()
.find_map(|(ctor_path, key_type, rel_path)| {
(path == rel_path.rel_path_unchecked())
.then_some((ctor_path, key_type, rel_path))
});
let res = match maybe_path {
Some((ctor_path, key_type, rel_path)) => Ok(KeystoreEntry::new(
KeyPath::CTor(ctor_path.clone()),
KeystoreItemType::Key(key_type.clone()),
self.id(),
RawEntryId::Path(rel_path.rel_path_unchecked().to_owned()),
)),
None => {
let raw_id = RawEntryId::Path(path.into());
let entry = RawKeystoreEntry::new(raw_id, self.id().clone()).into();
Err(UnrecognizedEntryError::new(
entry,
Arc::new(CTorKeystoreError::MalformedKey {
path: path.into(),
err: MalformedServiceKeyError::NotAKey.into(),
}),
))
}
};
Ok(Some(res))
})
.flatten_ok()
.collect()
}
}
macro_rules! parse_ed25519 {
($key:expr, $parse_fn:expr, $tag:expr, $key_len:expr) => {{
let expected_len = $tag.len() + $key_len;
if $key.len() != expected_len {
return Err(MalformedServiceKeyError::InvalidKeyLen {
len: $key.len(),
expected_len,
});
}
let (tag, key) = $key.split_at($tag.len());
if tag != $tag {
return Err(MalformedServiceKeyError::InvalidTag {
tag: tag.to_vec(),
expected_tag: $tag.into(),
});
}
($parse_fn)(key)
}};
}
fn parse_ed25519_public(key: &[u8]) -> StdResult<ed25519::PublicKey, MalformedServiceKeyError> {
const PUBKEY_TAG: &[u8] = b"== ed25519v1-public: type0 ==\0\0\0";
const PUBKEY_LEN: usize = 32;
parse_ed25519!(
key,
|key| ed25519::PublicKey::try_from(key)
.map_err(|e| MalformedServiceKeyError::from(Arc::new(e))),
PUBKEY_TAG,
PUBKEY_LEN
)
}
fn parse_ed25519_keypair(
key: &[u8],
) -> StdResult<ed25519::ExpandedKeypair, MalformedServiceKeyError> {
const KEYPAIR_TAG: &[u8] = b"== ed25519v1-secret: type0 ==\0\0\0";
const KEYPAIR_LEN: usize = 64;
parse_ed25519!(
key,
|key: &[u8]| {
let key: [u8; 64] = key
.try_into()
.map_err(|_| internal!("bad length on expanded ed25519 secret key "))?;
ed25519::ExpandedKeypair::from_secret_key_bytes(key)
.ok_or(MalformedServiceKeyError::Ed25519Keypair)
},
KEYPAIR_TAG,
KEYPAIR_LEN
)
}
#[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};
const PUBKEY: &[u8] = include_bytes!("../../../testdata/tor-service/hs_ed25519_public_key");
const PRIVKEY: &[u8] = include_bytes!("../../../testdata/tor-service/hs_ed25519_secret_key");
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
fn init_keystore(id: &str, nickname: &str) -> (CTorServiceKeystore, 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 nickname = HsNickname::from_str(nickname).unwrap();
let keystore = CTorServiceKeystore::from_path_and_mistrust(
&keystore_dir,
&Mistrust::default(),
id,
nickname,
)
.unwrap();
const KEYS: &[(&str, &[u8])] = &[
("hs_ed25519_public_key", PUBKEY),
("hs_ed25519_secret_key", PRIVKEY),
];
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", "allium-cepa");
let unk_nickname = HsNickname::new("acutus-cepa".into()).unwrap();
let path = CTorPath::HsIdPublicKey {
nickname: unk_nickname.clone(),
};
assert_found!(
keystore,
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519PublicKey,
false
);
let path = CTorPath::HsIdPublicKey {
nickname: keystore.nickname.clone(),
};
assert_found!(
keystore,
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519PublicKey,
true
);
let path = CTorPath::HsIdKeypair {
nickname: keystore.nickname.clone(),
};
assert_found!(
keystore,
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519ExpandedKeypair,
true
);
}
#[test]
fn unsupported_operation() {
let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
let path = CTorPath::HsIdPublicKey {
nickname: keystore.nickname.clone(),
};
let err = keystore
.remove(
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519PublicKey.into(),
)
.unwrap_err();
assert_eq!(err.to_string(), "Operation not supported: remove");
let err = keystore
.insert(&DummyKey, &TestCTorSpecifier(path.clone()))
.unwrap_err();
assert_eq!(err.to_string(), "Operation not supported: insert");
}
#[test]
fn wrong_keytype() {
let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
let path = CTorPath::HsIdPublicKey {
nickname: keystore.nickname.clone(),
};
let err = keystore
.get(
&TestCTorSpecifier(path.clone()),
&KeyType::X25519StaticKeypair.into(),
)
.map(|_| ())
.unwrap_err();
assert_eq!(
err.to_string(),
"Invalid item type X25519StaticKeypair for key hs_ed25519_public_key"
);
}
#[test]
fn list() {
let (keystore, keystore_dir) = init_keystore("foo", "allium-cepa");
let _ = fs::File::create(keystore_dir.path().join("unrecognized_key")).unwrap();
let keys: Vec<_> = keystore.list().unwrap();
assert_eq!(keys.len(), 3);
assert!(keys.iter().any(|entry| {
if let Ok(e) = entry.as_ref() {
return e.key_type() == &KeyType::Ed25519ExpandedKeypair.into();
}
false
}));
assert!(keys.iter().any(|entry| {
if let Ok(e) = entry.as_ref() {
return e.key_type() == &KeyType::Ed25519PublicKey.into();
}
false
}));
assert!(keys.iter().any(|entry| { entry.is_err() }));
}
}