1#![cfg_attr(feature = "no-std-net", no_std)]
14#![forbid(unsafe_code)]
15
16use core::convert::TryFrom;
17use core::fmt;
18use core::hash::Hasher;
19#[cfg(feature = "no-std-net")]
20use no_std_net::IpAddr;
21use siphasher::sip::SipHasher24;
22#[cfg(not(feature = "no-std-net"))]
23use std::net::IpAddr;
24use time::ext::NumericalDuration;
25use time::{OffsetDateTime, UtcOffset};
26
27const SERVER_COOKIE_LEN: usize = 16;
28const CLIENT_COOKIE_LEN: usize = 8;
29
30#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
32#[must_use]
33pub enum Version {
34 One = 1,
35}
36
37impl TryFrom<u8> for Version {
38 type Error = Error;
39
40 fn try_from(version: u8) -> Result<Self, Self::Error> {
41 match version {
42 v if Version::One as u8 == v => Ok(Version::One),
43 v => Err(Error::UnknownVersion(v)),
44 }
45 }
46}
47
48#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
50#[must_use]
51pub enum Algorithm {
52 SipHash24 = 4,
53}
54
55impl TryFrom<u8> for Algorithm {
56 type Error = Error;
57
58 fn try_from(algorithm: u8) -> Result<Self, Self::Error> {
59 match algorithm {
60 v if Algorithm::SipHash24 as u8 == v => Ok(Algorithm::SipHash24),
61 1 => Err(Error::UnsupportedAlgorithm("FNV")),
62 2 => Err(Error::UnsupportedAlgorithm("HMAC-SHA-256-64")),
63 3 => Err(Error::UnsupportedAlgorithm("AES")),
64 v => Err(Error::UnknownAlgorithm(v)),
65 }
66 }
67}
68
69#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
70struct Data {
71 version: Version,
72 algorithm: Algorithm,
73 reserved: u16,
74 time: OffsetDateTime,
75 client_cookie: [u8; CLIENT_COOKIE_LEN],
76}
77
78impl Data {
79 fn hash(&self, server_secret: &[u8]) -> u64 {
80 match self.version {
81 Version::One => match self.algorithm {
82 Algorithm::SipHash24 => {
83 let mut hasher = SipHasher24::new();
84 hasher.write(&self.client_cookie);
85 hasher.write_u8(self.version as u8);
86 hasher.write_u8(self.algorithm as u8);
87 hasher.write_u16(self.reserved);
88 hasher.write_u32(self.time.unix_timestamp() as u32);
89 hasher.write(server_secret);
90 hasher.finish()
91 }
92 },
93 }
94 }
95}
96
97#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
99#[must_use]
100pub struct Server {
101 data: Data,
102 hash: u64,
103}
104
105impl Server {
106 pub fn new(
108 version: Version,
109 algorithm: Algorithm,
110 reserved: u16,
111 time: OffsetDateTime,
112 client_cookie: [u8; CLIENT_COOKIE_LEN],
113 server_secret: &[u8],
114 ) -> Self {
115 let data = Data {
116 version,
117 algorithm,
118 reserved,
119 client_cookie,
120 time: time.to_offset(UtcOffset::UTC),
121 };
122 Self {
123 data,
124 hash: data.hash(server_secret),
125 }
126 }
127
128 pub fn regenerate(mut self, time: OffsetDateTime, server_secret: &[u8]) -> Self {
131 let time = time.to_offset(UtcOffset::UTC);
132 if self.data.time > time - 30.minutes() {
133 return self;
134 }
135 self.data.time = time;
136 self.hash = self.data.hash(server_secret);
137 self
138 }
139
140 pub fn decode(
142 mut now: OffsetDateTime,
143 client_cookie: [u8; CLIENT_COOKIE_LEN],
144 server_cookie: &[u8],
145 server_secrets: &[&[u8]],
146 ) -> Result<Self, Error> {
147 now = now.to_offset(UtcOffset::UTC);
148 let cookie_len = server_cookie.len();
149 if cookie_len != SERVER_COOKIE_LEN {
150 return Err(Error::IncorrectLength(cookie_len));
151 }
152 let version = Version::try_from(server_cookie[0])?;
153 let algorithm = Algorithm::try_from(server_cookie[1])?;
154 let reserved = u16::from_be_bytes([server_cookie[2], server_cookie[3]]);
155 let time = {
156 let timestamp = u32::from_be_bytes([
157 server_cookie[4],
158 server_cookie[5],
159 server_cookie[6],
160 server_cookie[7],
161 ]);
162 OffsetDateTime::from_unix_timestamp(timestamp as i64).map_err(Error::TimestampRange)?
163 };
164 if time < now - 1.hours() {
165 return Err(Error::Expired);
166 } else if time > now + 5.minutes() {
167 return Err(Error::TimeTravellor);
168 }
169 let hash = u64::from_be_bytes([
170 server_cookie[8],
171 server_cookie[9],
172 server_cookie[10],
173 server_cookie[11],
174 server_cookie[12],
175 server_cookie[13],
176 server_cookie[14],
177 server_cookie[15],
178 ]);
179 for secret in server_secrets {
180 let cookie = Self::new(version, algorithm, reserved, time, client_cookie, secret);
181 if cookie.hash == hash {
182 return Ok(cookie);
183 }
184 }
185 Err(Error::InvalidHash)
186 }
187
188 #[must_use]
190 pub const fn encode(self) -> [u8; SERVER_COOKIE_LEN] {
191 let reserved = self.data.reserved.to_be_bytes();
192 let timestamp = (self.data.time.unix_timestamp() as u32).to_be_bytes();
193 let hash = self.hash.to_be_bytes();
194 [
195 self.data.version as u8,
196 self.data.algorithm as u8,
197 reserved[0],
198 reserved[1],
199 timestamp[0],
200 timestamp[1],
201 timestamp[2],
202 timestamp[3],
203 hash[0],
204 hash[1],
205 hash[2],
206 hash[3],
207 hash[4],
208 hash[5],
209 hash[6],
210 hash[7],
211 ]
212 }
213}
214
215#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
217#[must_use]
218pub struct Client {
219 hash: u64,
220}
221
222impl Client {
223 pub fn new(
225 version: Version,
226 algorithm: Algorithm,
227 client_ip: IpAddr,
228 server_ip: IpAddr,
229 client_secret: &[u8],
230 ) -> Self {
231 match version {
232 Version::One => match algorithm {
233 Algorithm::SipHash24 => {
234 let mut hasher = SipHasher24::new();
235 match client_ip {
236 IpAddr::V4(ip) => hasher.write(&ip.octets()),
237 IpAddr::V6(ip) => hasher.write(&ip.octets()),
238 }
239 match server_ip {
240 IpAddr::V4(ip) => hasher.write(&ip.octets()),
241 IpAddr::V6(ip) => hasher.write(&ip.octets()),
242 }
243 hasher.write(client_secret);
244 Self {
245 hash: hasher.finish(),
246 }
247 }
248 },
249 }
250 }
251
252 pub fn decode(
254 version: Version,
255 algorithm: Algorithm,
256 client_ip: IpAddr,
257 server_ip: IpAddr,
258 client_cookie: [u8; CLIENT_COOKIE_LEN],
259 client_secrets: &[&[u8]],
260 ) -> Result<Self, Error> {
261 let hash = u64::from_be_bytes(client_cookie);
262 for secret in client_secrets {
263 let cookie = Self::new(version, algorithm, client_ip, server_ip, secret);
264 if cookie.hash == hash {
265 return Ok(cookie);
266 }
267 }
268 Err(Error::InvalidHash)
269 }
270
271 #[must_use]
273 pub const fn encode(self) -> [u8; CLIENT_COOKIE_LEN] {
274 self.hash.to_be_bytes()
275 }
276}
277
278#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
280#[must_use]
281pub enum Error {
282 IncorrectLength(usize),
283 TimestampRange(time::error::ComponentRange),
284 InvalidHash,
285 Expired,
286 TimeTravellor,
287 UnknownVersion(u8),
288 UnknownAlgorithm(u8),
289 UnsupportedAlgorithm(&'static str),
290}
291
292impl fmt::Display for Error {
293 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
294 match self {
295 Error::IncorrectLength(len) => write!(f, "cookie has an incorrect length ({})", len),
296 Error::TimestampRange(error) => write!(f, "{}", error),
297 Error::InvalidHash => write!(f, "cookie has an invalid hash"),
298 Error::Expired => write!(f, "cookie has expired"),
299 Error::TimeTravellor => write!(f, "cookie has a timestamp from the future"),
300 Error::UnknownVersion(version) => {
301 write!(f, "cookie has an unknown version ({})", version)
302 }
303 Error::UnknownAlgorithm(algorithm) => {
304 write!(f, "cookie has an unknown algorithm ({})", algorithm)
305 }
306 Error::UnsupportedAlgorithm(algorithm) => {
307 write!(f, "cookie has an unsupported algorithm ({})", algorithm)
308 }
309 }
310 }
311}
312
313#[cfg(not(feature = "no-std-net"))]
314impl std::error::Error for Error {}