age_setup/validation.rs
1//! Lightweight validation for age public keys.
2//!
3//! This module provides a **non‑cryptographic** sanity check for age public key
4//! strings. It is used internally (`pub(crate)`) to catch obvious programmer
5//! mistakes early, before the key is actually used in cryptographic operations.
6
7use crate::errors::{Error, Result, ValidationError};
8
9/// Verifies that a string is non‑empty and starts with the age public key prefix `"age1"`.
10///
11/// This is a **lightweight, internal sanity check** that ensures a candidate public
12/// key adheres to the most basic requirement of the [age specification]. It is called
13/// before a raw string is wrapped inside [`PublicKey`].
14///
15/// # Checks performed
16///
17/// 1. **Non‑emptiness** – an empty string is rejected immediately.
18/// 2. **Prefix** – the string must begin with `"age1"`. The comparison is
19/// case‑sensitive, so `"AGE1..."` is rejected.
20///
21/// No further validation (length, Bech32 character set, checksum, or curve point)
22/// is performed here. Full parsing is the responsibility of the upstream `age` crate.
23///
24/// # Why only a prefix check?
25///
26/// Parsing a real Bech32‑encoded age key is complex and already handled by the `age`
27/// library. This minimal check catches common programmer errors (e.g., passing a
28/// filename instead of a key) early and with a clear error message.
29///
30/// # Parameters
31///
32/// * `key` – A string slice (`&str`) representing the raw public key to validate.
33///
34/// # Returns
35///
36/// * `Ok(())` if the key is non‑empty and starts with `"age1"`.
37/// * `Err(Error::Validation(ValidationError::InvalidPublicKeyFormat { ... }))` if
38/// either condition is violated.
39///
40/// # Errors
41///
42/// | Condition | Error message |
43/// |-----------|---------------|
44/// | `key.is_empty()` | `"Key is empty"` |
45/// | `key` does not start with `"age1"` | `"Key must start with 'age1', got: <prefix>…"` |
46///
47/// When the prefix is wrong, the error message includes the first few characters of
48/// the supplied key (up to 10). The truncation is safe for short strings and will not
49/// panic.
50///
51/// # Security note
52///
53/// **This function does not guarantee that the key is a valid age recipient.**
54/// Malicious or malformed keys that still begin with `"age1"` will pass this check.
55/// The actual cryptographic validation happens inside the `age` crate when the key is
56/// used for encryption.
57///
58/// # Examples
59///
60/// ```rust,ignore
61/// use age_setup::validation::validate_age_prefix;
62///
63/// // ✅ Valid keys
64/// assert!(validate_age_prefix("age1abcdef").is_ok());
65/// assert!(validate_age_prefix("age1").is_ok()); // minimal valid input
66///
67/// // ❌ Invalid keys
68/// let empty = validate_age_prefix("");
69/// assert!(empty.is_err());
70///
71/// let wrong = validate_age_prefix("ssh-rsa AAAA...");
72/// let err_msg = wrong.unwrap_err().to_string();
73/// assert_eq!(
74/// err_msg,
75/// "Key must start with 'age1', got: ssh-rsa AA"
76/// );
77/// ```
78///
79/// # See also
80///
81/// * [`PublicKey::new`](crate::public_key::PublicKey::new) – uses this validator.
82/// * [age specification](https://github.com/FiloSottile/age) – formal documentation.
83pub(crate) fn validate_age_prefix(key: &str) -> Result<()> {
84 if key.is_empty() {
85 return Err(Error::from(ValidationError::invalid_public_key(
86 "Key is empty",
87 )));
88 }
89 if !key.starts_with("age1") {
90 return Err(Error::from(ValidationError::invalid_public_key(format!(
91 "Key must start with 'age1', got: {}",
92 &key[..key.len().min(10)]
93 ))));
94 }
95 Ok(())
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::errors::Error;
102
103 #[test]
104 fn empty() {
105 let e = validate_age_prefix("").unwrap_err();
106 assert!(matches!(e, Error::Validation(_)));
107 }
108
109 #[test]
110 fn wrong_prefix() {
111 let e = validate_age_prefix("xxx").unwrap_err();
112 let msg = format!("{}", e);
113 assert!(msg.contains("must start with 'age1'"));
114 }
115
116 #[test]
117 fn valid() {
118 assert!(validate_age_prefix("age1abc").is_ok());
119 }
120}