Skip to main content

rocketmq_security_api/
secret_provider.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Runtime-neutral contracts for injected secret providers.
16
17use std::fmt;
18
19use cheetah_string::CheetahString;
20use zeroize::Zeroize;
21
22use crate::SecurityContractViolation;
23use crate::SecurityIdentifierRule;
24use crate::SecurityProviderError;
25
26/// Stable identifier for an explicitly registered secret provider.
27#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct SecretProviderId(CheetahString);
29
30impl SecretProviderId {
31    /// Validates and creates a provider identifier.
32    ///
33    /// # Errors
34    ///
35    /// Returns [`SecurityContractViolation::InvalidIdentifier`] when the value
36    /// violates the closed identifier grammar.
37    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    /// Borrows this value as str.
44    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/// Validated logical secret name. The name cannot contain path separators or traversal segments.
56#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub struct SecretName(CheetahString);
58
59impl SecretName {
60    /// Validates and creates a secret name.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`SecurityContractViolation::InvalidIdentifier`] when the value
65    /// violates the closed identifier grammar.
66    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    /// Borrows this value as str.
73    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
109/// Sensitive byte material that is redacted and zeroized when dropped.
110pub struct SecretMaterial(Vec<u8>);
111
112impl SecretMaterial {
113    /// Creates non-empty secret material.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`SecurityContractViolation::EmptySecretMaterial`] when the
118    /// supplied material is empty.
119    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    /// Borrows the material for the shortest practical operation scope.
127    pub fn expose_secret(&self) -> &[u8] {
128        &self.0
129    }
130
131    /// Returns the len.
132    pub fn len(&self) -> usize {
133        self.0.len()
134    }
135
136    /// Returns whether empty.
137    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/// Version assigned by a provider supporting optimistic updates.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
162pub struct SecretVersion(u64);
163
164impl SecretVersion {
165    /// Creates a new `SecretVersion`.
166    pub const fn new(value: u64) -> Self {
167        Self(value)
168    }
169
170    /// Returns the get.
171    pub const fn get(self) -> u64 {
172        self.0
173    }
174}
175
176/// Secret value returned together with its optional provider version.
177pub struct VersionedSecret {
178    material: SecretMaterial,
179    version: Option<SecretVersion>,
180}
181
182impl VersionedSecret {
183    /// Creates a new `VersionedSecret`.
184    pub fn new(material: SecretMaterial, version: Option<SecretVersion>) -> Self {
185        Self { material, version }
186    }
187
188    /// Returns the material.
189    pub fn material(&self) -> &SecretMaterial {
190        &self.material
191    }
192
193    /// Converts this value into material.
194    pub fn into_material(self) -> SecretMaterial {
195        self.material
196    }
197
198    /// Returns the version.
199    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/// Mutation support declared by a provider.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum SecretAccess {
216    /// Represents the read only case.
217    ReadOnly,
218    /// Represents the read write case.
219    ReadWrite,
220}
221
222/// Storage class declared by a provider.
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum SecretPersistence {
225    /// Represents the process environment case.
226    ProcessEnvironment,
227    /// Represents the encrypted local storage case.
228    EncryptedLocalStorage,
229    /// Represents the external service case.
230    ExternalService,
231}
232
233/// Concurrency contract declared by a provider.
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub enum SecretVersioning {
236    /// Represents the unversioned case.
237    Unversioned,
238    /// Represents the optimistic case.
239    Optimistic,
240}
241
242/// Typed capabilities exposed before a provider is selected.
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub struct SecretProviderCapabilities {
245    access: SecretAccess,
246    persistence: SecretPersistence,
247    versioning: SecretVersioning,
248}
249
250impl SecretProviderCapabilities {
251    /// Creates a new `SecretProviderCapabilities`.
252    pub const fn new(access: SecretAccess, persistence: SecretPersistence, versioning: SecretVersioning) -> Self {
253        Self {
254            access,
255            persistence,
256            versioning,
257        }
258    }
259
260    /// Returns the access.
261    pub const fn access(self) -> SecretAccess {
262        self.access
263    }
264
265    /// Returns the persistence.
266    pub const fn persistence(self) -> SecretPersistence {
267        self.persistence
268    }
269
270    /// Returns the versioning.
271    pub const fn versioning(self) -> SecretVersioning {
272        self.versioning
273    }
274}
275
276/// Synchronous, runtime-neutral boundary implemented by injected providers.
277pub trait SecretProvider: Send + Sync {
278    /// Returns the id.
279    fn id(&self) -> &SecretProviderId;
280
281    /// Returns the capabilities.
282    fn capabilities(&self) -> SecretProviderCapabilities;
283
284    /// Reads a secret without exposing provider implementation types.
285    ///
286    /// # Errors
287    ///
288    /// Returns a redacted [`SecurityProviderError`] when the material cannot be obtained safely.
289    fn read(&self, name: &SecretName) -> Result<VersionedSecret, SecurityProviderError>;
290
291    /// Creates or atomically updates a secret. `None` is create-only; an existing secret requires
292    /// its current version to prevent lost updates.
293    ///
294    /// # Errors
295    ///
296    /// Returns a redacted [`SecurityProviderError`] when persistence or version validation fails.
297    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}