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}