1use std::fmt;
2
3use sofie_core::Result;
4
5use sofie_core::encoding::base64::{self, URL_SAFE};
6
7const SECRET_BYTES: usize = 32;
8const TEXT_LENGTH: usize = (SECRET_BYTES * 4).div_ceil(3);
9
10pub struct BearerToken(String);
12
13impl BearerToken {
14 pub fn generate() -> Result<Self> {
15 let secret: [u8; SECRET_BYTES] = crate::random_bytes("a token")?;
16 Ok(Self(base64::encode(&URL_SAFE, &secret)))
17 }
18
19 #[must_use]
21 pub fn presented(text: &str) -> Option<Self> {
22 let well_formed = text.len() == TEXT_LENGTH
23 && text
24 .bytes()
25 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_');
26 well_formed.then(|| Self(text.to_owned()))
27 }
28
29 #[must_use]
30 pub fn expose(&self) -> &str {
31 &self.0
32 }
33
34 #[must_use]
36 pub fn digest(&self) -> String {
37 crate::digest(&[self.0.as_bytes()])
38 }
39}
40
41impl fmt::Debug for BearerToken {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 formatter.write_str("BearerToken(…)")
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::BearerToken;
50
51 #[test]
52 fn a_token_is_random_presentable_and_never_printed() {
53 let token = BearerToken::generate().unwrap();
54 let again = BearerToken::presented(token.expose()).unwrap();
55
56 assert_eq!(token.digest(), again.digest());
57 assert_ne!(token.expose(), BearerToken::generate().unwrap().expose());
58 assert_eq!(format!("{token:?}"), "BearerToken(…)");
59 assert!(BearerToken::presented("short").is_none());
60 assert!(BearerToken::presented(&format!("{}=", &token.expose()[1..])).is_none());
61 }
62}