Skip to main content

simple_agent_type/
validation.rs

1//! Validation types for sensitive data.
2//!
3//! Provides secure handling of API keys and other credentials.
4
5use crate::error::{Result, ValidationError};
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use subtle::ConstantTimeEq;
9
10/// API key (validated, never logged or displayed).
11///
12/// This type ensures API keys are:
13/// - Validated on construction
14/// - Never logged in Debug output
15/// - Never serialized in plain text
16/// - Only exposed through explicit `expose()` method
17///
18/// # Example
19/// ```
20/// use simple_agent_type::validation::ApiKey;
21///
22/// let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
23/// let debug_str = format!("{:?}", key);
24/// assert!(debug_str.contains("REDACTED"));
25/// assert!(!debug_str.contains("sk-"));
26/// ```
27#[derive(Clone)]
28pub struct ApiKey(String);
29
30impl ApiKey {
31    /// Create a new API key with validation.
32    ///
33    /// # Validation Rules
34    /// - Must not be empty
35    /// - Must be at least 20 characters
36    /// - Must not contain null bytes (security)
37    ///
38    /// # Example
39    /// ```
40    /// use simple_agent_type::validation::ApiKey;
41    ///
42    /// let key = ApiKey::new("sk-1234567890abcdef1234567890");
43    /// assert!(key.is_ok());
44    ///
45    /// let invalid = ApiKey::new("short");
46    /// assert!(invalid.is_err());
47    /// ```
48    pub fn new(key: impl Into<String>) -> Result<Self> {
49        let key = key.into();
50
51        if key.is_empty() {
52            return Err(ValidationError::Empty {
53                field: "api_key".to_string(),
54            }
55            .into());
56        }
57
58        if key.len() < 20 {
59            return Err(ValidationError::TooShort {
60                field: "api_key".to_string(),
61                min: 20,
62            }
63            .into());
64        }
65
66        // Security: prevent null byte injection
67        if key.contains('\0') {
68            return Err(ValidationError::InvalidFormat {
69                field: "api_key".to_string(),
70                reason: "contains null bytes".to_string(),
71            }
72            .into());
73        }
74
75        Ok(Self(key))
76    }
77
78    /// Expose the raw API key.
79    ///
80    /// # Security Warning
81    /// Only use this when actually making API requests. Never log or display the result.
82    ///
83    /// # Example
84    /// ```
85    /// use simple_agent_type::validation::ApiKey;
86    ///
87    /// let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
88    /// let raw = key.expose();
89    /// assert_eq!(raw, "sk-1234567890abcdef1234567890");
90    /// ```
91    pub fn expose(&self) -> &str {
92        &self.0
93    }
94
95    /// Get a redacted preview of the key (for debugging).
96    ///
97    /// Shows only the prefix and length.
98    ///
99    /// # Example
100    /// ```
101    /// use simple_agent_type::validation::ApiKey;
102    ///
103    /// let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
104    /// let preview = key.preview();
105    /// assert!(preview.contains("sk-"));
106    /// assert!(preview.contains("29 chars"));
107    /// ```
108    pub fn preview(&self) -> String {
109        let prefix = if self.0.len() >= 7 {
110            &self.0[..7]
111        } else {
112            &self.0
113        };
114        format!("{}*** ({} chars)", prefix, self.0.len())
115    }
116}
117
118// CRITICAL: Never log API keys in Debug output
119impl fmt::Debug for ApiKey {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(f, "ApiKey([REDACTED])")
122    }
123}
124
125// CRITICAL: Never serialize API keys in plain text
126impl Serialize for ApiKey {
127    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
128    where
129        S: serde::Serializer,
130    {
131        serializer.serialize_str("[REDACTED]")
132    }
133}
134
135// Allow deserialization for config loading
136impl<'de> Deserialize<'de> for ApiKey {
137    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
138    where
139        D: serde::Deserializer<'de>,
140    {
141        let s = String::deserialize(deserializer)?;
142        ApiKey::new(s).map_err(serde::de::Error::custom)
143    }
144}
145
146// Implement PartialEq with constant-time comparison for security
147impl PartialEq for ApiKey {
148    fn eq(&self, other: &Self) -> bool {
149        self.0.as_bytes().ct_eq(other.0.as_bytes()).into()
150    }
151}
152
153impl Eq for ApiKey {}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_api_key_valid() {
161        let key = ApiKey::new("sk-1234567890abcdef1234567890");
162        assert!(key.is_ok());
163    }
164
165    #[test]
166    fn test_api_key_empty() {
167        let key = ApiKey::new("");
168        assert!(key.is_err());
169        assert!(matches!(
170            key.unwrap_err(),
171            crate::error::SimpleAgentsError::Validation(ValidationError::Empty { .. })
172        ));
173    }
174
175    #[test]
176    fn test_api_key_too_short() {
177        let key = ApiKey::new("short");
178        assert!(key.is_err());
179        assert!(matches!(
180            key.unwrap_err(),
181            crate::error::SimpleAgentsError::Validation(ValidationError::TooShort { .. })
182        ));
183    }
184
185    #[test]
186    fn test_api_key_null_byte() {
187        let key = ApiKey::new("sk-12345678901234567890\0extra");
188        assert!(key.is_err());
189        assert!(matches!(
190            key.unwrap_err(),
191            crate::error::SimpleAgentsError::Validation(ValidationError::InvalidFormat { .. })
192        ));
193    }
194
195    #[test]
196    fn test_api_key_expose() {
197        let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
198        assert_eq!(key.expose(), "sk-1234567890abcdef1234567890");
199    }
200
201    #[test]
202    fn test_api_key_debug_redacted() {
203        let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
204        let debug = format!("{:?}", key);
205        assert!(debug.contains("REDACTED"));
206        assert!(!debug.contains("sk-"));
207        assert!(!debug.contains("1234"));
208    }
209
210    #[test]
211    fn test_api_key_serialize_redacted() {
212        let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
213        let json = serde_json::to_string(&key).unwrap();
214        assert_eq!(json, "\"[REDACTED]\"");
215        assert!(!json.contains("sk-"));
216    }
217
218    #[test]
219    fn test_api_key_deserialize() {
220        let json = "\"sk-1234567890abcdef1234567890\"";
221        let key: ApiKey = serde_json::from_str(json).unwrap();
222        assert_eq!(key.expose(), "sk-1234567890abcdef1234567890");
223    }
224
225    #[test]
226    fn test_api_key_preview() {
227        let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
228        let preview = key.preview();
229        assert!(preview.contains("sk-"));
230        assert!(preview.contains("29 chars"));
231        assert!(!preview.contains("abcdef"));
232    }
233
234    #[test]
235    fn test_api_key_equality() {
236        let key1 = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
237        let key2 = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
238        let key3 = ApiKey::new("sk-differentkey1234567890").unwrap();
239
240        assert_eq!(key1, key2);
241        assert_ne!(key1, key3);
242    }
243
244    #[test]
245    fn test_api_key_constant_time_comparison() {
246        // Test that constant-time comparison works correctly
247        let key1 = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
248        let key2 = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
249        let key3 = ApiKey::new("sk-9999999999999999999999").unwrap();
250
251        // Same keys should be equal
252        assert_eq!(key1, key2);
253
254        // Different keys should not be equal
255        assert_ne!(key1, key3);
256
257        // Keys differing only in last character should still be detected as different
258        let key4 = ApiKey::new("sk-1234567890abcdef12345678901").unwrap();
259        assert_ne!(key1, key4);
260    }
261}