1use std::fmt::{self, Debug, Display};
8use std::str::FromStr;
9
10use digest::Digest;
11use itertools::{chain, Itertools};
12use safelog::DisplayRedacted;
13use thiserror::Error;
14use tor_basic_utils::{impl_debug_hex, StrExt as _};
15use tor_key_forge::ToEncodableKey;
16use tor_llcrypto::d::Sha3_256;
17use tor_llcrypto::pk::ed25519::{Ed25519PublicKey, Ed25519SigningKey};
18use tor_llcrypto::pk::{curve25519, ed25519, keymanip};
19use tor_llcrypto::util::ct::CtByteArray;
20
21use crate::macros::{define_bytes, define_pk_keypair};
22use crate::time::TimePeriod;
23
24#[allow(deprecated)]
25pub use hs_client_intro_auth::{HsClientIntroAuthKey, HsClientIntroAuthKeypair};
26
27define_bytes! {
28#[derive(Copy, Clone, Eq, PartialEq, Hash)]
40pub struct HsId([u8; 32]);
41}
42
43define_pk_keypair! {
44pub struct HsIdKey(ed25519::PublicKey) /
58 HsIdKeypair(ed25519::ExpandedKeypair);
72}
73
74impl HsIdKey {
75 pub fn id(&self) -> HsId {
79 HsId(self.0.to_bytes().into())
80 }
81}
82impl TryFrom<HsId> for HsIdKey {
83 type Error = signature::Error;
84
85 fn try_from(value: HsId) -> Result<Self, Self::Error> {
86 ed25519::PublicKey::from_bytes(value.0.as_ref()).map(HsIdKey)
87 }
88}
89impl From<HsIdKey> for HsId {
90 fn from(value: HsIdKey) -> Self {
91 value.id()
92 }
93}
94
95impl From<&HsIdKeypair> for HsIdKey {
96 fn from(value: &HsIdKeypair) -> Self {
97 Self(*value.0.public())
98 }
99}
100
101impl From<HsIdKeypair> for HsIdKey {
102 fn from(value: HsIdKeypair) -> Self {
103 Self(*value.0.public())
104 }
105}
106
107const HSID_ONION_VERSION: u8 = 0x03;
109
110pub const HSID_ONION_SUFFIX: &str = ".onion";
112
113impl safelog::DisplayRedacted for HsId {
114 fn fmt_unredacted(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
115 let checksum = self.onion_checksum();
117 let binary = chain!(self.0.as_ref(), &checksum, &[HSID_ONION_VERSION],)
118 .cloned()
119 .collect_vec();
120 let mut b32 = data_encoding::BASE32_NOPAD.encode(&binary);
121 b32.make_ascii_lowercase();
122 write!(f, "{}{}", b32, HSID_ONION_SUFFIX)
123 }
124
125 fn fmt_redacted(&self, f: &mut fmt::Formatter) -> fmt::Result {
129 let unredacted = self.display_unredacted().to_string();
130 const DATA: usize = 56;
132 assert_eq!(unredacted.len(), DATA + HSID_ONION_SUFFIX.len());
133
134 write!(f, "???{}", &unredacted[DATA - 3..])
143 }
144}
145
146impl safelog::DebugRedacted for HsId {
147 fn fmt_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 write!(f, "HsId({})", self.display_redacted())
149 }
150
151 fn fmt_unredacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 write!(f, "HsId({})", self.display_unredacted())
153 }
154}
155
156safelog::derive_redacted_debug!(HsId);
157
158impl FromStr for HsId {
159 type Err = HsIdParseError;
160 fn from_str(s: &str) -> Result<Self, HsIdParseError> {
161 use HsIdParseError as PE;
162
163 let s = s
164 .strip_suffix_ignore_ascii_case(HSID_ONION_SUFFIX)
165 .ok_or(PE::NotOnionDomain)?;
166
167 if s.contains('.') {
168 return Err(PE::HsIdContainsSubdomain);
169 }
170
171 let mut s = s.to_owned();
177 s.make_ascii_uppercase();
178
179 let binary = data_encoding::BASE32_NOPAD.decode(s.as_bytes())?;
184 let mut binary = tor_bytes::Reader::from_slice(&binary);
185
186 let pubkey: [u8; 32] = binary.extract()?;
187 let checksum: [u8; 2] = binary.extract()?;
188 let version: u8 = binary.extract()?;
189 let tentative = HsId(pubkey.into());
190
191 if version != HSID_ONION_VERSION {
193 return Err(PE::UnsupportedVersion(version));
194 }
195 if checksum != tentative.onion_checksum() {
196 return Err(PE::WrongChecksum);
197 }
198 Ok(tentative)
199 }
200}
201
202#[derive(Error, Clone, Debug)]
204#[non_exhaustive]
205pub enum HsIdParseError {
206 #[error("Domain name does not end in .onion")]
208 NotOnionDomain,
209
210 #[error("Invalid base32 in .onion address")]
214 InvalidBase32(#[from] data_encoding::DecodeError),
215
216 #[error("Invalid encoded binary data in .onion address")]
218 InvalidData(#[from] tor_bytes::Error),
219
220 #[error("Unsupported .onion address version, v{0}")]
222 UnsupportedVersion(u8),
223
224 #[error("Checksum failed, .onion address corrupted")]
226 WrongChecksum,
227
228 #[error("`.onion` address with subdomain passed where not expected")]
230 HsIdContainsSubdomain,
231}
232
233impl HsId {
234 fn onion_checksum(&self) -> [u8; 2] {
236 let mut h = Sha3_256::new();
237 h.update(b".onion checksum");
238 h.update(self.0.as_ref());
239 h.update([HSID_ONION_VERSION]);
240 h.finalize()[..2]
241 .try_into()
242 .expect("slice of fixed size wasn't that size")
243 }
244}
245
246impl HsIdKey {
247 pub fn compute_blinded_key(
249 &self,
250 cur_period: TimePeriod,
251 ) -> Result<(HsBlindIdKey, crate::Subcredential), keymanip::BlindingError> {
252 let secret = b"";
260 let h = self.blinding_factor(secret, cur_period);
261
262 let blinded_key = keymanip::blind_pubkey(&self.0, h)?.into();
263 let subcredential = self.compute_subcredential(&blinded_key, cur_period);
265
266 Ok((blinded_key, subcredential))
267 }
268
269 pub fn compute_subcredential(
271 &self,
272 blinded_key: &HsBlindIdKey,
273 cur_period: TimePeriod,
274 ) -> crate::Subcredential {
275 let subcredential_bytes: [u8; 32] = {
277 let n_hs_cred: [u8; 32] = {
281 let mut h = Sha3_256::new();
282 h.update(b"credential");
283 h.update(self.0.as_bytes());
284 h.finalize().into()
285 };
286 let mut h = Sha3_256::new();
287 h.update(b"subcredential");
288 h.update(n_hs_cred);
289 h.update(blinded_key.as_bytes());
290 h.finalize().into()
291 };
292
293 subcredential_bytes.into()
294 }
295
296 fn blinding_factor(&self, secret: &[u8], cur_period: TimePeriod) -> [u8; 32] {
301 const BLIND_STRING: &[u8] = b"Derive temporary signing key\0";
314 const ED25519_BASEPOINT: &[u8] =
316 b"(15112221349535400772501151409588531511454012693041857206046113283949847762202, \
317 46316835694926478169428394003475163141307993866256225615783033603165251855960)";
318
319 let mut h = Sha3_256::new();
320 h.update(BLIND_STRING);
321 h.update(self.0.as_bytes());
322 h.update(secret);
323 h.update(ED25519_BASEPOINT);
324 h.update(b"key-blind");
325 h.update(cur_period.interval_num.to_be_bytes());
326 h.update((u64::from(cur_period.length.as_minutes())).to_be_bytes());
327
328 h.finalize().into()
329 }
330}
331
332impl HsIdKeypair {
333 pub fn compute_blinded_key(
335 &self,
336 cur_period: TimePeriod,
337 ) -> Result<(HsBlindIdKey, HsBlindIdKeypair, crate::Subcredential), keymanip::BlindingError>
338 {
339 let secret = b"";
343
344 let public_key = HsIdKey(*self.0.public());
345
346 let (blinded_public_key, subcredential) = public_key.compute_blinded_key(cur_period)?;
351
352 let h = public_key.blinding_factor(secret, cur_period);
353
354 let blinded_keypair = keymanip::blind_keypair(&self.0, h)?;
355
356 Ok((blinded_public_key, blinded_keypair.into(), subcredential))
357 }
358}
359
360define_pk_keypair! {
361pub struct HsBlindIdKey(ed25519::PublicKey) / HsBlindIdKeypair(ed25519::ExpandedKeypair);
372}
373
374impl From<HsBlindIdKeypair> for HsBlindIdKey {
375 fn from(kp: HsBlindIdKeypair) -> HsBlindIdKey {
376 HsBlindIdKey(kp.0.into())
377 }
378}
379
380define_bytes! {
381#[derive(Copy, Clone, Eq, PartialEq, Hash)]
386pub struct HsBlindId([u8; 32]);
387}
388impl_debug_hex! { HsBlindId .0 }
389
390impl HsBlindIdKey {
391 pub fn id(&self) -> HsBlindId {
395 HsBlindId(self.0.to_bytes().into())
396 }
397}
398impl TryFrom<HsBlindId> for HsBlindIdKey {
399 type Error = signature::Error;
400
401 fn try_from(value: HsBlindId) -> Result<Self, Self::Error> {
402 ed25519::PublicKey::from_bytes(value.0.as_ref()).map(HsBlindIdKey)
403 }
404}
405
406impl From<&HsBlindIdKeypair> for HsBlindIdKey {
407 fn from(value: &HsBlindIdKeypair) -> Self {
408 HsBlindIdKey(*value.0.public())
409 }
410}
411
412impl From<HsBlindIdKey> for HsBlindId {
413 fn from(value: HsBlindIdKey) -> Self {
414 value.id()
415 }
416}
417impl From<ed25519::Ed25519Identity> for HsBlindId {
418 fn from(value: ed25519::Ed25519Identity) -> Self {
419 Self(CtByteArray::from(<[u8; 32]>::from(value)))
420 }
421}
422
423impl Ed25519SigningKey for HsBlindIdKeypair {
424 fn sign(&self, message: &[u8]) -> ed25519::Signature {
425 self.0.sign(message)
426 }
427}
428
429impl Ed25519PublicKey for HsBlindIdKeypair {
430 fn public_key(&self) -> ed25519::PublicKey {
431 *self.0.public()
432 }
433}
434
435define_pk_keypair! {
436pub struct HsDescSigningKey(ed25519::PublicKey) / HsDescSigningKeypair(ed25519::Keypair);
450}
451
452define_pk_keypair! {
453pub struct HsIntroPtSessionIdKey(ed25519::PublicKey) / HsIntroPtSessionIdKeypair(ed25519::Keypair);
461}
462
463define_pk_keypair! {
464pub struct HsSvcNtorKey(curve25519::PublicKey) / HsSvcNtorSecretKey(curve25519::StaticSecret);
471curve25519_pair as HsSvcNtorKeypair;
472}
473
474mod hs_client_intro_auth {
475 #![allow(deprecated)]
476 use tor_llcrypto::pk::ed25519;
479
480 use crate::macros::define_pk_keypair;
481
482 define_pk_keypair! {
483 #[deprecated(note = "This key type is not used in the protocol implemented today.")]
489 pub struct HsClientIntroAuthKey(ed25519::PublicKey) /
490 #[deprecated(note = "This key type is not used in the protocol implemented today.")]
491 HsClientIntroAuthKeypair(ed25519::Keypair);
492 }
493}
494
495define_pk_keypair! {
496pub struct HsClientDescEncKey(curve25519::PublicKey) / HsClientDescEncSecretKey(curve25519::StaticSecret);
527curve25519_pair as HsClientDescEncKeypair;
528}
529
530impl PartialEq for HsClientDescEncKey {
531 fn eq(&self, other: &Self) -> bool {
532 self.0 == other.0
533 }
534}
535
536impl Eq for HsClientDescEncKey {}
537
538impl Display for HsClientDescEncKey {
539 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540 let x25519_pk = data_encoding::BASE32_NOPAD.encode(&self.0.to_bytes());
541 write!(f, "descriptor:x25519:{}", x25519_pk)
542 }
543}
544
545impl FromStr for HsClientDescEncKey {
546 type Err = HsClientDescEncKeyParseError;
547
548 fn from_str(key: &str) -> Result<Self, HsClientDescEncKeyParseError> {
549 let (auth_type, key_type, encoded_key) = key
550 .split(':')
551 .collect_tuple()
552 .ok_or(HsClientDescEncKeyParseError::InvalidFormat)?;
553
554 if auth_type != "descriptor" {
555 return Err(HsClientDescEncKeyParseError::InvalidAuthType(
556 auth_type.into(),
557 ));
558 }
559
560 if key_type != "x25519" {
561 return Err(HsClientDescEncKeyParseError::InvalidKeyType(
562 key_type.into(),
563 ));
564 }
565
566 let encoded_key = encoded_key.to_uppercase();
572 let x25519_pk = data_encoding::BASE32_NOPAD.decode(encoded_key.as_bytes())?;
573 let x25519_pk: [u8; 32] = x25519_pk
574 .try_into()
575 .map_err(|_| HsClientDescEncKeyParseError::InvalidKeyMaterial)?;
576
577 Ok(Self(curve25519::PublicKey::from(x25519_pk)))
578 }
579}
580
581#[derive(Error, Clone, Debug, PartialEq)]
583#[non_exhaustive]
584pub enum HsClientDescEncKeyParseError {
585 #[error("Invalid auth type {0}")]
587 InvalidAuthType(String),
588
589 #[error("Invalid key type {0}")]
591 InvalidKeyType(String),
592
593 #[error("Invalid key format")]
595 InvalidFormat,
596
597 #[error("Invalid key material")]
599 InvalidKeyMaterial,
600
601 #[error("Invalid base32 in client key")]
603 InvalidBase32(#[from] data_encoding::DecodeError),
604}
605
606define_pk_keypair! {
607pub struct HsSvcDescEncKey(curve25519::PublicKey) / HsSvcDescEncSecretKey(curve25519::StaticSecret);
612}
613
614impl From<&HsClientDescEncSecretKey> for HsClientDescEncKey {
615 fn from(ks: &HsClientDescEncSecretKey) -> Self {
616 Self(curve25519::PublicKey::from(&ks.0))
617 }
618}
619
620impl From<&HsClientDescEncKeypair> for HsClientDescEncKey {
621 fn from(ks: &HsClientDescEncKeypair) -> Self {
622 Self(**ks.public())
623 }
624}
625
626#[allow(clippy::exhaustive_structs)]
629#[derive(Debug)]
630pub struct HsSvcDescEncKeypair {
631 pub public: HsSvcDescEncKey,
633 pub secret: HsSvcDescEncSecretKey,
635}
636
637impl ToEncodableKey for HsClientDescEncKeypair {
644 type Key = curve25519::StaticKeypair;
645 type KeyPair = HsClientDescEncKeypair;
646
647 fn to_encodable_key(self) -> Self::Key {
648 self.into()
649 }
650
651 fn from_encodable_key(key: Self::Key) -> Self {
652 HsClientDescEncKeypair::new(key.public.into(), key.secret.into())
653 }
654}
655
656impl ToEncodableKey for HsBlindIdKeypair {
657 type Key = ed25519::ExpandedKeypair;
658 type KeyPair = HsBlindIdKeypair;
659
660 fn to_encodable_key(self) -> Self::Key {
661 self.into()
662 }
663
664 fn from_encodable_key(key: Self::Key) -> Self {
665 HsBlindIdKeypair::from(key)
666 }
667}
668
669impl ToEncodableKey for HsBlindIdKey {
670 type Key = ed25519::PublicKey;
671 type KeyPair = HsBlindIdKeypair;
672
673 fn to_encodable_key(self) -> Self::Key {
674 self.into()
675 }
676
677 fn from_encodable_key(key: Self::Key) -> Self {
678 HsBlindIdKey::from(key)
679 }
680}
681
682impl ToEncodableKey for HsIdKeypair {
683 type Key = ed25519::ExpandedKeypair;
684 type KeyPair = HsIdKeypair;
685
686 fn to_encodable_key(self) -> Self::Key {
687 self.into()
688 }
689
690 fn from_encodable_key(key: Self::Key) -> Self {
691 HsIdKeypair::from(key)
692 }
693}
694
695impl ToEncodableKey for HsIdKey {
696 type Key = ed25519::PublicKey;
697 type KeyPair = HsIdKeypair;
698
699 fn to_encodable_key(self) -> Self::Key {
700 self.into()
701 }
702
703 fn from_encodable_key(key: Self::Key) -> Self {
704 HsIdKey::from(key)
705 }
706}
707
708impl ToEncodableKey for HsDescSigningKeypair {
709 type Key = ed25519::Keypair;
710 type KeyPair = HsDescSigningKeypair;
711
712 fn to_encodable_key(self) -> Self::Key {
713 self.into()
714 }
715
716 fn from_encodable_key(key: Self::Key) -> Self {
717 HsDescSigningKeypair::from(key)
718 }
719}
720
721impl ToEncodableKey for HsIntroPtSessionIdKeypair {
722 type Key = ed25519::Keypair;
723 type KeyPair = HsIntroPtSessionIdKeypair;
724
725 fn to_encodable_key(self) -> Self::Key {
726 self.into()
727 }
728
729 fn from_encodable_key(key: Self::Key) -> Self {
730 key.into()
731 }
732}
733
734impl ToEncodableKey for HsSvcNtorKeypair {
735 type Key = curve25519::StaticKeypair;
736 type KeyPair = HsSvcNtorKeypair;
737
738 fn to_encodable_key(self) -> Self::Key {
739 self.into()
740 }
741
742 fn from_encodable_key(key: Self::Key) -> Self {
743 key.into()
744 }
745}
746
747#[cfg(test)]
748mod test {
749 #![allow(clippy::bool_assert_comparison)]
751 #![allow(clippy::clone_on_copy)]
752 #![allow(clippy::dbg_macro)]
753 #![allow(clippy::mixed_attributes_style)]
754 #![allow(clippy::print_stderr)]
755 #![allow(clippy::print_stdout)]
756 #![allow(clippy::single_char_pattern)]
757 #![allow(clippy::unwrap_used)]
758 #![allow(clippy::unchecked_duration_subtraction)]
759 #![allow(clippy::useless_vec)]
760 #![allow(clippy::needless_pass_by_value)]
761 use hex_literal::hex;
764 use itertools::izip;
765 use std::time::{Duration, SystemTime};
766 use tor_basic_utils::test_rng::testing_rng;
767
768 use super::*;
769
770 #[test]
771 fn hsid_strings() {
772 use HsIdParseError as PE;
773
774 let hex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
776 let b32 = "25njqamcweflpvkl73j4szahhihoc4xt3ktcgjnpaingr5yhkenl5sid";
777
778 let hsid: [u8; 32] = hex::decode(hex).unwrap().try_into().unwrap();
779 let hsid = HsId::from(hsid);
780 let onion = format!("{}.onion", b32);
781
782 assert_eq!(onion.parse::<HsId>().unwrap(), hsid);
783 assert_eq!(hsid.display_unredacted().to_string(), onion);
784
785 let weird_case: String = izip!(onion.chars(), [false, true].iter().cloned().cycle(),)
786 .map(|(c, swap)| if swap { c.to_ascii_uppercase() } else { c })
787 .collect();
788 dbg!(&weird_case);
789 assert_eq!(weird_case.parse::<HsId>().unwrap(), hsid);
790
791 macro_rules! chk_err { { $s:expr, $($pat:tt)* } => {
792 let e = $s.parse::<HsId>();
793 assert!(matches!(e, Err($($pat)*)), "{:?}", &e);
794 } }
795 let edited = |i, c| {
796 let mut s = b32.to_owned().into_bytes();
797 s[i] = c;
798 format!("{}.onion", String::from_utf8(s).unwrap())
799 };
800
801 chk_err!("wrong", PE::NotOnionDomain);
802 chk_err!("@.onion", PE::InvalidBase32(..));
803 chk_err!("aaaaaaaa.onion", PE::InvalidData(..));
804 chk_err!(edited(55, b'E'), PE::UnsupportedVersion(4));
805 chk_err!(edited(53, b'X'), PE::WrongChecksum);
806 chk_err!(&format!("www.{}", &onion), PE::HsIdContainsSubdomain);
807
808 safelog::with_safe_logging_suppressed(|| {
809 assert_eq!(format!("{:?}", &hsid), format!("HsId({})", onion));
810 });
811
812 assert_eq!(format!("{}", hsid.display_redacted()), "???sid.onion");
813 }
814
815 #[test]
816 fn key_blinding_blackbox() {
817 let mut rng = testing_rng();
818 let offset = Duration::new(12 * 60 * 60, 0);
819 let when = TimePeriod::new(Duration::from_secs(3600), SystemTime::now(), offset).unwrap();
820 let keypair = ed25519::Keypair::generate(&mut rng);
821 let id_pub = HsIdKey::from(keypair.verifying_key());
822 let id_keypair = HsIdKeypair::from(ed25519::ExpandedKeypair::from(&keypair));
823
824 let (blinded_pub, subcred1) = id_pub.compute_blinded_key(when).unwrap();
825 let (blinded_pub2, blinded_keypair, subcred2) =
826 id_keypair.compute_blinded_key(when).unwrap();
827
828 assert_eq!(subcred1.as_ref(), subcred2.as_ref());
829 assert_eq!(blinded_pub.0.to_bytes(), blinded_pub2.0.to_bytes());
830 assert_eq!(blinded_pub.id(), blinded_pub2.id());
831
832 let message = b"Here is a terribly important string to authenticate.";
833 let other_message = b"Hey, that is not what I signed!";
834 let sign = blinded_keypair.sign(message);
835
836 assert!(blinded_pub.as_ref().verify(message, &sign).is_ok());
837 assert!(blinded_pub.as_ref().verify(other_message, &sign).is_err());
838 }
839
840 #[test]
841 fn key_blinding_testvec() {
842 let id = HsId::from(hex!(
844 "833990B085C1A688C1D4C8B1F6B56AFAF5A2ECA674449E1D704F83765CCB7BC6"
845 ));
846 let id_pubkey = HsIdKey::try_from(id).unwrap();
847 let id_seckey = HsIdKeypair::from(
848 ed25519::ExpandedKeypair::from_secret_key_bytes(hex!(
849 "D8C7FF0E31295B66540D789AF3E3DF992038A9592EEA01D8B7CBA06D6E66D159
850 4D6167696320576F7264733A20737065697373636F62616C742062697669756D"
851 ))
852 .unwrap(),
853 );
854 let time_period = TimePeriod::new(
855 humantime::parse_duration("1 day").unwrap(),
856 humantime::parse_rfc3339("1973-05-20T01:50:33Z").unwrap(),
857 humantime::parse_duration("12 hours").unwrap(),
858 )
859 .unwrap();
860 assert_eq!(time_period.interval_num, 1234);
861
862 let h = id_pubkey.blinding_factor(b"", time_period);
863 assert_eq!(
864 h,
865 hex!("379E50DB31FEE6775ABD0AF6FB7C371E060308F4F847DB09FE4CFE13AF602287")
866 );
867
868 let (blinded_pub1, subcred1) = id_pubkey.compute_blinded_key(time_period).unwrap();
869 assert_eq!(
870 blinded_pub1.0.to_bytes(),
871 hex!("3A50BF210E8F9EE955AE0014F7A6917FB65EBF098A86305ABB508D1A7291B6D5")
872 );
873 assert_eq!(
874 subcred1.as_ref(),
875 &hex!("635D55907816E8D76398A675A50B1C2F3E36B42A5CA77BA3A0441285161AE07D")
876 );
877
878 let (blinded_pub2, blinded_sec, subcred2) =
879 id_seckey.compute_blinded_key(time_period).unwrap();
880 assert_eq!(blinded_pub1.0.to_bytes(), blinded_pub2.0.to_bytes());
881 assert_eq!(subcred1.as_ref(), subcred2.as_ref());
882 assert_eq!(
883 blinded_sec.0.to_secret_key_bytes(),
884 hex!(
885 "A958DC83AC885F6814C67035DE817A2C604D5D2F715282079448F789B656350B
886 4540FE1F80AA3F7E91306B7BF7A8E367293352B14A29FDCC8C19F3558075524B"
887 )
888 );
889 }
890
891 #[test]
892 fn parse_client_desc_enc_key() {
893 use HsClientDescEncKeyParseError::*;
894
895 const VALID_KEY_BASE32: &str = "dz4q5xqlb4ldnbs72iarrml4ephk3du4i7o2cgiva5lwr6wkquja";
897
898 const WRONG_FORMAT: &[&str] = &["a:b:c:d:e", "descriptor:", "descriptor:x25519", ""];
900
901 for key in WRONG_FORMAT {
902 let err = HsClientDescEncKey::from_str(key).unwrap_err();
903
904 assert_eq!(err, InvalidFormat);
905 }
906
907 let err =
908 HsClientDescEncKey::from_str(&format!("foo:descriptor:x25519:{VALID_KEY_BASE32}"))
909 .unwrap_err();
910
911 assert_eq!(err, InvalidFormat);
912
913 let err = HsClientDescEncKey::from_str("bar:x25519:aa==").unwrap_err();
915 assert_eq!(err, InvalidAuthType("bar".into()));
916
917 let err = HsClientDescEncKey::from_str("descriptor:not-x25519:aa==").unwrap_err();
919 assert_eq!(err, InvalidKeyType("not-x25519".into()));
920
921 let err = HsClientDescEncKey::from_str("descriptor:x25519:aa==").unwrap_err();
923 assert!(matches!(err, InvalidBase32(_)));
924
925 let _key =
927 HsClientDescEncKey::from_str(&format!("descriptor:x25519:{VALID_KEY_BASE32}")).unwrap();
928
929 let desc_enc_key = HsClientDescEncKey::from(curve25519::PublicKey::from(
931 &curve25519::StaticSecret::random_from_rng(testing_rng()),
932 ));
933
934 assert_eq!(
935 desc_enc_key,
936 HsClientDescEncKey::from_str(&desc_enc_key.to_string()).unwrap()
937 );
938 }
939}