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