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}