Skip to main content

age_setup/
secret_key.rs

1//! Age secret key type.
2//!
3//! This module provides the [`SecretKey`] type, a validated, memory‑safe
4//! wrapper around an age secret key string (starting with `AGE-SECRET-KEY-1`).
5//! The type is the private half of an [`KeyPair`](crate::KeyPair) and is
6//! designed to keep the secret confidential:
7//!
8//! - It uses [`zeroize::Zeroizing`] internally to overwrite memory on drop.
9//! - Its [`Display`] and [`Debug`] implementations intentionally redact the
10//!   key material, printing `[REDACTED]` instead.
11//!
12//! # Accessing the secret
13//!
14//! To obtain the raw key string (e.g., for writing to a file or passing to
15//! an age encryption function), call [`expose_secret`](SecretKey::expose_secret).
16//! Do so **only when necessary** and ensure the returned reference is not
17//! copied, logged, or leaked accidentally.
18
19use crate::errors::{Error, Result, ValidationError};
20use std::fmt;
21use zeroize::Zeroizing;
22
23/// A validated age secret key protected by memory zeroization.
24///
25/// `SecretKey` wraps the raw key string inside [`Zeroizing`], which guarantees
26/// that the memory is securely erased when the value is dropped. This prevents
27/// secrets from lingering in memory dumps or swap files.
28///
29/// # Validation
30///
31/// The key is validated at construction time via [`new`](SecretKey::new):
32/// - It must be non‑empty.
33/// - It must start with the string `AGE-SECRET-KEY-1` (case‑sensitive).
34///
35/// # Security properties
36///
37/// - **Redacted display** – `Display` and `Debug` print `[REDACTED]`, never
38///   the actual key.
39/// - **Zeroization on drop** – memory is overwritten with zeros when the
40///   `SecretKey` (or any clone) is dropped.
41/// - **Cloneable** – cloning creates a new independent `Zeroizing` copy that
42///   is also zeroized separately.
43///
44/// # Examples
45///
46/// ```rust
47/// use age_setup::SecretKey;
48///
49/// let sk = SecretKey::new("AGE-SECRET-KEY-1mytestkey".into())?;
50/// println!("{}", sk);                       // prints: [REDACTED]
51/// println!("{:?}", sk);                     // prints: SecretKey { ... [REDACTED] ... }
52/// let raw = sk.expose_secret();             // careful: raw secret exposed
53/// # Ok::<(), age_setup::Error>(())
54/// ```
55#[derive(Clone)]
56pub struct SecretKey {
57    inner: Zeroizing<String>,
58}
59
60impl SecretKey {
61    /// Creates a new `SecretKey` after validating the raw string.
62    ///
63    /// # Validation checks
64    ///
65    /// 1. The key must not be empty.
66    /// 2. The key must start with `"AGE-SECRET-KEY-1"`.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`Error::Validation`](crate::Error::Validation) with a
71    /// descriptive reason if any check fails.
72    ///
73    /// # Examples
74    ///
75    /// ```rust
76    /// # use age_setup::SecretKey;
77    /// let valid = SecretKey::new("AGE-SECRET-KEY-1abc".into()).unwrap();
78    ///
79    /// let empty = SecretKey::new("".into());
80    /// assert!(empty.is_err());
81    ///
82    /// let wrong_prefix = SecretKey::new("ssh-rsa ...".into());
83    /// assert!(wrong_prefix.is_err());
84    /// ```
85    pub fn new(raw: String) -> Result<Self> {
86        if raw.is_empty() {
87            return Err(Error::from(ValidationError::invalid_secret_key(
88                "Secret key is empty",
89            )));
90        }
91        if !raw.starts_with("AGE-SECRET-KEY-1") {
92            return Err(Error::from(ValidationError::invalid_secret_key(
93                "Secret key must start with 'AGE-SECRET-KEY-1'",
94            )));
95        }
96        Ok(Self {
97            inner: Zeroizing::new(raw),
98        })
99    }
100
101    /// Exposes the raw secret key string.
102    ///
103    /// ⚠️ **Security Warning** – this method returns the actual secret material
104    /// as a `&str`. Only use it when absolutely necessary (e.g., to pass the
105    /// key to an age decryption function or to write it to a securely
106    /// permissioned file). Avoid logging, printing, or storing the returned
107    /// string in an unsecured location.
108    ///
109    /// # Examples
110    ///
111    /// ```rust
112    /// # use age_setup::SecretKey;
113    /// let sk = SecretKey::new("AGE-SECRET-KEY-1test".into()).unwrap();
114    /// let raw = sk.expose_secret();
115    /// assert_eq!(raw, "AGE-SECRET-KEY-1test");
116    /// ```
117    #[must_use]
118    pub fn expose_secret(&self) -> &str {
119        &self.inner
120    }
121}
122
123/// The `Debug` implementation records the secret value as `[REDACTED]` to
124/// prevent accidental leakage through debug output.
125impl fmt::Debug for SecretKey {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        f.debug_struct("SecretKey")
128            .field("value", &"[REDACTED]")
129            .finish()
130    }
131}
132
133/// The `Display` implementation always writes `[REDACTED]`, never the actual
134/// key. Use [`expose_secret`](SecretKey::expose_secret) if you need the raw
135/// string.
136impl fmt::Display for SecretKey {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "[REDACTED]")
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn valid() {
148        let sk = SecretKey::new("AGE-SECRET-KEY-1TEST".into()).unwrap();
149        assert_eq!(sk.expose_secret(), "AGE-SECRET-KEY-1TEST");
150    }
151
152    /// Confirm that `Debug` and `Display` never contain the secret.
153    #[test]
154    fn debug_redacted() {
155        let sk = SecretKey::new("AGE-SECRET-KEY-1TEST".into()).unwrap();
156        let d = format!("{:?}", sk);
157        assert!(d.contains("[REDACTED]"));
158        assert!(!d.contains("TEST"));
159    }
160}