Skip to main content

clawbox_proxy/
credentials.rs

1//! Credential injection at the network proxy boundary.
2//!
3//! Credentials are injected into outbound requests based on the destination
4//! domain. The WASM guest and container never see raw API keys.
5
6use std::collections::HashMap;
7use zeroize::Zeroizing;
8
9/// Maps destination domains to credential injection rules.
10#[derive(Debug, Clone)]
11#[non_exhaustive]
12#[must_use]
13pub struct CredentialInjector {
14    /// domain → (header_name, credential_value)
15    mappings: HashMap<String, CredentialMapping>,
16}
17
18/// How to inject a credential into an outbound request.
19#[derive(Clone)]
20#[non_exhaustive]
21pub struct CredentialMapping {
22    /// HTTP header name to set.
23    pub header: String,
24    /// Header value (including any prefix like "Bearer ").
25    /// Wrapped in `Zeroizing` so the secret is cleared from memory on drop.
26    pub value: Zeroizing<String>,
27}
28
29impl std::fmt::Debug for CredentialMapping {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.debug_struct("CredentialMapping")
32            .field("header", &self.header)
33            .field("value", &"[REDACTED]")
34            .finish()
35    }
36}
37
38impl CredentialInjector {
39    /// Create a new empty injector.
40    pub fn new() -> Self {
41        Self {
42            mappings: HashMap::new(),
43        }
44    }
45
46    /// Register a credential mapping for a domain.
47    pub fn add_mapping(
48        &mut self,
49        domain: impl Into<String>,
50        header: impl Into<String>,
51        value: impl Into<String>,
52    ) {
53        self.mappings.insert(
54            domain.into(),
55            CredentialMapping {
56                header: header.into(),
57                value: Zeroizing::new(value.into()),
58            },
59        );
60    }
61
62    /// Get the credential mapping for a domain, if any.
63    pub fn get_mapping(&self, domain: &str) -> Option<&CredentialMapping> {
64        self.mappings.get(domain)
65    }
66}
67
68impl Default for CredentialInjector {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_add_and_get_mapping() {
80        let mut injector = CredentialInjector::new();
81        injector.add_mapping("api.github.com", "Authorization", "token ghp_abc");
82
83        let mapping = injector.get_mapping("api.github.com").unwrap();
84        assert_eq!(mapping.header, "Authorization");
85        assert_eq!(&*mapping.value, "token ghp_abc");
86    }
87
88    #[test]
89    fn test_get_missing_mapping() {
90        let injector = CredentialInjector::new();
91        assert!(injector.get_mapping("api.github.com").is_none());
92    }
93
94    #[test]
95    fn test_overwrite_mapping() {
96        let mut injector = CredentialInjector::new();
97        injector.add_mapping("api.example.com", "X-Key", "old");
98        injector.add_mapping("api.example.com", "X-Key", "new");
99
100        let mapping = injector.get_mapping("api.example.com").unwrap();
101        assert_eq!(&*mapping.value, "new");
102    }
103}