blobd_token/
lib.rs

1use blake3::Hash;
2use data_encoding::BASE64URL_NOPAD;
3use serde::Deserialize;
4use serde::Serialize;
5use std::mem::size_of;
6use std::time::SystemTime;
7use std::time::UNIX_EPOCH;
8
9pub struct BlobdTokens {
10  secret: [u8; 32],
11}
12
13const BLOBD_TOKEN_MAC_LEN: usize = 32;
14
15impl BlobdTokens {
16  pub fn new(secret: [u8; BLOBD_TOKEN_MAC_LEN]) -> Self {
17    Self { secret }
18  }
19
20  fn calculate_mac<T: Serialize>(&self, token_data: &T) -> Hash {
21    let mut raw = Vec::new();
22    token_data
23      .serialize(&mut rmp_serde::Serializer::new(&mut raw))
24      .unwrap();
25    blake3::keyed_hash(&self.secret, &raw)
26  }
27
28  pub fn generate<T: Serialize>(&self, token_data: &T) -> Vec<u8> {
29    self.calculate_mac(token_data).as_bytes().to_vec()
30  }
31
32  pub fn verify<T: Serialize>(&self, token: &[u8], expected_token_data: &T) -> bool {
33    if token.len() != BLOBD_TOKEN_MAC_LEN {
34      return false;
35    };
36    let mac: [u8; BLOBD_TOKEN_MAC_LEN] = token.try_into().unwrap();
37    // We must use Hash to avoid timing attacks that defeat MAC security.
38    let mac: Hash = mac.into();
39    if self.calculate_mac(expected_token_data) != mac {
40      return false;
41    };
42    true
43  }
44}
45
46#[derive(Serialize, Deserialize, PartialEq, Eq)]
47// WARNING: Order of fields is significant, as rmp_serde will serialise in this order without field names.
48pub enum AuthTokenAction {
49  BatchCreateObjects {},
50  // TODO Support incomplete_token.
51  CommitObject { key: Vec<u8> },
52  CreateObject { key: Vec<u8>, size: u64 },
53  DeleteObject { key: Vec<u8> },
54  InspectObject { key: Vec<u8> },
55  ReadObject { key: Vec<u8> },
56  // TODO Support incomplete_token.
57  WriteObject { key: Vec<u8> },
58}
59
60#[derive(Serialize, Deserialize)]
61// WARNING: Order of fields is significant, as rmp_serde will serialise in this order without field names.
62pub struct AuthToken {
63  action: AuthTokenAction,
64  expires: u64, // UNIX timestamp, seconds since epoch.
65}
66
67impl AuthToken {
68  pub fn new(tokens: &BlobdTokens, action: AuthTokenAction, expires: u64) -> String {
69    let token_data = AuthToken { action, expires };
70    let token_raw = tokens.generate(&token_data);
71    let mut raw = expires.to_be_bytes().to_vec();
72    raw.extend_from_slice(&token_raw);
73    BASE64URL_NOPAD.encode(&raw)
74  }
75
76  pub fn verify(tokens: &BlobdTokens, token: &str, expected_action: AuthTokenAction) -> bool {
77    let now = SystemTime::now()
78      .duration_since(UNIX_EPOCH)
79      .expect("system clock is before 1970")
80      .as_secs();
81    let Ok(raw) = BASE64URL_NOPAD.decode(token.as_bytes()) else {
82      return false;
83    };
84    if raw.len() != 8 + BLOBD_TOKEN_MAC_LEN {
85      return false;
86    };
87    let (expires_raw, token_raw) = raw.split_at(size_of::<u64>());
88    let expires = u64::from_be_bytes(expires_raw.try_into().unwrap());
89    if !tokens.verify(token_raw, &AuthToken {
90      action: expected_action,
91      expires,
92    }) {
93      return false;
94    };
95    if expires <= now {
96      return false;
97    };
98    true
99  }
100}