1use std::fmt::{self, Debug, Display};
8use std::str::FromStr;
9
10use digest::Digest;
11use itertools::{chain, Itertools};
12use thiserror::Error;
13use tor_basic_utils::{impl_debug_hex, StrExt as _};
14use tor_key_forge::ToEncodableKey;
15use tor_llcrypto::d::Sha3_256;
16use tor_llcrypto::pk::ed25519::{Ed25519PublicKey, Ed25519SigningKey};
17use tor_llcrypto::pk::{curve25519, ed25519, keymanip};
18use tor_llcrypto::util::ct::CtByteArray;
19
20use crate::macros::{define_bytes, define_pk_keypair};
21use crate::time::TimePeriod;
22
23#[allow(deprecated)]
24pub use hs_client_intro_auth::{HsClientIntroAuthKey, HsClientIntroAuthKeypair};
25
26define_bytes! {
27#[derive(Copy, Clone, Eq, PartialEq, Hash)]
39pub struct HsId([u8; 32]);
40}
41
42impl fmt::LowerHex for HsId {
43 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44 write!(f, "HsId(0x")?;
45 for v in self.0.as_ref() {
46 write!(f, "{:02x}", v)?;
47 }
48 write!(f, ")")?;
49 Ok(())
50 }
51}
52
53impl Debug for HsId {
54 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
55 write!(f, "HsId({})", self)
56 }
57}
58
59define_pk_keypair! {
60pub struct HsIdKey(ed25519::PublicKey) /
74 HsIdKeypair(ed25519::ExpandedKeypair);
88}
89
90impl HsIdKey {
91 pub fn id(&self) -> HsId {
95 HsId(self.0.to_bytes().into())
96 }
97}
98impl TryFrom<HsId> for HsIdKey {
99 type Error = signature::Error;
100
101 fn try_from(value: HsId) -> Result<Self, Self::Error> {
102 ed25519::PublicKey::from_bytes(value.0.as_ref()).map(HsIdKey)
103 }
104}
105impl From<HsIdKey> for HsId {
106 fn from(value: HsIdKey) -> Self {
107 value.id()
108 }
109}
110
111impl From<&HsIdKeypair> for HsIdKey {
112 fn from(value: &HsIdKeypair) -> Self {
113 Self(*value.0.public())
114 }
115}
116
117impl From<HsIdKeypair> for HsIdKey {
118 fn from(value: HsIdKeypair) -> Self {
119 Self(*value.0.public())
120 }
121}
122
123const HSID_ONION_VERSION: u8 = 0x03;
125
126pub const HSID_ONION_SUFFIX: &str = ".onion";
128
129impl Display for HsId {
130 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
131 let checksum = self.onion_checksum();
133 let binary = chain!(self.0.as_ref(), &checksum, &[HSID_ONION_VERSION],)
134 .cloned()
135 .collect_vec();
136 let mut b32 = data_encoding::BASE32_NOPAD.encode(&binary);
137 b32.make_ascii_lowercase();
138 write!(f, "{}{}", b32, HSID_ONION_SUFFIX)
139 }
140}
141
142impl safelog::Redactable for HsId {
143 fn display_redacted(&self, f: &mut fmt::Formatter) -> fmt::Result {
147 let unredacted = self.to_string();
148 const DATA: usize = 56;
150 assert_eq!(unredacted.len(), DATA + HSID_ONION_SUFFIX.len());
151
152 write!(f, "???{}", &unredacted[DATA - 3..])
161 }
162}
163
164impl FromStr for HsId {
165 type Err = HsIdParseError;
166 fn from_str(s: &str) -> Result<Self, HsIdParseError> {
167 use HsIdParseError as PE;
168
169 let s = s
170 .strip_suffix_ignore_ascii_case(HSID_ONION_SUFFIX)
171 .ok_or(PE::NotOnionDomain)?;
172
173 if s.contains('.') {
174 return Err(PE::HsIdContainsSubdomain);
175 }
176
177 let mut s = s.to_owned();
183 s.make_ascii_uppercase();
184
185 let binary = data_encoding::BASE32_NOPAD.decode(s.as_bytes())?;
190 let mut binary = tor_bytes::Reader::from_slice(&binary);
191
192 let pubkey: [u8; 32] = binary.extract()?;
193 let checksum: [u8; 2] = binary.extract()?;
194 let version: u8 = binary.extract()?;
195 let tentative = HsId(pubkey.into());
196
197 if version != HSID_ONION_VERSION {
199 return Err(PE::UnsupportedVersion(version));
200 }
201 if checksum != tentative.onion_checksum() {
202 return Err(PE::WrongChecksum);
203 }
204 Ok(tentative)
205 }
206}
207
208#[derive(Error, Clone, Debug)]
210#[non_exhaustive]
211pub enum HsIdParseError {
212 #[error("Domain name does not end in .onion")]
214 NotOnionDomain,
215
216 #[error("Invalid base32 in .onion address")]
220 InvalidBase32(#[from] data_encoding::DecodeError),
221
222 #[error("Invalid encoded binary data in .onion address")]
224 InvalidData(#[from] tor_bytes::Error),
225
226 #[error("Unsupported .onion address version, v{0}")]
228 UnsupportedVersion(u8),
229
230 #[error("Checksum failed, .onion address corrupted")]
232 WrongChecksum,
233
234 #[error("`.onion` address with subdomain passed where not expected")]
236 HsIdContainsSubdomain,
237}
238
239impl HsId {
240 fn onion_checksum(&self) -> [u8; 2] {
242 let mut h = Sha3_256::new();
243 h.update(b".onion checksum");
244 h.update(self.0.as_ref());
245 h.update([HSID_ONION_VERSION]);
246 h.finalize()[..2]
247 .try_into()
248 .expect("slice of fixed size wasn't that size")
249 }
250}
251
252impl HsIdKey {
253 pub fn compute_blinded_key(
255 &self,
256 cur_period: TimePeriod,
257 ) -> Result<(HsBlindIdKey, crate::Subcredential), keymanip::BlindingError> {
258 let secret = b"";
266 let h = self.blinding_factor(secret, cur_period);
267
268 let blinded_key = keymanip::blind_pubkey(&self.0, h)?.into();
269 let subcredential = self.compute_subcredential(&blinded_key, cur_period);
271
272 Ok((blinded_key, subcredential))
273 }
274
275 pub fn compute_subcredential(
277 &self,
278 blinded_key: &HsBlindIdKey,
279 cur_period: TimePeriod,
280 ) -> crate::Subcredential {
281 let subcredential_bytes: [u8; 32] = {
283 let n_hs_cred: [u8; 32] = {
287 let mut h = Sha3_256::new();
288 h.update(b"credential");
289 h.update(self.0.as_bytes());
290 h.finalize().into()
291 };
292 let mut h = Sha3_256::new();
293 h.update(b"subcredential");
294 h.update(n_hs_cred);
295 h.update(blinded_key.as_bytes());
296 h.finalize().into()
297 };
298
299 subcredential_bytes.into()
300 }
301
302 fn blinding_factor(&self, secret: &[u8], cur_period: TimePeriod) -> [u8; 32] {
307 const BLIND_STRING: &[u8] = b"Derive temporary signing key\0";
320 const ED25519_BASEPOINT: &[u8] =
322 b"(15112221349535400772501151409588531511454012693041857206046113283949847762202, \
323 46316835694926478169428394003475163141307993866256225615783033603165251855960)";
324
325 let mut h = Sha3_256::new();
326 h.update(BLIND_STRING);
327 h.update(self.0.as_bytes());
328 h.update(secret);
329 h.update(ED25519_BASEPOINT);
330 h.update(b"key-blind");
331 h.update(cur_period.interval_num.to_be_bytes());
332 h.update((u64::from(cur_period.length.as_minutes())).to_be_bytes());
333
334 h.finalize().into()
335 }
336}
337
338impl HsIdKeypair {
339 pub fn compute_blinded_key(
341 &self,
342 cur_period: TimePeriod,
343 ) -> Result<(HsBlindIdKey, HsBlindIdKeypair, crate::Subcredential), keymanip::BlindingError>
344 {
345 let secret = b"";
349
350 let public_key = HsIdKey(*self.0.public());
351
352 let (blinded_public_key, subcredential) = public_key.compute_blinded_key(cur_period)?;
357
358 let h = public_key.blinding_factor(secret, cur_period);
359
360 let blinded_keypair = keymanip::blind_keypair(&self.0, h)?;
361
362 Ok((blinded_public_key, blinded_keypair.into(), subcredential))
363 }
364}
365
366define_pk_keypair! {
367pub struct HsBlindIdKey(ed25519::PublicKey) / HsBlindIdKeypair(ed25519::ExpandedKeypair);
378}
379
380impl From<HsBlindIdKeypair> for HsBlindIdKey {
381 fn from(kp: HsBlindIdKeypair) -> HsBlindIdKey {
382 HsBlindIdKey(kp.0.into())
383 }
384}
385
386define_bytes! {
387#[derive(Copy, Clone, Eq, PartialEq, Hash)]
392pub struct HsBlindId([u8; 32]);
393}
394impl_debug_hex! { HsBlindId .0 }
395
396impl HsBlindIdKey {
397 pub fn id(&self) -> HsBlindId {
401 HsBlindId(self.0.to_bytes().into())
402 }
403}
404impl TryFrom<HsBlindId> for HsBlindIdKey {
405 type Error = signature::Error;
406
407 fn try_from(value: HsBlindId) -> Result<Self, Self::Error> {
408 ed25519::PublicKey::from_bytes(value.0.as_ref()).map(HsBlindIdKey)
409 }
410}
411
412impl From<&HsBlindIdKeypair> for HsBlindIdKey {
413 fn from(value: &HsBlindIdKeypair) -> Self {
414 HsBlindIdKey(*value.0.public())
415 }
416}
417
418impl From<HsBlindIdKey> for HsBlindId {
419 fn from(value: HsBlindIdKey) -> Self {
420 value.id()
421 }
422}
423impl From<ed25519::Ed25519Identity> for HsBlindId {
424 fn from(value: ed25519::Ed25519Identity) -> Self {
425 Self(CtByteArray::from(<[u8; 32]>::from(value)))
426 }
427}
428
429impl Ed25519SigningKey for HsBlindIdKeypair {
430 fn sign(&self, message: &[u8]) -> ed25519::Signature {
431 self.0.sign(message)
432 }
433}
434
435impl Ed25519PublicKey for HsBlindIdKeypair {
436 fn public_key(&self) -> ed25519::PublicKey {
437 *self.0.public()
438 }
439}
440
441define_pk_keypair! {
442pub struct HsDescSigningKey(ed25519::PublicKey) / HsDescSigningKeypair(ed25519::Keypair);
456}
457
458define_pk_keypair! {
459pub struct HsIntroPtSessionIdKey(ed25519::PublicKey) / HsIntroPtSessionIdKeypair(ed25519::Keypair);
467}
468
469define_pk_keypair! {
470pub struct HsSvcNtorKey(curve25519::PublicKey) / HsSvcNtorSecretKey(curve25519::StaticSecret);
477curve25519_pair as HsSvcNtorKeypair;
478}
479
480mod hs_client_intro_auth {
481 #![allow(deprecated)]
482 use tor_llcrypto::pk::ed25519;
485
486 use crate::macros::define_pk_keypair;
487
488 define_pk_keypair! {
489 #[deprecated(note = "This key type is not used in the protocol implemented today.")]
495 pub struct HsClientIntroAuthKey(ed25519::PublicKey) /
496 #[deprecated(note = "This key type is not used in the protocol implemented today.")]
497 HsClientIntroAuthKeypair(ed25519::Keypair);
498 }
499}
500
501define_pk_keypair! {
502pub struct HsClientDescEncKey(curve25519::PublicKey) / HsClientDescEncSecretKey(curve25519::StaticSecret);
533curve25519_pair as HsClientDescEncKeypair;
534}
535
536impl PartialEq for HsClientDescEncKey {
537 fn eq(&self, other: &Self) -> bool {
538 self.0 == other.0
539 }
540}
541
542impl Eq for HsClientDescEncKey {}
543
544impl Display for HsClientDescEncKey {
545 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
546 let x25519_pk = data_encoding::BASE32_NOPAD.encode(&self.0.to_bytes());
547 write!(f, "descriptor:x25519:{}", x25519_pk)
548 }
549}
550
551impl FromStr for HsClientDescEncKey {
552 type Err = HsClientDescEncKeyParseError;
553
554 fn from_str(key: &str) -> Result<Self, HsClientDescEncKeyParseError> {
555 let (auth_type, key_type, encoded_key) = key
556 .split(':')
557 .collect_tuple()
558 .ok_or(HsClientDescEncKeyParseError::InvalidFormat)?;
559
560 if auth_type != "descriptor" {
561 return Err(HsClientDescEncKeyParseError::InvalidAuthType(
562 auth_type.into(),
563 ));
564 }
565
566 if key_type != "x25519" {
567 return Err(HsClientDescEncKeyParseError::InvalidKeyType(
568 key_type.into(),
569 ));
570 }
571
572 let encoded_key = encoded_key.to_uppercase();
578 let x25519_pk = data_encoding::BASE32_NOPAD.decode(encoded_key.as_bytes())?;
579 let x25519_pk: [u8; 32] = x25519_pk
580 .try_into()
581 .map_err(|_| HsClientDescEncKeyParseError::InvalidKeyMaterial)?;
582
583 Ok(Self(curve25519::PublicKey::from(x25519_pk)))
584 }
585}
586
587#[derive(Error, Clone, Debug, PartialEq)]
589#[non_exhaustive]
590pub enum HsClientDescEncKeyParseError {
591 #[error("Invalid auth type {0}")]
593 InvalidAuthType(String),
594
595 #[error("Invalid key type {0}")]
597 InvalidKeyType(String),
598
599 #[error("Invalid key format")]
601 InvalidFormat,
602
603 #[error("Invalid key material")]
605 InvalidKeyMaterial,
606
607 #[error("Invalid base32 in client key")]
609 InvalidBase32(#[from] data_encoding::DecodeError),
610}
611
612define_pk_keypair! {
613pub struct HsSvcDescEncKey(curve25519::PublicKey) / HsSvcDescEncSecretKey(curve25519::StaticSecret);
618}
619
620impl From<&HsClientDescEncSecretKey> for HsClientDescEncKey {
621 fn from(ks: &HsClientDescEncSecretKey) -> Self {
622 Self(curve25519::PublicKey::from(&ks.0))
623 }
624}
625
626impl From<&HsClientDescEncKeypair> for HsClientDescEncKey {
627 fn from(ks: &HsClientDescEncKeypair) -> Self {
628 Self(**ks.public())
629 }
630}
631
632#[allow(clippy::exhaustive_structs)]
635#[derive(Debug)]
636pub struct HsSvcDescEncKeypair {
637 pub public: HsSvcDescEncKey,
639 pub secret: HsSvcDescEncSecretKey,
641}
642
643impl ToEncodableKey for HsClientDescEncKeypair {
650 type Key = curve25519::StaticKeypair;
651 type KeyPair = HsClientDescEncKeypair;
652
653 fn to_encodable_key(self) -> Self::Key {
654 self.into()
655 }
656
657 fn from_encodable_key(key: Self::Key) -> Self {
658 HsClientDescEncKeypair::new(key.public.into(), key.secret.into())
659 }
660}
661
662impl ToEncodableKey for HsBlindIdKeypair {
663 type Key = ed25519::ExpandedKeypair;
664 type KeyPair = HsBlindIdKeypair;
665
666 fn to_encodable_key(self) -> Self::Key {
667 self.into()
668 }
669
670 fn from_encodable_key(key: Self::Key) -> Self {
671 HsBlindIdKeypair::from(key)
672 }
673}
674
675impl ToEncodableKey for HsBlindIdKey {
676 type Key = ed25519::PublicKey;
677 type KeyPair = HsBlindIdKeypair;
678
679 fn to_encodable_key(self) -> Self::Key {
680 self.into()
681 }
682
683 fn from_encodable_key(key: Self::Key) -> Self {
684 HsBlindIdKey::from(key)
685 }
686}
687
688impl ToEncodableKey for HsIdKeypair {
689 type Key = ed25519::ExpandedKeypair;
690 type KeyPair = HsIdKeypair;
691
692 fn to_encodable_key(self) -> Self::Key {
693 self.into()
694 }
695
696 fn from_encodable_key(key: Self::Key) -> Self {
697 HsIdKeypair::from(key)
698 }
699}
700
701impl ToEncodableKey for HsIdKey {
702 type Key = ed25519::PublicKey;
703 type KeyPair = HsIdKeypair;
704
705 fn to_encodable_key(self) -> Self::Key {
706 self.into()
707 }
708
709 fn from_encodable_key(key: Self::Key) -> Self {
710 HsIdKey::from(key)
711 }
712}
713
714impl ToEncodableKey for HsDescSigningKeypair {
715 type Key = ed25519::Keypair;
716 type KeyPair = HsDescSigningKeypair;
717
718 fn to_encodable_key(self) -> Self::Key {
719 self.into()
720 }
721
722 fn from_encodable_key(key: Self::Key) -> Self {
723 HsDescSigningKeypair::from(key)
724 }
725}
726
727impl ToEncodableKey for HsIntroPtSessionIdKeypair {
728 type Key = ed25519::Keypair;
729 type KeyPair = HsIntroPtSessionIdKeypair;
730
731 fn to_encodable_key(self) -> Self::Key {
732 self.into()
733 }
734
735 fn from_encodable_key(key: Self::Key) -> Self {
736 key.into()
737 }
738}
739
740impl ToEncodableKey for HsSvcNtorKeypair {
741 type Key = curve25519::StaticKeypair;
742 type KeyPair = HsSvcNtorKeypair;
743
744 fn to_encodable_key(self) -> Self::Key {
745 self.into()
746 }
747
748 fn from_encodable_key(key: Self::Key) -> Self {
749 key.into()
750 }
751}
752
753#[cfg(test)]
754mod test {
755 #![allow(clippy::bool_assert_comparison)]
757 #![allow(clippy::clone_on_copy)]
758 #![allow(clippy::dbg_macro)]
759 #![allow(clippy::mixed_attributes_style)]
760 #![allow(clippy::print_stderr)]
761 #![allow(clippy::print_stdout)]
762 #![allow(clippy::single_char_pattern)]
763 #![allow(clippy::unwrap_used)]
764 #![allow(clippy::unchecked_duration_subtraction)]
765 #![allow(clippy::useless_vec)]
766 #![allow(clippy::needless_pass_by_value)]
767 use hex_literal::hex;
770 use itertools::izip;
771 use safelog::Redactable;
772 use std::time::{Duration, SystemTime};
773 use tor_basic_utils::test_rng::testing_rng;
774
775 use super::*;
776
777 #[test]
778 fn hsid_strings() {
779 use HsIdParseError as PE;
780
781 let hex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
783 let b32 = "25njqamcweflpvkl73j4szahhihoc4xt3ktcgjnpaingr5yhkenl5sid";
784
785 let hsid: [u8; 32] = hex::decode(hex).unwrap().try_into().unwrap();
786 let hsid = HsId::from(hsid);
787 let onion = format!("{}.onion", b32);
788
789 assert_eq!(onion.parse::<HsId>().unwrap(), hsid);
790 assert_eq!(hsid.to_string(), onion);
791
792 let weird_case: String = izip!(onion.chars(), [false, true].iter().cloned().cycle(),)
793 .map(|(c, swap)| if swap { c.to_ascii_uppercase() } else { c })
794 .collect();
795 dbg!(&weird_case);
796 assert_eq!(weird_case.parse::<HsId>().unwrap(), hsid);
797
798 macro_rules! chk_err { { $s:expr, $($pat:tt)* } => {
799 let e = $s.parse::<HsId>();
800 assert!(matches!(e, Err($($pat)*)), "{:?}", &e);
801 } }
802 let edited = |i, c| {
803 let mut s = b32.to_owned().into_bytes();
804 s[i] = c;
805 format!("{}.onion", String::from_utf8(s).unwrap())
806 };
807
808 chk_err!("wrong", PE::NotOnionDomain);
809 chk_err!("@.onion", PE::InvalidBase32(..));
810 chk_err!("aaaaaaaa.onion", PE::InvalidData(..));
811 chk_err!(edited(55, b'E'), PE::UnsupportedVersion(4));
812 chk_err!(edited(53, b'X'), PE::WrongChecksum);
813 chk_err!(&format!("www.{}", &onion), PE::HsIdContainsSubdomain);
814
815 assert_eq!(format!("{:x}", &hsid), format!("HsId(0x{})", hex));
816 assert_eq!(format!("{:?}", &hsid), format!("HsId({})", onion));
817
818 assert_eq!(format!("{}", hsid.redacted()), "???sid.onion");
819 }
820
821 #[test]
822 fn key_blinding_blackbox() {
823 let mut rng = testing_rng();
824 let offset = Duration::new(12 * 60 * 60, 0);
825 let when = TimePeriod::new(Duration::from_secs(3600), SystemTime::now(), offset).unwrap();
826 let keypair = ed25519::Keypair::generate(&mut rng);
827 let id_pub = HsIdKey::from(keypair.verifying_key());
828 let id_keypair = HsIdKeypair::from(ed25519::ExpandedKeypair::from(&keypair));
829
830 let (blinded_pub, subcred1) = id_pub.compute_blinded_key(when).unwrap();
831 let (blinded_pub2, blinded_keypair, subcred2) =
832 id_keypair.compute_blinded_key(when).unwrap();
833
834 assert_eq!(subcred1.as_ref(), subcred2.as_ref());
835 assert_eq!(blinded_pub.0.to_bytes(), blinded_pub2.0.to_bytes());
836 assert_eq!(blinded_pub.id(), blinded_pub2.id());
837
838 let message = b"Here is a terribly important string to authenticate.";
839 let other_message = b"Hey, that is not what I signed!";
840 let sign = blinded_keypair.sign(message);
841
842 assert!(blinded_pub.as_ref().verify(message, &sign).is_ok());
843 assert!(blinded_pub.as_ref().verify(other_message, &sign).is_err());
844 }
845
846 #[test]
847 fn key_blinding_testvec() {
848 let id = HsId::from(hex!(
850 "833990B085C1A688C1D4C8B1F6B56AFAF5A2ECA674449E1D704F83765CCB7BC6"
851 ));
852 let id_pubkey = HsIdKey::try_from(id).unwrap();
853 let id_seckey = HsIdKeypair::from(
854 ed25519::ExpandedKeypair::from_secret_key_bytes(hex!(
855 "D8C7FF0E31295B66540D789AF3E3DF992038A9592EEA01D8B7CBA06D6E66D159
856 4D6167696320576F7264733A20737065697373636F62616C742062697669756D"
857 ))
858 .unwrap(),
859 );
860 let time_period = TimePeriod::new(
861 humantime::parse_duration("1 day").unwrap(),
862 humantime::parse_rfc3339("1973-05-20T01:50:33Z").unwrap(),
863 humantime::parse_duration("12 hours").unwrap(),
864 )
865 .unwrap();
866 assert_eq!(time_period.interval_num, 1234);
867
868 let h = id_pubkey.blinding_factor(b"", time_period);
869 assert_eq!(
870 h,
871 hex!("379E50DB31FEE6775ABD0AF6FB7C371E060308F4F847DB09FE4CFE13AF602287")
872 );
873
874 let (blinded_pub1, subcred1) = id_pubkey.compute_blinded_key(time_period).unwrap();
875 assert_eq!(
876 blinded_pub1.0.to_bytes(),
877 hex!("3A50BF210E8F9EE955AE0014F7A6917FB65EBF098A86305ABB508D1A7291B6D5")
878 );
879 assert_eq!(
880 subcred1.as_ref(),
881 &hex!("635D55907816E8D76398A675A50B1C2F3E36B42A5CA77BA3A0441285161AE07D")
882 );
883
884 let (blinded_pub2, blinded_sec, subcred2) =
885 id_seckey.compute_blinded_key(time_period).unwrap();
886 assert_eq!(blinded_pub1.0.to_bytes(), blinded_pub2.0.to_bytes());
887 assert_eq!(subcred1.as_ref(), subcred2.as_ref());
888 assert_eq!(
889 blinded_sec.0.to_secret_key_bytes(),
890 hex!(
891 "A958DC83AC885F6814C67035DE817A2C604D5D2F715282079448F789B656350B
892 4540FE1F80AA3F7E91306B7BF7A8E367293352B14A29FDCC8C19F3558075524B"
893 )
894 );
895 }
896
897 #[test]
898 fn parse_client_desc_enc_key() {
899 use HsClientDescEncKeyParseError::*;
900
901 const VALID_KEY_BASE32: &str = "dz4q5xqlb4ldnbs72iarrml4ephk3du4i7o2cgiva5lwr6wkquja";
903
904 const WRONG_FORMAT: &[&str] = &["a:b:c:d:e", "descriptor:", "descriptor:x25519", ""];
906
907 for key in WRONG_FORMAT {
908 let err = HsClientDescEncKey::from_str(key).unwrap_err();
909
910 assert_eq!(err, InvalidFormat);
911 }
912
913 let err =
914 HsClientDescEncKey::from_str(&format!("foo:descriptor:x25519:{VALID_KEY_BASE32}"))
915 .unwrap_err();
916
917 assert_eq!(err, InvalidFormat);
918
919 let err = HsClientDescEncKey::from_str("bar:x25519:aa==").unwrap_err();
921 assert_eq!(err, InvalidAuthType("bar".into()));
922
923 let err = HsClientDescEncKey::from_str("descriptor:not-x25519:aa==").unwrap_err();
925 assert_eq!(err, InvalidKeyType("not-x25519".into()));
926
927 let err = HsClientDescEncKey::from_str("descriptor:x25519:aa==").unwrap_err();
929 assert!(matches!(err, InvalidBase32(_)));
930
931 let _key =
933 HsClientDescEncKey::from_str(&format!("descriptor:x25519:{VALID_KEY_BASE32}")).unwrap();
934
935 let desc_enc_key = HsClientDescEncKey::from(curve25519::PublicKey::from(
937 &curve25519::StaticSecret::random_from_rng(testing_rng()),
938 ));
939
940 assert_eq!(
941 desc_enc_key,
942 HsClientDescEncKey::from_str(&desc_enc_key.to_string()).unwrap()
943 );
944 }
945}