cookie_agent_server/
token.rs1use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
4use serde::Serialize;
5use zeroize::Zeroizing;
6
7const TOKEN_BYTES: usize = 32;
9pub(crate) const TOKEN_ENCODED_BYTES: usize = 43;
11pub const READY_LINE_PREFIX: &str = "daemon-ready ";
13
14#[derive(Debug, thiserror::Error)]
16pub enum TokenError {
17 #[error("could not obtain randomness for the daemon authentication token")]
19 Random(#[source] getrandom::Error),
20}
21
22pub fn generate_token() -> Result<Zeroizing<String>, TokenError> {
25 let mut bytes = [0_u8; TOKEN_BYTES];
26 getrandom::getrandom(&mut bytes).map_err(TokenError::Random)?;
27 Ok(Zeroizing::new(URL_SAFE_NO_PAD.encode(bytes)))
28}
29
30pub fn ready_line(url: &str, token: &str) -> String {
35 #[derive(Serialize)]
36 struct ReadyFrame<'a> {
37 url: &'a str,
38 token: &'a str,
39 }
40
41 let frame = serde_json::to_string(&ReadyFrame { url, token })
42 .expect("serializing a two-string ready frame cannot fail");
43 format!("{READY_LINE_PREFIX}{frame}")
44}
45
46#[cfg(test)]
47mod tests {
48 use std::collections::HashSet;
49
50 use super::*;
51
52 #[test]
53 fn generated_tokens_are_43_char_base64url_and_distinct() {
54 let mut seen = HashSet::new();
55 for _ in 0..64 {
56 let token = generate_token().expect("token");
57 assert_eq!(token.len(), TOKEN_ENCODED_BYTES);
58 assert!(
59 token
60 .bytes()
61 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')),
62 "{}",
63 token.as_str()
64 );
65 assert!(seen.insert(token.to_string()));
66 }
67 }
68
69 #[test]
70 fn ready_line_is_one_json_frame_with_the_documented_prefix() {
71 let token = "A".repeat(TOKEN_ENCODED_BYTES);
72 let line = ready_line("ws://127.0.0.1:7419/ws", &token);
73 let payload = line
74 .strip_prefix(READY_LINE_PREFIX)
75 .expect("daemon-ready prefix");
76 let value: serde_json::Value = serde_json::from_str(payload).expect("json frame");
77 assert_eq!(value["url"].as_str(), Some("ws://127.0.0.1:7419/ws"));
78 assert_eq!(value["token"].as_str(), Some(token.as_str()));
79 assert_eq!(
80 payload,
81 format!(r#"{{"url":"ws://127.0.0.1:7419/ws","token":"{token}"}}"#)
82 );
83 }
84}