1use std::fmt::{self, Display, Formatter};
23use std::str::FromStr;
24
25use base32::Alphabet;
26use cypher::ed25519::PublicKey;
27use cypher::{EcPk, EcPkInvalid};
28use sha3::Digest;
29
30const ALPHABET: Alphabet = Alphabet::RFC4648 { padding: false };
31pub const ONION_V3_RAW_LEN: usize = 35;
32
33#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
34#[cfg_attr(
35 feature = "serde",
36 derive(Serialize, Deserialize),
37 serde(into = "String", try_from = "String")
38)]
39pub struct OnionAddrV3 {
40 pk: PublicKey,
41 checksum: u16,
42}
43
44impl From<PublicKey> for OnionAddrV3 {
45 fn from(pk: PublicKey) -> Self {
46 let mut h = sha3::Sha3_256::default();
47 h.update(b".onion checksum");
48 h.update(&pk[..]);
49 h.update([3u8]);
50 let hash = h.finalize();
51 let checksum = u16::from_le_bytes([hash[0], hash[1]]);
52 Self { pk, checksum }
53 }
54}
55
56impl From<OnionAddrV3> for PublicKey {
57 fn from(onion: OnionAddrV3) -> Self { onion.pk }
58}
59
60#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
61#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
62#[display(doc_comments)]
63pub enum OnionAddrDecodeError {
64 UnsupportedVersion(u8),
66
67 #[from(EcPkInvalid)]
69 InvalidKey,
70
71 InvalidChecksum,
73}
74
75impl OnionAddrV3 {
76 pub fn into_public_key(self) -> PublicKey { self.pk }
77
78 pub fn from_raw_bytes(bytes: [u8; ONION_V3_RAW_LEN]) -> Result<Self, OnionAddrDecodeError> {
79 let mut pk = [0u8; 32];
80 let mut checksum = [0u8; 2];
81 let version = bytes[ONION_V3_RAW_LEN - 1];
82
83 if version != 3 {
84 return Err(OnionAddrDecodeError::UnsupportedVersion(version));
85 }
86
87 pk.copy_from_slice(&bytes[..32]);
88 checksum.copy_from_slice(&bytes[32..34]);
89
90 let pk = PublicKey::from_pk_compressed(pk)?;
91 let checksum = u16::from_le_bytes(checksum);
92 let addr = OnionAddrV3::from(pk);
93
94 if addr.checksum != checksum {
95 return Err(OnionAddrDecodeError::InvalidChecksum);
96 }
97
98 Ok(addr)
99 }
100
101 pub fn into_raw_bytes(self) -> [u8; ONION_V3_RAW_LEN] {
102 let mut data = [0u8; ONION_V3_RAW_LEN];
103 data[..32].copy_from_slice(self.pk.as_slice());
104 data[32..34].copy_from_slice(&self.checksum.to_le_bytes());
105 data[ONION_V3_RAW_LEN - 1] = 3;
106 data
107 }
108
109 pub fn checksum(self) -> u16 { self.checksum }
110}
111
112#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
113#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
114#[display(doc_comments)]
115pub enum OnionAddrParseError {
116 NoSuffix(String),
118
119 InvalidBase32(String),
121
122 InvalidLen(String),
124
125 VersionMismatch(String, u8),
127
128 InvalidChecksum {
130 expected: u16,
131 found: u16,
132 addr: String,
133 },
134
135 #[from(EcPkInvalid)]
137 InvalidKey,
138}
139
140impl FromStr for OnionAddrV3 {
141 type Err = OnionAddrParseError;
142
143 fn from_str(s: &str) -> Result<Self, Self::Err> {
144 let stripped =
145 s.strip_suffix(".onion").ok_or_else(|| OnionAddrParseError::NoSuffix(s.to_owned()))?;
146 let data: Vec<u8> = base32::decode(ALPHABET, stripped)
147 .ok_or_else(|| OnionAddrParseError::InvalidBase32(s.to_owned()))?;
148 if data.len() != ONION_V3_RAW_LEN {
149 return Err(OnionAddrParseError::InvalidLen(s.to_owned()));
150 }
151 let ver = data[ONION_V3_RAW_LEN - 1];
152 if ver != 3 {
153 return Err(OnionAddrParseError::VersionMismatch(s.to_owned(), ver));
154 }
155 let mut key = [0u8; 32];
156 key.copy_from_slice(&data[..32]);
157 let pk = OnionAddrV3::from(PublicKey::from_pk_compressed(key)?);
158 let checksum = u16::from_le_bytes([data[32], data[33]]);
159 if pk.checksum != checksum {
160 return Err(OnionAddrParseError::InvalidChecksum {
161 expected: pk.checksum,
162 found: checksum,
163 addr: s.to_owned(),
164 });
165 }
166 Ok(pk)
167 }
168}
169
170impl Display for OnionAddrV3 {
171 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
172 let mut b32 = base32::encode(ALPHABET, &self.into_raw_bytes());
173 if !f.alternate() {
174 b32 = b32.to_lowercase();
175 }
176 write!(f, "{}.onion", b32)
177 }
178}
179
180impl From<OnionAddrV3> for String {
181 fn from(other: OnionAddrV3) -> Self { other.to_string() }
182}
183
184impl TryFrom<String> for OnionAddrV3 {
185 type Error = OnionAddrParseError;
186
187 fn try_from(value: String) -> Result<Self, Self::Error> { Self::from_str(&value) }
188}
189
190#[cfg(test)]
191mod test {
192 use super::*;
193
194 #[test]
195 fn display_from_str() {
196 let onion_str = "vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion";
197 let onion = OnionAddrV3::from_str(onion_str).unwrap();
198 assert_eq!(onion_str, onion.to_string());
199 }
200
201 #[test]
202 fn binary_code() {
203 let onion =
204 OnionAddrV3::from_str("vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion")
205 .unwrap();
206 assert_eq!(onion, OnionAddrV3::from_raw_bytes(onion.into_raw_bytes()).unwrap());
207 }
208}