rocketmq_security_api/
secret_provider.rs1use std::fmt;
18
19use cheetah_string::CheetahString;
20use zeroize::Zeroize;
21
22use crate::SecurityContractViolation;
23use crate::SecurityIdentifierRule;
24use crate::SecurityProviderError;
25
26#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct SecretProviderId(CheetahString);
29
30impl SecretProviderId {
31 pub fn new(value: impl Into<CheetahString>) -> Result<Self, SecurityContractViolation> {
38 let value = value.into();
39 validate_identifier(&value)?;
40 Ok(Self(value))
41 }
42
43 pub fn as_str(&self) -> &str {
45 &self.0
46 }
47}
48
49impl fmt::Debug for SecretProviderId {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.debug_tuple("SecretProviderId").field(&self.0).finish()
52 }
53}
54
55#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub struct SecretName(CheetahString);
58
59impl SecretName {
60 pub fn new(value: impl Into<CheetahString>) -> Result<Self, SecurityContractViolation> {
67 let value = value.into();
68 validate_identifier(&value)?;
69 Ok(Self(value))
70 }
71
72 pub fn as_str(&self) -> &str {
74 &self.0
75 }
76}
77
78impl fmt::Debug for SecretName {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.write_str("SecretName([REDACTED])")
81 }
82}
83
84fn validate_identifier(value: &str) -> Result<(), SecurityContractViolation> {
85 if value.is_empty() {
86 return Err(SecurityContractViolation::InvalidIdentifier {
87 rule: SecurityIdentifierRule::NotEmpty,
88 });
89 }
90 if value.len() > 128 {
91 return Err(SecurityContractViolation::InvalidIdentifier {
92 rule: SecurityIdentifierRule::MaximumLength,
93 });
94 }
95 if !value
96 .bytes()
97 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
98 || value == "."
99 || value == ".."
100 || value.starts_with('.')
101 {
102 return Err(SecurityContractViolation::InvalidIdentifier {
103 rule: SecurityIdentifierRule::CanonicalCharacters,
104 });
105 }
106 Ok(())
107}
108
109pub struct SecretMaterial(Vec<u8>);
111
112impl SecretMaterial {
113 pub fn new(value: Vec<u8>) -> Result<Self, SecurityContractViolation> {
120 if value.is_empty() {
121 return Err(SecurityContractViolation::EmptySecretMaterial);
122 }
123 Ok(Self(value))
124 }
125
126 pub fn expose_secret(&self) -> &[u8] {
128 &self.0
129 }
130
131 pub fn len(&self) -> usize {
133 self.0.len()
134 }
135
136 pub fn is_empty(&self) -> bool {
138 self.0.is_empty()
139 }
140}
141
142impl fmt::Debug for SecretMaterial {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 f.write_str("[REDACTED]")
145 }
146}
147
148impl Zeroize for SecretMaterial {
149 fn zeroize(&mut self) {
150 self.0.zeroize();
151 }
152}
153
154impl Drop for SecretMaterial {
155 fn drop(&mut self) {
156 self.zeroize();
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
162pub struct SecretVersion(u64);
163
164impl SecretVersion {
165 pub const fn new(value: u64) -> Self {
167 Self(value)
168 }
169
170 pub const fn get(self) -> u64 {
172 self.0
173 }
174}
175
176pub struct VersionedSecret {
178 material: SecretMaterial,
179 version: Option<SecretVersion>,
180}
181
182impl VersionedSecret {
183 pub fn new(material: SecretMaterial, version: Option<SecretVersion>) -> Self {
185 Self { material, version }
186 }
187
188 pub fn material(&self) -> &SecretMaterial {
190 &self.material
191 }
192
193 pub fn into_material(self) -> SecretMaterial {
195 self.material
196 }
197
198 pub const fn version(&self) -> Option<SecretVersion> {
200 self.version
201 }
202}
203
204impl fmt::Debug for VersionedSecret {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 f.debug_struct("VersionedSecret")
207 .field("material", &"[REDACTED]")
208 .field("version", &self.version)
209 .finish()
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum SecretAccess {
216 ReadOnly,
218 ReadWrite,
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum SecretPersistence {
225 ProcessEnvironment,
227 EncryptedLocalStorage,
229 ExternalService,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub enum SecretVersioning {
236 Unversioned,
238 Optimistic,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub struct SecretProviderCapabilities {
245 access: SecretAccess,
246 persistence: SecretPersistence,
247 versioning: SecretVersioning,
248}
249
250impl SecretProviderCapabilities {
251 pub const fn new(access: SecretAccess, persistence: SecretPersistence, versioning: SecretVersioning) -> Self {
253 Self {
254 access,
255 persistence,
256 versioning,
257 }
258 }
259
260 pub const fn access(self) -> SecretAccess {
262 self.access
263 }
264
265 pub const fn persistence(self) -> SecretPersistence {
267 self.persistence
268 }
269
270 pub const fn versioning(self) -> SecretVersioning {
272 self.versioning
273 }
274}
275
276pub trait SecretProvider: Send + Sync {
278 fn id(&self) -> &SecretProviderId;
280
281 fn capabilities(&self) -> SecretProviderCapabilities;
283
284 fn read(&self, name: &SecretName) -> Result<VersionedSecret, SecurityProviderError>;
290
291 fn write(
298 &self,
299 name: &SecretName,
300 material: SecretMaterial,
301 expected_version: Option<SecretVersion>,
302 ) -> Result<SecretVersion, SecurityProviderError>;
303}
304
305#[cfg(test)]
306mod tests {
307 use zeroize::Zeroize;
308
309 use super::*;
310
311 #[test]
312 fn identifiers_reject_traversal_and_separators() {
313 for value in ["", ".", "..", ".hidden", "a/b", "a\\b", "a:b", "a b"] {
314 assert!(SecretName::new(value).is_err(), "accepted {value:?}");
315 }
316 assert_eq!(
317 SecretName::new("broker-admin_key.1").unwrap().as_str(),
318 "broker-admin_key.1"
319 );
320 }
321
322 #[test]
323 fn material_and_name_debug_are_redacted() {
324 let material = SecretMaterial::new(b"visible-only-through-explicit-access".to_vec()).unwrap();
325 let name = SecretName::new("cluster-private-key").unwrap();
326 assert_eq!(format!("{material:?}"), "[REDACTED]");
327 assert_eq!(format!("{name:?}"), "SecretName([REDACTED])");
328 }
329
330 #[test]
331 fn explicit_zeroize_clears_material() {
332 let mut material = SecretMaterial::new(vec![7; 32]).unwrap();
333 material.zeroize();
334 assert!(material.is_empty());
335 }
336}