Skip to main content

atomr_persistence_azure/
auth.rs

1//! Azure Storage Shared Key signer (lite variant for Table Storage).
2//!
3//! Implements the subset of the signing rules needed for REST operations
4//! the provider invokes (GET / POST / MERGE / DELETE on table resources).
5
6use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
7use hmac::{Hmac, Mac};
8use sha2::Sha256;
9
10type HmacSha256 = Hmac<Sha256>;
11
12pub struct SharedKeySigner {
13    account: String,
14    decoded_key: Vec<u8>,
15}
16
17impl SharedKeySigner {
18    pub fn new(account: impl Into<String>, key_b64: &str) -> Result<Self, String> {
19        let decoded = B64.decode(key_b64).map_err(|e| e.to_string())?;
20        Ok(Self { account: account.into(), decoded_key: decoded })
21    }
22
23    /// Produce the `Authorization` header value for the given request.
24    ///
25    /// `canonicalized_resource` must start with `/{account}` and include
26    /// the resource path (e.g. `/devstoreaccount1/Tables`). For SharedKeyLite
27    /// against the Table service, `StringToSign` is exactly
28    /// `Date + "\n" + CanonicalizedResource` — the verb, Content-MD5 and
29    /// Content-Type are NOT included (that's the plain SharedKey scheme).
30    /// `method` is accepted for API symmetry but unused in the signature.
31    pub fn sign_lite(&self, _method: &str, date_header: &str, canonicalized_resource: &str) -> String {
32        let string_to_sign = format!("{date_header}\n{canonicalized_resource}");
33        let mut mac = HmacSha256::new_from_slice(&self.decoded_key).expect("hmac key");
34        mac.update(string_to_sign.as_bytes());
35        let sig = B64.encode(mac.finalize().into_bytes());
36        format!("SharedKeyLite {account}:{sig}", account = self.account)
37    }
38
39    pub fn account(&self) -> &str {
40        &self.account
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn signs_produces_stable_output() {
50        let key = B64.encode(b"0123456789abcdef0123456789abcdef");
51        let signer = SharedKeySigner::new("acct", &key).unwrap();
52        let a = signer.sign_lite("GET", "Mon, 01 Jan 2024 00:00:00 GMT", "/acct/Tables");
53        let b = signer.sign_lite("GET", "Mon, 01 Jan 2024 00:00:00 GMT", "/acct/Tables");
54        assert_eq!(a, b, "signer should be deterministic");
55        assert!(a.starts_with("SharedKeyLite acct:"));
56    }
57
58    #[test]
59    fn rejects_bad_key() {
60        assert!(SharedKeySigner::new("acct", "not-base64!!").is_err());
61    }
62}