use std::fmt::{self, Debug, Display};
use std::str::FromStr;
use derive_deftly::Deftly;
use digest::Digest;
use itertools::{Itertools, chain};
use safelog::DisplayRedacted;
use subtle::ConstantTimeEq;
use thiserror::Error;
use tor_basic_utils::{StrExt as _, impl_debug_hex};
use tor_key_forge::ToEncodableKey;
use tor_llcrypto::d::Sha3_256;
use tor_llcrypto::pk::ed25519::{Ed25519PublicKey, Ed25519SigningKey};
use tor_llcrypto::pk::{curve25519, ed25519, keymanip};
use tor_llcrypto::util::ct::CtByteArray;
use tor_llcrypto::{
derive_deftly_template_ConstantTimeEq, derive_deftly_template_PartialEqFromCtEq,
};
use crate::macros::{define_bytes, define_pk_keypair};
use crate::time::TimePeriod;
#[allow(deprecated)]
pub use hs_client_intro_auth::{HsClientIntroAuthKey, HsClientIntroAuthKeypair};
define_bytes! {
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HsId([u8; 32]);
}
define_pk_keypair! {
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
pub struct HsIdKey(ed25519::PublicKey) /
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
HsIdKeypair(ed25519::ExpandedKeypair);
}
impl HsIdKey {
pub fn id(&self) -> HsId {
HsId(self.0.to_bytes().into())
}
}
impl TryFrom<HsId> for HsIdKey {
type Error = signature::Error;
fn try_from(value: HsId) -> Result<Self, Self::Error> {
ed25519::PublicKey::from_bytes(value.0.as_ref()).map(HsIdKey)
}
}
impl From<HsIdKey> for HsId {
fn from(value: HsIdKey) -> Self {
value.id()
}
}
impl From<&HsIdKeypair> for HsIdKey {
fn from(value: &HsIdKeypair) -> Self {
Self(*value.0.public())
}
}
impl From<HsIdKeypair> for HsIdKey {
fn from(value: HsIdKeypair) -> Self {
Self(*value.0.public())
}
}
const HSID_ONION_VERSION: u8 = 0x03;
pub const HSID_ONION_SUFFIX: &str = ".onion";
impl safelog::DisplayRedacted for HsId {
fn fmt_unredacted(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
let checksum = self.onion_checksum();
let binary = chain!(self.0.as_ref(), &checksum, &[HSID_ONION_VERSION],)
.cloned()
.collect_vec();
let mut b32 = data_encoding::BASE32_NOPAD.encode(&binary);
b32.make_ascii_lowercase();
write!(f, "{}{}", b32, HSID_ONION_SUFFIX)
}
fn fmt_redacted(&self, f: &mut fmt::Formatter) -> fmt::Result {
let unredacted = self.display_unredacted().to_string();
const DATA: usize = 56;
assert_eq!(unredacted.len(), DATA + HSID_ONION_SUFFIX.len());
write!(f, "[…]{}", &unredacted[DATA - 3..])
}
}
impl safelog::DebugRedacted for HsId {
fn fmt_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "HsId({})", self.display_redacted())
}
fn fmt_unredacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "HsId({})", self.display_unredacted())
}
}
safelog::derive_redacted_debug!(HsId);
impl FromStr for HsId {
type Err = HsIdParseError;
fn from_str(s: &str) -> Result<Self, HsIdParseError> {
use HsIdParseError as PE;
let s = s
.strip_suffix_ignore_ascii_case(HSID_ONION_SUFFIX)
.ok_or(PE::NotOnionDomain)?;
if s.contains('.') {
return Err(PE::HsIdContainsSubdomain);
}
let mut s = s.to_owned();
s.make_ascii_uppercase();
let binary = data_encoding::BASE32_NOPAD.decode(s.as_bytes())?;
let mut binary = tor_bytes::Reader::from_slice(&binary);
let pubkey: [u8; 32] = binary.extract()?;
let checksum: [u8; 2] = binary.extract()?;
let version: u8 = binary.extract()?;
let tentative = HsId(pubkey.into());
if version != HSID_ONION_VERSION {
return Err(PE::UnsupportedVersion(version));
}
if checksum != tentative.onion_checksum() {
return Err(PE::WrongChecksum);
}
Ok(tentative)
}
}
#[derive(Error, Clone, Debug)]
#[non_exhaustive]
pub enum HsIdParseError {
#[error("Domain name does not end in .onion")]
NotOnionDomain,
#[error("Invalid base32 in .onion address")]
InvalidBase32(#[from] data_encoding::DecodeError),
#[error("Invalid encoded binary data in .onion address")]
InvalidData(#[from] tor_bytes::Error),
#[error("Unsupported .onion address version, v{0}")]
UnsupportedVersion(u8),
#[error("Checksum failed, .onion address corrupted")]
WrongChecksum,
#[error("`.onion` address with subdomain passed where not expected")]
HsIdContainsSubdomain,
}
impl HsId {
fn onion_checksum(&self) -> [u8; 2] {
let mut h = Sha3_256::new();
h.update(b".onion checksum");
h.update(self.0.as_ref());
h.update([HSID_ONION_VERSION]);
h.finalize()[..2]
.try_into()
.expect("slice of fixed size wasn't that size")
}
}
impl HsIdKey {
pub fn compute_blinded_key(
&self,
cur_period: TimePeriod,
) -> Result<(HsBlindIdKey, crate::Subcredential), keymanip::BlindingError> {
let secret = b"";
let h = self.blinding_factor(secret, cur_period);
let blinded_key = keymanip::blind_pubkey(&self.0, h)?.into();
let subcredential = self.compute_subcredential(&blinded_key, cur_period);
Ok((blinded_key, subcredential))
}
pub fn compute_subcredential(
&self,
blinded_key: &HsBlindIdKey,
cur_period: TimePeriod,
) -> crate::Subcredential {
let subcredential_bytes: [u8; 32] = {
let n_hs_cred: [u8; 32] = {
let mut h = Sha3_256::new();
h.update(b"credential");
h.update(self.0.as_bytes());
h.finalize().into()
};
let mut h = Sha3_256::new();
h.update(b"subcredential");
h.update(n_hs_cred);
h.update(blinded_key.as_bytes());
h.finalize().into()
};
subcredential_bytes.into()
}
fn blinding_factor(&self, secret: &[u8], cur_period: TimePeriod) -> [u8; 32] {
const BLIND_STRING: &[u8] = b"Derive temporary signing key\0";
const ED25519_BASEPOINT: &[u8] =
b"(15112221349535400772501151409588531511454012693041857206046113283949847762202, \
46316835694926478169428394003475163141307993866256225615783033603165251855960)";
let mut h = Sha3_256::new();
h.update(BLIND_STRING);
h.update(self.0.as_bytes());
h.update(secret);
h.update(ED25519_BASEPOINT);
h.update(b"key-blind");
h.update(cur_period.interval_num.to_be_bytes());
h.update((u64::from(cur_period.length.as_minutes())).to_be_bytes());
h.finalize().into()
}
}
impl HsIdKeypair {
pub fn compute_blinded_key(
&self,
cur_period: TimePeriod,
) -> Result<(HsBlindIdKey, HsBlindIdKeypair, crate::Subcredential), keymanip::BlindingError>
{
let secret = b"";
let public_key = HsIdKey(*self.0.public());
let (blinded_public_key, subcredential) = public_key.compute_blinded_key(cur_period)?;
let h = public_key.blinding_factor(secret, cur_period);
let blinded_keypair = keymanip::blind_keypair(&self.0, h)?;
Ok((blinded_public_key, blinded_keypair.into(), subcredential))
}
}
define_pk_keypair! {
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
pub struct HsBlindIdKey(ed25519::PublicKey) /
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
HsBlindIdKeypair(ed25519::ExpandedKeypair);
}
impl From<HsBlindIdKeypair> for HsBlindIdKey {
fn from(kp: HsBlindIdKeypair) -> HsBlindIdKey {
HsBlindIdKey(kp.0.into())
}
}
define_bytes! {
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HsBlindId([u8; 32]);
}
impl_debug_hex! { HsBlindId .0 }
impl HsBlindIdKey {
pub fn id(&self) -> HsBlindId {
HsBlindId(self.0.to_bytes().into())
}
}
impl TryFrom<HsBlindId> for HsBlindIdKey {
type Error = signature::Error;
fn try_from(value: HsBlindId) -> Result<Self, Self::Error> {
ed25519::PublicKey::from_bytes(value.0.as_ref()).map(HsBlindIdKey)
}
}
impl From<&HsBlindIdKeypair> for HsBlindIdKey {
fn from(value: &HsBlindIdKeypair) -> Self {
HsBlindIdKey(*value.0.public())
}
}
impl From<HsBlindIdKey> for HsBlindId {
fn from(value: HsBlindIdKey) -> Self {
value.id()
}
}
impl From<ed25519::Ed25519Identity> for HsBlindId {
fn from(value: ed25519::Ed25519Identity) -> Self {
Self(CtByteArray::from(<[u8; 32]>::from(value)))
}
}
impl Ed25519SigningKey for HsBlindIdKeypair {
fn sign(&self, message: &[u8]) -> ed25519::Signature {
self.0.sign(message)
}
}
impl Ed25519PublicKey for HsBlindIdKeypair {
fn public_key(&self) -> ed25519::PublicKey {
*self.0.public()
}
}
define_pk_keypair! {
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
pub struct HsDescSigningKey(ed25519::PublicKey) /
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
HsDescSigningKeypair(ed25519::Keypair);
}
define_pk_keypair! {
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
pub struct HsIntroPtSessionIdKey(ed25519::PublicKey) /
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
HsIntroPtSessionIdKeypair(ed25519::Keypair);
}
define_pk_keypair! {
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
pub struct HsSvcNtorKey(curve25519::PublicKey) /
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
HsSvcNtorSecretKey(curve25519::StaticSecret);
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
curve25519_pair as HsSvcNtorKeypair;
}
mod hs_client_intro_auth {
#![allow(deprecated)]
use subtle::ConstantTimeEq;
use tor_llcrypto::pk::ed25519;
use tor_llcrypto::{
derive_deftly::Deftly, derive_deftly_template_ConstantTimeEq,
derive_deftly_template_PartialEqFromCtEq,
};
use crate::macros::define_pk_keypair;
define_pk_keypair! {
#[deprecated(note = "This key type is not used in the protocol implemented today.")]
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
pub struct HsClientIntroAuthKey(ed25519::PublicKey) /
#[deprecated(note = "This key type is not used in the protocol implemented today.")]
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
HsClientIntroAuthKeypair(ed25519::Keypair);
}
}
define_pk_keypair! {
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
pub struct HsClientDescEncKey(curve25519::PublicKey) /
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
HsClientDescEncSecretKey(curve25519::StaticSecret);
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
curve25519_pair as HsClientDescEncKeypair;
}
impl Eq for HsClientDescEncKey {}
impl Display for HsClientDescEncKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let x25519_pk = data_encoding::BASE32_NOPAD.encode(&self.0.to_bytes());
write!(f, "descriptor:x25519:{}", x25519_pk)
}
}
impl FromStr for HsClientDescEncKey {
type Err = HsClientDescEncKeyParseError;
fn from_str(key: &str) -> Result<Self, HsClientDescEncKeyParseError> {
let (auth_type, key_type, encoded_key) = key
.split(':')
.collect_tuple()
.ok_or(HsClientDescEncKeyParseError::InvalidFormat)?;
if auth_type != "descriptor" {
return Err(HsClientDescEncKeyParseError::InvalidAuthType(
auth_type.into(),
));
}
if key_type != "x25519" {
return Err(HsClientDescEncKeyParseError::InvalidKeyType(
key_type.into(),
));
}
let encoded_key = encoded_key.to_uppercase();
let x25519_pk = data_encoding::BASE32_NOPAD.decode(encoded_key.as_bytes())?;
let x25519_pk: [u8; 32] = x25519_pk
.try_into()
.map_err(|_| HsClientDescEncKeyParseError::InvalidKeyMaterial)?;
Ok(Self(curve25519::PublicKey::from(x25519_pk)))
}
}
#[derive(Error, Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum HsClientDescEncKeyParseError {
#[error("Invalid auth type {0}")]
InvalidAuthType(String),
#[error("Invalid key type {0}")]
InvalidKeyType(String),
#[error("Invalid key format")]
InvalidFormat,
#[error("Invalid key material")]
InvalidKeyMaterial,
#[error("Invalid base32 in client key")]
InvalidBase32(#[from] data_encoding::DecodeError),
}
define_pk_keypair! {
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
pub struct HsSvcDescEncKey(curve25519::PublicKey) /
#[derive(Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
HsSvcDescEncSecretKey(curve25519::StaticSecret);
}
impl From<&HsClientDescEncSecretKey> for HsClientDescEncKey {
fn from(ks: &HsClientDescEncSecretKey) -> Self {
Self(curve25519::PublicKey::from(&ks.0))
}
}
impl From<&HsClientDescEncKeypair> for HsClientDescEncKey {
fn from(ks: &HsClientDescEncKeypair) -> Self {
Self(**ks.public())
}
}
#[allow(clippy::exhaustive_structs)]
#[derive(Debug, Deftly)]
#[derive_deftly(ConstantTimeEq, PartialEqFromCtEq)]
pub struct HsSvcDescEncKeypair {
pub public: HsSvcDescEncKey,
pub secret: HsSvcDescEncSecretKey,
}
impl ToEncodableKey for HsClientDescEncKeypair {
type Key = curve25519::StaticKeypair;
type KeyPair = HsClientDescEncKeypair;
fn to_encodable_key(self) -> Self::Key {
self.into()
}
fn from_encodable_key(key: Self::Key) -> Self {
HsClientDescEncKeypair::new(key.public.into(), key.secret.into())
}
}
impl ToEncodableKey for HsBlindIdKeypair {
type Key = ed25519::ExpandedKeypair;
type KeyPair = HsBlindIdKeypair;
fn to_encodable_key(self) -> Self::Key {
self.into()
}
fn from_encodable_key(key: Self::Key) -> Self {
HsBlindIdKeypair::from(key)
}
}
impl ToEncodableKey for HsBlindIdKey {
type Key = ed25519::PublicKey;
type KeyPair = HsBlindIdKeypair;
fn to_encodable_key(self) -> Self::Key {
self.into()
}
fn from_encodable_key(key: Self::Key) -> Self {
HsBlindIdKey::from(key)
}
}
impl ToEncodableKey for HsIdKeypair {
type Key = ed25519::ExpandedKeypair;
type KeyPair = HsIdKeypair;
fn to_encodable_key(self) -> Self::Key {
self.into()
}
fn from_encodable_key(key: Self::Key) -> Self {
HsIdKeypair::from(key)
}
}
impl ToEncodableKey for HsIdKey {
type Key = ed25519::PublicKey;
type KeyPair = HsIdKeypair;
fn to_encodable_key(self) -> Self::Key {
self.into()
}
fn from_encodable_key(key: Self::Key) -> Self {
HsIdKey::from(key)
}
}
impl ToEncodableKey for HsDescSigningKeypair {
type Key = ed25519::Keypair;
type KeyPair = HsDescSigningKeypair;
fn to_encodable_key(self) -> Self::Key {
self.into()
}
fn from_encodable_key(key: Self::Key) -> Self {
HsDescSigningKeypair::from(key)
}
}
impl ToEncodableKey for HsIntroPtSessionIdKeypair {
type Key = ed25519::Keypair;
type KeyPair = HsIntroPtSessionIdKeypair;
fn to_encodable_key(self) -> Self::Key {
self.into()
}
fn from_encodable_key(key: Self::Key) -> Self {
key.into()
}
}
impl ToEncodableKey for HsSvcNtorKeypair {
type Key = curve25519::StaticKeypair;
type KeyPair = HsSvcNtorKeypair;
fn to_encodable_key(self) -> Self::Key {
self.into()
}
fn from_encodable_key(key: Self::Key) -> Self {
key.into()
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use hex_literal::hex;
use itertools::izip;
use tor_basic_utils::test_rng::testing_rng;
use web_time_compat::{Duration, SystemTime, SystemTimeExt};
use super::*;
#[test]
fn hsid_strings() {
use HsIdParseError as PE;
let hex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
let b32 = "25njqamcweflpvkl73j4szahhihoc4xt3ktcgjnpaingr5yhkenl5sid";
let hsid: [u8; 32] = hex::decode(hex).unwrap().try_into().unwrap();
let hsid = HsId::from(hsid);
let onion = format!("{}.onion", b32);
assert_eq!(onion.parse::<HsId>().unwrap(), hsid);
assert_eq!(hsid.display_unredacted().to_string(), onion);
let weird_case: String = izip!(onion.chars(), [false, true].iter().cloned().cycle(),)
.map(|(c, swap)| if swap { c.to_ascii_uppercase() } else { c })
.collect();
dbg!(&weird_case);
assert_eq!(weird_case.parse::<HsId>().unwrap(), hsid);
macro_rules! chk_err { { $s:expr, $($pat:tt)* } => {
let e = $s.parse::<HsId>();
assert!(matches!(e, Err($($pat)*)), "{:?}", &e);
} }
let edited = |i, c| {
let mut s = b32.to_owned().into_bytes();
s[i] = c;
format!("{}.onion", String::from_utf8(s).unwrap())
};
chk_err!("wrong", PE::NotOnionDomain);
chk_err!("@.onion", PE::InvalidBase32(..));
chk_err!("aaaaaaaa.onion", PE::InvalidData(..));
chk_err!(edited(55, b'E'), PE::UnsupportedVersion(4));
chk_err!(edited(53, b'X'), PE::WrongChecksum);
chk_err!(&format!("www.{}", &onion), PE::HsIdContainsSubdomain);
safelog::with_safe_logging_suppressed(|| {
assert_eq!(format!("{:?}", &hsid), format!("HsId({})", onion));
});
assert_eq!(format!("{}", hsid.display_redacted()), "[…]sid.onion");
}
#[test]
fn key_blinding_blackbox() {
let mut rng = testing_rng();
let offset = Duration::new(12 * 60 * 60, 0);
let when = TimePeriod::new(Duration::from_secs(3600), SystemTime::get(), offset).unwrap();
let keypair = ed25519::Keypair::generate(&mut rng);
let id_pub = HsIdKey::from(keypair.verifying_key());
let id_keypair = HsIdKeypair::from(ed25519::ExpandedKeypair::from(&keypair));
let (blinded_pub, subcred1) = id_pub.compute_blinded_key(when).unwrap();
let (blinded_pub2, blinded_keypair, subcred2) =
id_keypair.compute_blinded_key(when).unwrap();
assert_eq!(subcred1.as_ref(), subcred2.as_ref());
assert_eq!(blinded_pub.0.to_bytes(), blinded_pub2.0.to_bytes());
assert_eq!(blinded_pub.id(), blinded_pub2.id());
let message = b"Here is a terribly important string to authenticate.";
let other_message = b"Hey, that is not what I signed!";
let sign = blinded_keypair.sign(message);
assert!(blinded_pub.as_ref().verify(message, &sign).is_ok());
assert!(blinded_pub.as_ref().verify(other_message, &sign).is_err());
}
#[test]
fn key_blinding_testvec() {
let id = HsId::from(hex!(
"833990B085C1A688C1D4C8B1F6B56AFAF5A2ECA674449E1D704F83765CCB7BC6"
));
let id_pubkey = HsIdKey::try_from(id).unwrap();
let id_seckey = HsIdKeypair::from(
ed25519::ExpandedKeypair::from_secret_key_bytes(hex!(
"D8C7FF0E31295B66540D789AF3E3DF992038A9592EEA01D8B7CBA06D6E66D159
4D6167696320576F7264733A20737065697373636F62616C742062697669756D"
))
.unwrap(),
);
let time_period = TimePeriod::new(
humantime::parse_duration("1 day").unwrap(),
humantime::parse_rfc3339("1973-05-20T01:50:33Z").unwrap(),
humantime::parse_duration("12 hours").unwrap(),
)
.unwrap();
assert_eq!(time_period.interval_num, 1234);
let h = id_pubkey.blinding_factor(b"", time_period);
assert_eq!(
h,
hex!("379E50DB31FEE6775ABD0AF6FB7C371E060308F4F847DB09FE4CFE13AF602287")
);
let (blinded_pub1, subcred1) = id_pubkey.compute_blinded_key(time_period).unwrap();
assert_eq!(
blinded_pub1.0.to_bytes(),
hex!("3A50BF210E8F9EE955AE0014F7A6917FB65EBF098A86305ABB508D1A7291B6D5")
);
assert_eq!(
subcred1.as_ref(),
&hex!("635D55907816E8D76398A675A50B1C2F3E36B42A5CA77BA3A0441285161AE07D")
);
let (blinded_pub2, blinded_sec, subcred2) =
id_seckey.compute_blinded_key(time_period).unwrap();
assert_eq!(blinded_pub1.0.to_bytes(), blinded_pub2.0.to_bytes());
assert_eq!(subcred1.as_ref(), subcred2.as_ref());
assert_eq!(
blinded_sec.0.to_secret_key_bytes(),
hex!(
"A958DC83AC885F6814C67035DE817A2C604D5D2F715282079448F789B656350B
4540FE1F80AA3F7E91306B7BF7A8E367293352B14A29FDCC8C19F3558075524B"
)
);
}
#[test]
fn parse_client_desc_enc_key() {
use HsClientDescEncKeyParseError::*;
const VALID_KEY_BASE32: &str = "dz4q5xqlb4ldnbs72iarrml4ephk3du4i7o2cgiva5lwr6wkquja";
const WRONG_FORMAT: &[&str] = &["a:b:c:d:e", "descriptor:", "descriptor:x25519", ""];
for key in WRONG_FORMAT {
let err = HsClientDescEncKey::from_str(key).unwrap_err();
assert_eq!(err, InvalidFormat);
}
let err =
HsClientDescEncKey::from_str(&format!("foo:descriptor:x25519:{VALID_KEY_BASE32}"))
.unwrap_err();
assert_eq!(err, InvalidFormat);
let err = HsClientDescEncKey::from_str("bar:x25519:aa==").unwrap_err();
assert_eq!(err, InvalidAuthType("bar".into()));
let err = HsClientDescEncKey::from_str("descriptor:not-x25519:aa==").unwrap_err();
assert_eq!(err, InvalidKeyType("not-x25519".into()));
let err = HsClientDescEncKey::from_str("descriptor:x25519:aa==").unwrap_err();
assert!(matches!(err, InvalidBase32(_)));
let _key =
HsClientDescEncKey::from_str(&format!("descriptor:x25519:{VALID_KEY_BASE32}")).unwrap();
let desc_enc_key = HsClientDescEncKey::from(curve25519::PublicKey::from(
&curve25519::StaticSecret::random_from_rng(testing_rng()),
));
assert_eq!(
desc_enc_key,
HsClientDescEncKey::from_str(&desc_enc_key.to_string()).unwrap()
);
}
}