Skip to main content

age_setup/
secret_key.rs

1use crate::errors::{Error, Result, ValidationError};
2use std::fmt;
3use zeroize::Zeroizing;
4
5/// A zeroizing age secret key.
6///
7/// Wraps a secret key string inside [`Zeroizing`], guaranteeing that the
8/// underlying memory is cleared when the `SecretKey` is dropped. The key must
9/// start with the standard age secret key prefix `"AGE-SECRET-KEY-1"`.
10///
11/// The [`Debug`] and [`Display`] implementations intentionally redact the
12/// actual value to prevent accidental leakage in logs or error messages.
13///
14/// # Invariants
15///
16/// * The inner string is never empty.
17/// * The inner string always starts with `"AGE-SECRET-KEY-1"`.
18/// * Memory is zeroized on drop via [`Zeroizing`].
19///
20/// # Examples
21///
22/// ```rust
23/// use age_setup::SecretKey;
24///
25/// let sk = SecretKey::new("AGE-SECRET-KEY-1ABCDEF".into())?;
26/// // The debug representation hides the actual value.
27/// assert_eq!(format!("{:?}", sk), "SecretKey { value: \"[REDACTED]\" }");
28/// # Ok::<(), age_setup::Error>(())
29/// ```
30///
31/// # See Also
32///
33/// * [`PublicKey`](crate::PublicKey) – The corresponding public key wrapper.
34/// * [`KeyPair`](crate::KeyPair) – Container holding both keys.
35#[derive(Clone)]
36pub struct SecretKey {
37    inner: Zeroizing<String>,
38}
39
40impl SecretKey {
41    /// Creates a new `SecretKey` after validating the age secret key prefix.
42    ///
43    /// The provided `raw` string must start with `"AGE-SECRET-KEY-1"` and must
44    /// not be empty.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`Error::Validation`](crate::Error::Validation) with
49    /// [`ValidationError::InvalidSecretKeyFormat`](crate::ValidationError::InvalidSecretKeyFormat)
50    /// if the key is empty or does not start with the required prefix.
51    ///
52    /// # Examples
53    ///
54    /// ```rust
55    /// use age_setup::SecretKey;
56    ///
57    /// assert!(SecretKey::new("AGE-SECRET-KEY-1VALID".into()).is_ok());
58    /// assert!(SecretKey::new("bad".into()).is_err());
59    /// assert!(SecretKey::new("".into()).is_err());
60    /// ```
61    pub fn new(raw: String) -> Result<Self> {
62        if raw.is_empty() {
63            return Err(Error::from(ValidationError::invalid_secret_key(
64                "Secret key is empty",
65            )));
66        }
67        if !raw.starts_with("AGE-SECRET-KEY-1") {
68            return Err(Error::from(ValidationError::invalid_secret_key(
69                "Secret key must start with 'AGE-SECRET-KEY-1'",
70            )));
71        }
72        Ok(Self {
73            inner: Zeroizing::new(raw),
74        })
75    }
76
77    /// Returns a reference to the underlying secret key string.
78    ///
79    /// Use this only when the secret must be passed to another API. Prefer
80    /// to keep the `SecretKey` in scope and avoid unnecessary copies.
81    ///
82    /// # Examples
83    ///
84    /// ```rust
85    /// use age_setup::SecretKey;
86    ///
87    /// let sk = SecretKey::new("AGE-SECRET-KEY-1SECRET".into())?;
88    /// assert_eq!(sk.expose_secret(), "AGE-SECRET-KEY-1SECRET");
89    /// # Ok::<(), age_setup::Error>(())
90    /// ```
91    #[must_use]
92    pub fn expose_secret(&self) -> &str {
93        &self.inner
94    }
95}
96
97impl fmt::Debug for SecretKey {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.debug_struct("SecretKey")
100            .field("value", &"[REDACTED]")
101            .finish()
102    }
103}
104
105impl fmt::Display for SecretKey {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(f, "[REDACTED]")
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn valid() {
117        let sk = SecretKey::new("AGE-SECRET-KEY-1TEST".into()).unwrap();
118        assert_eq!(sk.expose_secret(), "AGE-SECRET-KEY-1TEST");
119    }
120
121    #[test]
122    fn debug_redacted() {
123        let sk = SecretKey::new("AGE-SECRET-KEY-1TEST".into()).unwrap();
124        let d = format!("{:?}", sk);
125        assert!(d.contains("[REDACTED]"));
126        assert!(!d.contains("TEST"));
127    }
128}