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