Skip to main content

infrastore_server/
auth.rs

1//! Tonic interceptor for `api_key` authentication.
2
3use std::sync::Arc;
4
5use tonic::Status;
6use tonic::service::Interceptor;
7
8const HEADER: &str = "x-api-key";
9
10#[derive(Clone)]
11pub struct ApiKeyInterceptor {
12    keys: Arc<Vec<String>>,
13}
14
15impl ApiKeyInterceptor {
16    pub fn new(keys: Vec<String>) -> Self {
17        Self {
18            keys: Arc::new(keys),
19        }
20    }
21}
22
23impl Interceptor for ApiKeyInterceptor {
24    fn call(&mut self, req: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
25        let header = req
26            .metadata()
27            .get(HEADER)
28            .ok_or_else(|| Status::unauthenticated("missing x-api-key header"))?;
29        let supplied = header
30            .to_str()
31            .map_err(|_| Status::unauthenticated("x-api-key is not valid ASCII"))?;
32        if !any_match(&self.keys, supplied) {
33            return Err(Status::unauthenticated("invalid x-api-key"));
34        }
35        Ok(req)
36    }
37}
38
39/// Compares against every configured key regardless of where a match falls, so
40/// there is no early-exit timing leak across keys. The comparison is only
41/// constant-time among keys of the same length as `supplied`: a length mismatch
42/// is rejected before the XOR loop, which leaks the supplied key's length.
43fn any_match(keys: &[String], supplied: &str) -> bool {
44    let supplied = supplied.as_bytes();
45    let mut found = false;
46    for k in keys {
47        let k = k.as_bytes();
48        if k.len() != supplied.len() {
49            // Length itself isn't secret; cheap reject.
50            continue;
51        }
52        let mut diff: u8 = 0;
53        for (a, b) in k.iter().zip(supplied.iter()) {
54            diff |= a ^ b;
55        }
56        found |= diff == 0;
57    }
58    found
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn matches_valid_key() {
67        let keys = vec!["secret-1".to_string(), "secret-2".to_string()];
68        assert!(any_match(&keys, "secret-1"));
69        assert!(any_match(&keys, "secret-2"));
70    }
71
72    #[test]
73    fn rejects_unknown_key() {
74        let keys = vec!["secret-1".to_string()];
75        assert!(!any_match(&keys, "nope"));
76        assert!(!any_match(&keys, ""));
77        assert!(!any_match(&keys, "secret-12"));
78    }
79}