blobd_token/
lib.rs

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