Skip to main content

zeph_common/
secret.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::fmt;
5
6use serde::Deserialize;
7use zeroize::Zeroizing;
8
9/// Wrapper for sensitive strings with redacted Debug/Display.
10///
11/// The inner value is wrapped in [`Zeroizing`] which overwrites the memory on drop.
12/// `Clone` is intentionally not derived — secrets must be explicitly duplicated via
13/// `Secret::new(existing.expose().to_owned())`.
14///
15/// # Clone is not implemented
16///
17/// ```compile_fail
18/// use zeph_common::secret::Secret;
19/// let s = Secret::new("x");
20/// let _ = s.clone(); // must not compile — Secret intentionally does not implement Clone
21/// ```
22#[derive(Deserialize)]
23#[serde(transparent)]
24pub struct Secret(Zeroizing<String>);
25
26impl Secret {
27    /// Create a new secret from a string-like value.
28    ///
29    /// The inner string is wrapped in [`Zeroizing`], which overwrites the memory when the
30    /// secret is dropped. This constructor is marked `#[must_use]` to encourage explicit
31    /// handling of the returned secret value rather than accidental discarding.
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use zeph_common::secret::Secret;
37    ///
38    /// let secret = Secret::new("my_api_key");
39    /// assert_eq!(secret.expose(), "my_api_key");
40    /// // Memory is zeroized when secret is dropped
41    /// ```
42    #[must_use]
43    pub fn new(s: impl Into<String>) -> Self {
44        Self(Zeroizing::new(s.into()))
45    }
46
47    /// Expose the inner secret string as a borrowed reference.
48    ///
49    /// Use this method to access the secret for API calls or comparisons. The reference
50    /// is bounded by the secret's lifetime, so the underlying string cannot be dropped
51    /// while the reference is in use. Note that the string itself is not zeroized on
52    /// reference — zeroization occurs only when the containing [`Secret`] is dropped.
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use zeph_common::secret::Secret;
58    ///
59    /// let secret = Secret::new("password123");
60    /// let exposed = secret.expose();
61    /// println!("Length: {}", exposed.len());
62    /// ```
63    #[must_use]
64    pub fn expose(&self) -> &str {
65        self.0.as_str()
66    }
67}
68
69impl fmt::Debug for Secret {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        f.write_str("[REDACTED]")
72    }
73}
74
75impl fmt::Display for Secret {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        f.write_str("[REDACTED]")
78    }
79}
80
81/// Error type for vault operations.
82///
83/// Returned by `VaultProvider::get_secret` on failure.
84///
85/// The `Backend(String)` variant is the escape hatch for third-party vault implementations:
86/// format the underlying error into the `String` when no more specific variant applies.
87#[derive(Debug, thiserror::Error)]
88#[non_exhaustive]
89pub enum VaultError {
90    #[error("secret not found: {0}")]
91    NotFound(String),
92    /// Generic backend failure. Third-party vault implementors should use this variant
93    /// to surface errors that do not fit `NotFound` or `Io`.
94    #[error("vault backend error: {0}")]
95    Backend(String),
96    #[error("vault I/O error: {0}")]
97    Io(#[from] std::io::Error),
98}