Skip to main content

age_setup/
generator.rs

1//! Key pair generation.
2//!
3//! This module provides [`build_keypair`], the primary function for generating
4//! a fresh X25519 key pair suitable for use with the `age` encryption tool.
5//! The generation uses cryptographically secure randomness provided by the
6//! operating system.
7
8use crate::errors::Result;
9use crate::keypair::KeyPair;
10use crate::public_key::PublicKey;
11use crate::secret_key::SecretKey;
12use age::secrecy::ExposeSecret;
13use age::x25519::Identity;
14
15/// Generates a new age X25519 key pair.
16///
17/// This is the recommended way to create a [`KeyPair`]. It performs the following
18/// steps **securely and automatically**:
19///
20/// 1. **Generate a fresh identity** using the `age` crate. The identity is
21///    created with randomness sourced from the operating system's secure
22///    random number generator (e.g. `/dev/urandom` on Linux, `getrandom`).
23/// 2. **Extract the public and secret halves** from the identity. The secret
24///    is temporarily exposed in a local variable which is immediately moved
25///    into a [`SecretKey`] that guarantees zeroization on drop.
26/// 3. **Validate both keys** – the public key is checked for the `"age1"`
27///    prefix, the secret key for `"AGE-SECRET-KEY-1"`. This step acts as a
28///    safety net; because the strings originate from the `age` crate they are
29///    expected to be valid, but the check catches potential internal bugs
30///    early.
31/// 4. **Assemble the [`KeyPair`]** and return it to the caller.
32///
33/// The entire operation is **infallible** in practice – [`Identity::generate`]
34/// does not return a `Result`. The only possible failures are the validation
35/// steps, which would indicate a serious bug in the `age` library or this
36/// crate.
37///
38/// # Returns
39///
40/// * `Ok(KeyPair)` – a newly generated key pair ready for encryption and
41///   decryption.
42/// * `Err(Error::Validation(...))` – if the generated key strings fail the
43///   prefix checks (should never happen in practice).
44///
45/// # Security properties
46///
47/// * The secret key is automatically **zeroized** when the `KeyPair` (or its
48///   `SecretKey` field) is dropped. No additional cleanup is needed.
49/// * The function itself holds the raw secret string for a minimal amount of
50///   time; it is moved directly into a [`Zeroizing`]-backed container.
51/// * The randomness source is the same as the one used by the `age` CLI tool
52///   and is suitable for production use.
53///
54/// # Examples
55///
56/// ```rust
57/// use age_setup::build_keypair;
58///
59/// let kp = build_keypair()?;
60/// assert!(kp.public.expose().starts_with("age1"));
61/// assert!(kp.secret.expose_secret().starts_with("AGE-SECRET-KEY-1"));
62/// # Ok::<(), age_setup::Error>(())
63/// ```
64#[must_use = "generating a key pair is an expensive operation; consider reusing the result"]
65pub fn build_keypair() -> Result<KeyPair> {
66    // 1. Generate a fresh X25519 identity
67    let identity = Identity::generate();
68
69    // 2. Obtain the public recipient string (age1...)
70    let recipient = identity.to_public();
71    let public_raw = recipient.to_string();
72
73    // 3. Obtain the secret key string (AGE-SECRET-KEY-1...)
74    //    `expose_secret()` returns a reference; we clone it into a new String.
75    let secret_raw = identity.to_string().expose_secret().to_string();
76
77    // 4. Validate and wrap in our secure types
78    let public = PublicKey::new(public_raw)?;
79    let secret = SecretKey::new(secret_raw)?;
80
81    // 5. Return the key pair (secret_raw has been moved into `secret` and will be zeroized)
82    Ok(KeyPair::new(public, secret))
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    /// A freshly generated key pair must pass our prefix checks.
90    #[test]
91    fn generated_keypair_has_valid_format() {
92        let kp = build_keypair().unwrap();
93        assert!(kp.public.expose().starts_with("age1"));
94        assert!(kp.secret.expose_secret().starts_with("AGE-SECRET-KEY-1"));
95    }
96
97    /// Two consecutive calls must produce distinct key pairs
98    /// (i.e., randomness is actually random).
99    #[test]
100    fn generated_keypairs_are_random() {
101        let kp1 = build_keypair().unwrap();
102        let kp2 = build_keypair().unwrap();
103        assert_ne!(kp1.public.expose(), kp2.public.expose());
104        assert_ne!(kp1.secret.expose_secret(), kp2.secret.expose_secret());
105    }
106
107    /// The secret key must be redacted in Debug output.
108    #[test]
109    fn secret_is_not_leaked() {
110        let kp = build_keypair().unwrap();
111        let debug = format!("{:?}", kp);
112        assert!(!debug.contains(kp.secret.expose_secret()));
113    }
114
115    /// The public and secret keys must have more than just the prefix.
116    #[test]
117    fn keys_have_body_after_prefix() {
118        let kp = build_keypair().unwrap();
119        assert!(kp.public.expose().len() > "age1".len());
120        assert!(kp.secret.expose_secret().len() > "AGE-SECRET-KEY-1".len());
121    }
122}