1use fs_mistrust::Mistrust;
3use safelog::Sensitive;
4use std::{
5 fs, io,
6 path::{Path, PathBuf},
7 str::FromStr,
8 sync::Arc,
9};
10use subtle::ConstantTimeEq as _;
11use tiny_keccak::Hasher as _;
12use zeroize::Zeroizing;
13
14#[derive(Clone, Debug)]
16pub struct Cookie {
17 value: Sensitive<Zeroizing<[u8; COOKIE_LEN]>>,
19}
20impl AsRef<[u8; COOKIE_LEN]> for Cookie {
21 fn as_ref(&self) -> &[u8; COOKIE_LEN] {
22 self.value.as_inner()
23 }
24}
25
26pub const COOKIE_LEN: usize = 32;
28
29pub const COOKIE_PREFIX_LEN: usize = 32;
31
32const COOKIE_MAC_LEN: usize = 32;
34
35const COOKIE_NONCE_LEN: usize = 32;
37
38pub const COOKIE_PREFIX: &[u8; COOKIE_PREFIX_LEN] = b"====== arti-rpc-cookie-v1 ======";
43
44const TUPLEHASH_CUSTOMIZATION: &[u8] = b"arti-rpc-cookie-v1";
46
47impl Cookie {
48 pub fn load(path: &Path, mistrust: &Mistrust) -> Result<Cookie, CookieAccessError> {
50 use std::io::Read;
51
52 let mut file = mistrust
53 .verifier()
54 .file_access()
55 .follow_final_links(true)
56 .open(path, fs::OpenOptions::new().read(true))?;
57
58 let mut buf = [0_u8; COOKIE_PREFIX_LEN];
59 file.read_exact(&mut buf)?;
60 if &buf != COOKIE_PREFIX {
61 return Err(CookieAccessError::FileFormat);
62 }
63
64 let mut cookie = Cookie {
65 value: Default::default(),
66 };
67 file.read_exact(cookie.value.as_mut().as_mut())?;
68 if file.read(&mut buf)? != 0 {
69 return Err(CookieAccessError::FileFormat);
70 }
71
72 Ok(cookie)
73 }
74
75 #[cfg(feature = "rpc-server")]
78 pub fn create<R: rand::CryptoRng + rand::RngCore>(
79 path: &Path,
80 rng: &mut R,
81 mistrust: &Mistrust,
82 ) -> Result<Cookie, CookieAccessError> {
83 use std::io::Write;
84
85 let parent = path.parent().ok_or(CookieAccessError::UnusablePath)?;
88 mistrust
89 .verifier()
90 .require_directory()
91 .make_directory(parent)?;
92 let mut file = mistrust.file_access().follow_final_links(true).open(
93 path,
94 fs::OpenOptions::new()
95 .write(true)
96 .create(true)
97 .truncate(true),
98 )?;
99 let cookie = Self::new(rng);
100 file.write_all(&COOKIE_PREFIX[..])?;
101 file.write_all(cookie.value.as_inner().as_ref())?;
102
103 Ok(cookie)
104 }
105
106 fn new<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> Self {
108 let mut cookie = Cookie {
109 value: Default::default(),
110 };
111 rng.fill_bytes(cookie.value.as_mut().as_mut());
112 cookie
113 }
114
115 fn new_mac(&self) -> tiny_keccak::TupleHash {
117 let mut mac = tiny_keccak::TupleHash::v128(TUPLEHASH_CUSTOMIZATION);
118 mac.update(&**self.value);
119 mac
120 }
121
122 pub fn server_mac(
124 &self,
125 client_nonce: &CookieAuthNonce,
126 server_nonce: &CookieAuthNonce,
127 socket_canonical: &str,
128 ) -> CookieAuthMac {
129 let mut mac = self.new_mac();
131 mac.update(b"Server");
132 mac.update(socket_canonical.as_bytes());
133 mac.update(&**client_nonce.0);
134 mac.update(&**server_nonce.0);
135 CookieAuthMac::finalize_from(mac)
136 }
137
138 pub fn client_mac(
140 &self,
141 client_nonce: &CookieAuthNonce,
142 server_nonce: &CookieAuthNonce,
143 socket_canonical: &str,
144 ) -> CookieAuthMac {
145 let mut mac = self.new_mac();
147 mac.update(b"Client");
148 mac.update(socket_canonical.as_bytes());
149 mac.update(&**client_nonce.0);
150 mac.update(&**server_nonce.0);
151 CookieAuthMac::finalize_from(mac)
152 }
153}
154
155#[derive(Clone, Debug, thiserror::Error)]
157#[non_exhaustive]
158pub enum CookieAccessError {
159 #[error("Unable to access cookie file")]
161 Access(#[from] fs_mistrust::Error),
162 #[error("IO error while accessing cookie file")]
164 Io(#[source] Arc<io::Error>),
165 #[error("Could not find parent directory or filename for cookie file")]
167 UnusablePath,
168 #[error("Path did not point to a cookie file")]
170 FileFormat,
171}
172impl From<io::Error> for CookieAccessError {
173 fn from(err: io::Error) -> Self {
174 CookieAccessError::Io(Arc::new(err))
175 }
176}
177impl crate::HasClientErrorAction for CookieAccessError {
178 fn client_action(&self) -> crate::ClientErrorAction {
179 use crate::ClientErrorAction as A;
180 use CookieAccessError as E;
181 match self {
182 E::Access(err) => err.client_action(),
183 E::Io(err) => crate::fs_error_action(err.as_ref()),
184 E::UnusablePath => A::Decline,
185 E::FileFormat => A::Abort,
188 }
189 }
190}
191
192#[derive(Debug, Clone)]
194pub struct CookieLocation {
195 pub(crate) path: PathBuf,
197 pub(crate) mistrust: Mistrust,
199}
200
201impl CookieLocation {
202 pub fn load(&self) -> Result<Cookie, CookieAccessError> {
204 Cookie::load(self.path.as_ref(), &self.mistrust)
205 }
206}
207
208#[derive(Clone, Debug, thiserror::Error)]
210#[non_exhaustive]
211pub enum HexError {
212 #[error("Invalid hexadecimal value")]
214 InvalidHex,
215}
216
217#[derive(Clone, Debug, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
219pub struct CookieAuthNonce(Sensitive<Zeroizing<[u8; COOKIE_NONCE_LEN]>>);
220impl CookieAuthNonce {
221 pub fn new<R: rand::RngCore + rand::CryptoRng>(rng: &mut R) -> Self {
223 let mut nonce = Self(Default::default());
224 rng.fill_bytes(nonce.0.as_mut().as_mut());
225 nonce
226 }
227 pub fn to_hex(&self) -> String {
229 base16ct::upper::encode_string(&**self.0)
230 }
231 pub fn from_hex(s: &str) -> Result<Self, HexError> {
235 let mut nonce = Self(Default::default());
236 let decoded =
237 base16ct::mixed::decode(s, nonce.0.as_mut()).map_err(|_| HexError::InvalidHex)?;
238 if decoded.len() != COOKIE_NONCE_LEN {
239 return Err(HexError::InvalidHex);
240 }
241 Ok(nonce)
242 }
243}
244impl std::fmt::Display for CookieAuthNonce {
245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 write!(f, "{}", self.to_hex())
247 }
248}
249impl FromStr for CookieAuthNonce {
250 type Err = HexError;
251 fn from_str(s: &str) -> Result<Self, Self::Err> {
252 Self::from_hex(s)
253 }
254}
255
256#[derive(Clone, Debug, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
258pub struct CookieAuthMac(Sensitive<Zeroizing<[u8; COOKIE_MAC_LEN]>>);
259impl CookieAuthMac {
260 fn finalize_from(hasher: tiny_keccak::TupleHash) -> Self {
262 let mut mac = Self(Default::default());
263 hasher.finalize(mac.0.as_mut());
264 mac
265 }
266
267 pub fn to_hex(&self) -> String {
269 base16ct::upper::encode_string(&**self.0)
270 }
271 pub fn from_hex(s: &str) -> Result<Self, HexError> {
275 let mut mac = Self(Default::default());
276 let decoded =
277 base16ct::mixed::decode(s, mac.0.as_mut()).map_err(|_| HexError::InvalidHex)?;
278 if decoded.len() != COOKIE_MAC_LEN {
279 return Err(HexError::InvalidHex);
280 }
281 Ok(mac)
282 }
283}
284impl std::fmt::Display for CookieAuthMac {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 write!(f, "{}", self.to_hex())
287 }
288}
289impl FromStr for CookieAuthMac {
290 type Err = HexError;
291 fn from_str(s: &str) -> Result<Self, Self::Err> {
292 Self::from_hex(s)
293 }
294}
295impl PartialEq for CookieAuthMac {
296 fn eq(&self, other: &Self) -> bool {
297 self.0.ct_eq(&**other.0).into()
298 }
299}
300impl Eq for CookieAuthMac {}
301
302#[cfg(test)]
303mod test {
304 #![allow(clippy::bool_assert_comparison)]
306 #![allow(clippy::clone_on_copy)]
307 #![allow(clippy::dbg_macro)]
308 #![allow(clippy::mixed_attributes_style)]
309 #![allow(clippy::print_stderr)]
310 #![allow(clippy::print_stdout)]
311 #![allow(clippy::single_char_pattern)]
312 #![allow(clippy::unwrap_used)]
313 #![allow(clippy::unchecked_duration_subtraction)]
314 #![allow(clippy::useless_vec)]
315 #![allow(clippy::needless_pass_by_value)]
316 use super::*;
319 use crate::testing::tempdir;
320
321 #[test]
323 #[cfg(all(feature = "rpc-client", feature = "rpc-server"))]
324 fn cookie_file() {
325 let (_tempdir, dir, mistrust) = tempdir();
326 let path1 = dir.join("foo/foo.cookie");
327 let path2 = dir.join("bar.cookie");
328
329 let s_c1 = Cookie::create(path1.as_path(), &mut rand::thread_rng(), &mistrust).unwrap();
330 let s_c2 = Cookie::create(path2.as_path(), &mut rand::thread_rng(), &mistrust).unwrap();
331 assert_ne!(s_c1.as_ref(), s_c2.as_ref());
332
333 let c_c1 = Cookie::load(path1.as_path(), &mistrust).unwrap();
334 let c_c2 = Cookie::load(path2.as_path(), &mistrust).unwrap();
335 assert_eq!(s_c1.as_ref(), c_c1.as_ref());
336 assert_eq!(s_c2.as_ref(), c_c2.as_ref());
337 }
338
339 fn tuplehash(customization: &[u8], input: &[&[u8]]) -> [u8; 32] {
341 let mut th = tiny_keccak::TupleHash::v128(customization);
342 for v in input {
343 th.update(v);
344 }
345 let mut output: [u8; 32] = Default::default();
346 th.finalize(&mut output);
347 output
348 }
349
350 #[test]
352 fn auth_roundtrip() {
353 let addr = "127.0.0.1:9999";
354 let mut rng = rand::thread_rng();
355 let client_nonce = CookieAuthNonce::new(&mut rng);
356 let server_nonce = CookieAuthNonce::new(&mut rng);
357 let cookie = Cookie::new(&mut rng);
358
359 let smac = cookie.server_mac(&client_nonce, &server_nonce, addr);
360 let cmac = cookie.client_mac(&client_nonce, &server_nonce, addr);
361
362 let smac_expected = tuplehash(
364 TUPLEHASH_CUSTOMIZATION,
365 &[
366 &**cookie.value,
367 b"Server",
368 addr.as_bytes(),
369 &**client_nonce.0,
370 &**server_nonce.0,
371 ],
372 );
373 let cmac_expected = tuplehash(
375 TUPLEHASH_CUSTOMIZATION,
376 &[
377 &**cookie.value,
378 b"Client",
379 addr.as_bytes(),
380 &**client_nonce.0,
381 &**server_nonce.0,
382 ],
383 );
384 assert_eq!(**smac.0, smac_expected);
385 assert_eq!(**cmac.0, cmac_expected);
386
387 let smac_hex = smac.to_hex();
388 let smac2 = CookieAuthMac::from_hex(smac_hex.as_str()).unwrap();
389 assert_eq!(smac, smac2);
390
391 assert_ne!(cmac, smac); }
393
394 #[test]
396 fn tuplehash_testvec() {
397 use hex_literal::hex;
399 let val = tuplehash(b"", &[&hex!("00 01 02"), &hex!("10 11 12 13 14 15")]);
400 assert_eq!(
401 val,
402 hex!(
403 "C5 D8 78 6C 1A FB 9B 82 11 1A B3 4B 65 B2 C0 04
404 8F A6 4E 6D 48 E2 63 26 4C E1 70 7D 3F FC 8E D1"
405 )
406 );
407
408 let val = tuplehash(
409 b"My Tuple App",
410 &[&hex!("00 01 02"), &hex!("10 11 12 13 14 15")],
411 );
412 assert_eq!(
413 val,
414 hex!(
415 "75 CD B2 0F F4 DB 11 54 E8 41 D7 58 E2 41 60 C5
416 4B AE 86 EB 8C 13 E7 F5 F4 0E B3 55 88 E9 6D FB"
417 )
418 );
419
420 let val = tuplehash(
421 b"My Tuple App",
422 &[
423 &hex!("00 01 02"),
424 &hex!("10 11 12 13 14 15"),
425 &hex!("20 21 22 23 24 25 26 27 28"),
426 ],
427 );
428 assert_eq!(
429 val,
430 hex!(
431 "E6 0F 20 2C 89 A2 63 1E DA 8D 4C 58 8C A5 FD 07
432 F3 9E 51 51 99 8D EC CF 97 3A DB 38 04 BB 6E 84"
433 )
434 );
435 }
436
437 #[test]
438 fn hex_encoding() {
439 let s = "0000000000000000000000000000000000000000000000000012345678ABCDEF";
440 let expected = [
441 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34,
442 0x56, 0x78, 0xAB, 0xCD, 0xEF,
443 ];
444 assert_eq!(s.len(), COOKIE_NONCE_LEN * 2);
445 assert_eq!(s.len(), COOKIE_MAC_LEN * 2);
446 let cn = CookieAuthNonce::from_hex(s).unwrap();
447 assert_eq!(**cn.0, expected);
448 assert_eq!(cn.to_hex().as_str(), s);
449
450 let cm = CookieAuthMac::from_hex(s).unwrap();
451 assert_eq!(**cm.0, expected);
452 assert_eq!(cm.to_hex().as_str(), s);
453
454 let s2 = s.to_ascii_lowercase();
455 let cn2 = CookieAuthNonce::from_hex(&s2).unwrap();
456 let cm2 = CookieAuthMac::from_hex(&s2).unwrap();
457 assert_eq!(cn2.0, cn.0);
458 assert_eq!(cm2, cm);
459
460 for bad in [
461 "12345678",
463 "0000000000000000000000000000000000000000000000000012345678XXXXXX",
465 "0000000000000000000000000000000000000000000000000012345678ABCDEF12345678",
467 ] {
468 dbg!(bad);
469 assert!(CookieAuthNonce::from_hex(bad).is_err());
470 assert!(CookieAuthMac::from_hex(bad).is_err());
471 }
472 }
473}