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}