1use crate::keystore::ctor::CTorKeystore;
6use crate::keystore::ctor::err::{CTorKeystoreError, MalformedServiceKeyError};
7use crate::keystore::fs_utils::{FilesystemAction, FilesystemError, checked_op};
8use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore, KeystoreId};
9use crate::raw::RawEntryId;
10use crate::{
11 CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, Result, UnrecognizedEntry,
12 UnrecognizedEntryError,
13};
14
15use fs_mistrust::Mistrust;
16use tor_basic_utils::PathExt as _;
17use tor_error::internal;
18use tor_key_forge::{KeyType, KeystoreItemType};
19use tor_llcrypto::pk::ed25519;
20use tor_persist::hsnickname::HsNickname;
21
22use std::io;
23use std::path::{Path, PathBuf};
24use std::result::Result as StdResult;
25#[allow(unused_imports)]
26use std::str::FromStr;
27use std::sync::Arc;
28
29use itertools::Itertools;
30use walkdir::WalkDir;
31
32pub struct CTorServiceKeystore {
61 keystore: CTorKeystore,
63 nickname: HsNickname,
65}
66
67impl CTorServiceKeystore {
68 pub fn from_path_and_mistrust(
74 keystore_dir: impl AsRef<Path>,
75 mistrust: &Mistrust,
76 id: KeystoreId,
77 nickname: HsNickname,
78 ) -> Result<Self> {
79 let keystore = CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id)?;
80
81 Ok(Self { keystore, nickname })
82 }
83}
84
85macro_rules! rel_path_if_supported {
90 ($self:expr, $spec:expr, $ret:expr, $item_type:expr) => {{
91 use CTorPath::*;
92 use KeystoreItemType::*;
93
94 let Some(ctor_path) = $spec.ctor_path() else {
97 return $ret;
98 };
99
100 let nickname = match &ctor_path {
102 HsClientDescEncKeypair { .. } => return $ret,
103 HsIdKeypair { nickname } | HsIdPublicKey { nickname } => nickname,
104 };
105
106 if nickname != &$self.nickname {
109 return $ret;
110 };
111
112 let relpath = $self
113 .keystore
114 .rel_path(PathBuf::from(ctor_path.to_string()));
115 match ($item_type, &ctor_path) {
116 (Key(KeyType::Ed25519ExpandedKeypair), HsIdKeypair { .. })
117 | (Key(KeyType::Ed25519PublicKey), HsIdPublicKey { .. }) => Ok(()),
118 _ => Err(CTorKeystoreError::InvalidKeystoreItemType {
119 item_type: $item_type.clone(),
120 item: format!("key {}", relpath.rel_path_unchecked().display_lossy()),
121 }),
122 }?;
123
124 relpath
125 }};
126}
127
128impl Keystore for CTorServiceKeystore {
129 fn id(&self) -> &KeystoreId {
130 &self.keystore.id
131 }
132
133 fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
134 let path = rel_path_if_supported!(self, key_spec, Ok(false), item_type);
135
136 let meta = match checked_op!(metadata, path) {
137 Ok(meta) => meta,
138 Err(fs_mistrust::Error::NotFound(_)) => return Ok(false),
139 Err(e) => {
140 return Err(FilesystemError::FsMistrust {
141 action: FilesystemAction::Read,
142 path: path.rel_path_unchecked().into(),
143 err: e.into(),
144 })
145 .map_err(|e| CTorKeystoreError::Filesystem(e).into());
146 }
147 };
148
149 if meta.is_file() {
151 Ok(true)
152 } else {
153 Err(
154 CTorKeystoreError::Filesystem(FilesystemError::NotARegularFile(
155 path.rel_path_unchecked().into(),
156 ))
157 .into(),
158 )
159 }
160 }
161
162 fn get(
163 &self,
164 key_spec: &dyn KeySpecifier,
165 item_type: &KeystoreItemType,
166 ) -> Result<Option<ErasedKey>> {
167 use KeystoreItemType::*;
168
169 let path = rel_path_if_supported!(self, key_spec, Ok(None), item_type);
170
171 let key = match checked_op!(read, path) {
172 Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
173 res => res
174 .map_err(|err| FilesystemError::FsMistrust {
175 action: FilesystemAction::Read,
176 path: path.rel_path_unchecked().into(),
177 err: err.into(),
178 })
179 .map_err(CTorKeystoreError::Filesystem)?,
180 };
181
182 let parse_err = |err: MalformedServiceKeyError| CTorKeystoreError::MalformedKey {
183 path: path.rel_path_unchecked().into(),
184 err: err.into(),
185 };
186
187 let parsed_key: ErasedKey = match item_type {
188 Key(KeyType::Ed25519ExpandedKeypair) => parse_ed25519_keypair(&key)
189 .map_err(parse_err)
190 .map(Box::new)?,
191 Key(KeyType::Ed25519PublicKey) => parse_ed25519_public(&key)
192 .map_err(parse_err)
193 .map(Box::new)?,
194 _ => {
195 return Err(
196 internal!("item type was not validated by rel_path_if_supported?!").into(),
197 );
198 }
199 };
200
201 Ok(Some(parsed_key))
202 }
203
204 #[cfg(feature = "onion-service-cli-extra")]
205 fn raw_entry_id(&self, raw_id: &str) -> Result<RawEntryId> {
206 Ok(RawEntryId::Path(PathBuf::from(raw_id.to_string())))
207 }
208
209 fn insert(&self, _key: &dyn EncodableItem, _key_spec: &dyn KeySpecifier) -> Result<()> {
210 Err(CTorKeystoreError::NotSupported { action: "insert" }.into())
211 }
212
213 fn remove(
214 &self,
215 _key_spec: &dyn KeySpecifier,
216 _item_type: &KeystoreItemType,
217 ) -> Result<Option<()>> {
218 Err(CTorKeystoreError::NotSupported { action: "remove" }.into())
219 }
220
221 #[cfg(feature = "onion-service-cli-extra")]
222 fn remove_unchecked(&self, _entry_id: &RawEntryId) -> Result<()> {
223 Err(CTorKeystoreError::NotSupported {
224 action: "remove_unchecked",
225 }
226 .into())
227 }
228
229 fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
230 let all_keys = [
233 (
234 CTorPath::HsIdPublicKey {
235 nickname: self.nickname.clone(),
236 },
237 KeyType::Ed25519PublicKey,
238 ),
239 (
240 CTorPath::HsIdKeypair {
241 nickname: self.nickname.clone(),
242 },
243 KeyType::Ed25519ExpandedKeypair,
244 ),
245 ];
246
247 let valid_rel_paths = all_keys
248 .into_iter()
249 .map(|(ctor_path, key_type)| {
250 let path = rel_path_if_supported!(
251 self,
252 ctor_path,
253 Err(internal!("Failed to build {ctor_path:?} path?!").into()),
254 KeystoreItemType::Key(key_type.clone())
255 );
256
257 Ok((ctor_path, key_type, path))
258 })
259 .collect::<Result<Vec<_>>>()?;
260
261 let keystore_path = self.keystore.keystore_dir.as_path();
262
263 WalkDir::new(keystore_path)
266 .into_iter()
267 .map(|entry| {
268 let entry = entry
269 .map_err(|e| {
270 let msg = e.to_string();
271 FilesystemError::Io {
272 action: FilesystemAction::Read,
273 path: keystore_path.into(),
274 err: e
275 .into_io_error()
276 .unwrap_or_else(|| io::Error::other(msg.clone()))
277 .into(),
278 }
279 })
280 .map_err(CTorKeystoreError::Filesystem)?;
281
282 let path = entry.path();
283
284 if entry.file_type().is_dir() {
286 return Ok(None);
287 }
288
289 let path = path.strip_prefix(keystore_path).map_err(|_| {
290 tor_error::internal!(
292 "found key {} outside of keystore_dir {}?!",
293 path.display_lossy(),
294 keystore_path.display_lossy()
295 )
296 })?;
297
298 if let Some(parent) = path.parent() {
299 self.keystore
302 .keystore_dir
303 .read_directory(parent)
304 .map_err(|e| FilesystemError::FsMistrust {
305 action: FilesystemAction::Read,
306 path: parent.into(),
307 err: e.into(),
308 })
309 .map_err(CTorKeystoreError::Filesystem)?;
310 }
311
312 let maybe_path =
314 valid_rel_paths
315 .iter()
316 .find_map(|(ctor_path, key_type, rel_path)| {
317 (path == rel_path.rel_path_unchecked())
318 .then_some((ctor_path, key_type, rel_path))
319 });
320
321 let res = match maybe_path {
322 Some((ctor_path, key_type, rel_path)) => Ok(KeystoreEntry::new(
323 KeyPath::CTor(ctor_path.clone()),
324 KeystoreItemType::Key(key_type.clone()),
325 self.id(),
326 RawEntryId::Path(rel_path.rel_path_unchecked().to_owned()),
327 )),
328 None => {
329 let raw_id = RawEntryId::Path(path.into());
330 let entry = UnrecognizedEntry::new(raw_id, self.id().clone());
331 Err(UnrecognizedEntryError::new(
332 entry,
333 Arc::new(CTorKeystoreError::MalformedKey {
334 path: path.into(),
335 err: MalformedServiceKeyError::NotAKey.into(),
336 }),
337 ))
338 }
339 };
340
341 Ok(Some(res))
342 })
343 .flatten_ok()
344 .collect()
345 }
346}
347
348macro_rules! parse_ed25519 {
350 ($key:expr, $parse_fn:expr, $tag:expr, $key_len:expr) => {{
351 let expected_len = $tag.len() + $key_len;
352
353 if $key.len() != expected_len {
354 return Err(MalformedServiceKeyError::InvalidKeyLen {
355 len: $key.len(),
356 expected_len,
357 });
358 }
359
360 let (tag, key) = $key.split_at($tag.len());
361
362 if tag != $tag {
363 return Err(MalformedServiceKeyError::InvalidTag {
364 tag: tag.to_vec(),
365 expected_tag: $tag.into(),
366 });
367 }
368
369 ($parse_fn)(key)
370 }};
371}
372
373fn parse_ed25519_public(key: &[u8]) -> StdResult<ed25519::PublicKey, MalformedServiceKeyError> {
375 const PUBKEY_TAG: &[u8] = b"== ed25519v1-public: type0 ==\0\0\0";
377 const PUBKEY_LEN: usize = 32;
379
380 parse_ed25519!(
381 key,
382 |key| ed25519::PublicKey::try_from(key)
383 .map_err(|e| MalformedServiceKeyError::from(Arc::new(e))),
384 PUBKEY_TAG,
385 PUBKEY_LEN
386 )
387}
388
389fn parse_ed25519_keypair(
391 key: &[u8],
392) -> StdResult<ed25519::ExpandedKeypair, MalformedServiceKeyError> {
393 const KEYPAIR_TAG: &[u8] = b"== ed25519v1-secret: type0 ==\0\0\0";
395 const KEYPAIR_LEN: usize = 64;
397
398 parse_ed25519!(
399 key,
400 |key: &[u8]| {
401 let key: [u8; 64] = key
402 .try_into()
403 .map_err(|_| internal!("bad length on expanded ed25519 secret key "))?;
404 ed25519::ExpandedKeypair::from_secret_key_bytes(key)
405 .ok_or(MalformedServiceKeyError::Ed25519Keypair)
406 },
407 KEYPAIR_TAG,
408 KEYPAIR_LEN
409 )
410}
411
412#[cfg(test)]
413mod tests {
414 #![allow(clippy::bool_assert_comparison)]
416 #![allow(clippy::clone_on_copy)]
417 #![allow(clippy::dbg_macro)]
418 #![allow(clippy::mixed_attributes_style)]
419 #![allow(clippy::print_stderr)]
420 #![allow(clippy::print_stdout)]
421 #![allow(clippy::single_char_pattern)]
422 #![allow(clippy::unwrap_used)]
423 #![allow(clippy::unchecked_time_subtraction)]
424 #![allow(clippy::useless_vec)]
425 #![allow(clippy::needless_pass_by_value)]
426 #![allow(clippy::string_slice)] use super::*;
430 use std::fs;
431 use tempfile::{TempDir, tempdir};
432
433 use crate::test_utils::{DummyKey, TestCTorSpecifier, assert_found};
434
435 const PUBKEY: &[u8] = include_bytes!("../../../testdata/tor-service/hs_ed25519_public_key");
436 const PRIVKEY: &[u8] = include_bytes!("../../../testdata/tor-service/hs_ed25519_secret_key");
437
438 #[cfg(unix)]
439 use std::os::unix::fs::PermissionsExt;
440
441 fn init_keystore(id: &str, nickname: &str) -> (CTorServiceKeystore, TempDir) {
442 let keystore_dir = tempdir().unwrap();
443
444 #[cfg(unix)]
445 fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
446
447 let id = KeystoreId::from_str(id).unwrap();
448 let nickname = HsNickname::from_str(nickname).unwrap();
449 let keystore = CTorServiceKeystore::from_path_and_mistrust(
450 &keystore_dir,
451 &Mistrust::default(),
452 id,
453 nickname,
454 )
455 .unwrap();
456
457 const KEYS: &[(&str, &[u8])] = &[
458 ("hs_ed25519_public_key", PUBKEY),
459 ("hs_ed25519_secret_key", PRIVKEY),
460 ];
461
462 for (name, key) in KEYS {
463 fs::write(keystore_dir.path().join(name), key).unwrap();
464 }
465
466 (keystore, keystore_dir)
467 }
468
469 #[test]
470 fn get() {
471 let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
472
473 let unk_nickname = HsNickname::new("acutus-cepa".into()).unwrap();
474 let path = CTorPath::HsIdPublicKey {
475 nickname: unk_nickname.clone(),
476 };
477
478 assert_found!(
480 keystore,
481 &TestCTorSpecifier(path.clone()),
482 &KeyType::Ed25519PublicKey,
483 false
484 );
485
486 let path = CTorPath::HsIdPublicKey {
489 nickname: keystore.nickname.clone(),
490 };
491 assert_found!(
492 keystore,
493 &TestCTorSpecifier(path.clone()),
494 &KeyType::Ed25519PublicKey,
495 true
496 );
497
498 let path = CTorPath::HsIdKeypair {
499 nickname: keystore.nickname.clone(),
500 };
501 assert_found!(
502 keystore,
503 &TestCTorSpecifier(path.clone()),
504 &KeyType::Ed25519ExpandedKeypair,
505 true
506 );
507 }
508
509 #[test]
510 fn unsupported_operation() {
511 let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
512 let path = CTorPath::HsIdPublicKey {
513 nickname: keystore.nickname.clone(),
514 };
515
516 let err = keystore
517 .remove(
518 &TestCTorSpecifier(path.clone()),
519 &KeyType::Ed25519PublicKey.into(),
520 )
521 .unwrap_err();
522
523 assert_eq!(err.to_string(), "Operation not supported: remove");
524
525 let err = keystore
526 .insert(&DummyKey, &TestCTorSpecifier(path.clone()))
527 .unwrap_err();
528
529 assert_eq!(err.to_string(), "Operation not supported: insert");
530 }
531
532 #[test]
533 fn wrong_keytype() {
534 let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
535
536 let path = CTorPath::HsIdPublicKey {
537 nickname: keystore.nickname.clone(),
538 };
539
540 let err = keystore
541 .get(
542 &TestCTorSpecifier(path.clone()),
543 &KeyType::X25519StaticKeypair.into(),
544 )
545 .map(|_| ())
546 .unwrap_err();
547
548 assert_eq!(
549 err.to_string(),
550 "Invalid item type X25519StaticKeypair for key hs_ed25519_public_key"
551 );
552 }
553
554 #[test]
555 fn list() {
556 let (keystore, keystore_dir) = init_keystore("foo", "allium-cepa");
557
558 let _ = fs::File::create(keystore_dir.path().join("unrecognized_key")).unwrap();
560
561 let keys: Vec<_> = keystore.list().unwrap();
562
563 assert_eq!(keys.len(), 3);
565
566 assert!(keys.iter().any(|entry| {
567 if let Ok(e) = entry.as_ref() {
568 return e.key_type() == &KeyType::Ed25519ExpandedKeypair.into();
569 }
570 false
571 }));
572
573 assert!(keys.iter().any(|entry| {
574 if let Ok(e) = entry.as_ref() {
575 return e.key_type() == &KeyType::Ed25519PublicKey.into();
576 }
577 false
578 }));
579
580 assert!(keys.iter().any(|entry| { entry.is_err() }));
581 }
582}