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