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 pub fn new(s: impl Into<String>) -> Self {
28 Self(Zeroizing::new(s.into()))
29 }
30
31 #[must_use]
32 pub fn expose(&self) -> &str {
33 self.0.as_str()
34 }
35}
36
37impl fmt::Debug for Secret {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 f.write_str("[REDACTED]")
40 }
41}
42
43impl fmt::Display for Secret {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str("[REDACTED]")
46 }
47}
48
49/// Error type for vault operations.
50///
51/// Returned by `VaultProvider::get_secret` on failure.
52///
53/// The `Backend(String)` variant is the escape hatch for third-party vault implementations:
54/// format the underlying error into the `String` when no more specific variant applies.
55#[derive(Debug, thiserror::Error)]
56pub enum VaultError {
57 #[error("secret not found: {0}")]
58 NotFound(String),
59 /// Generic backend failure. Third-party vault implementors should use this variant
60 /// to surface errors that do not fit `NotFound` or `Io`.
61 #[error("vault backend error: {0}")]
62 Backend(String),
63 #[error("vault I/O error: {0}")]
64 Io(#[from] std::io::Error),
65}