secrets_core/
generators.rs1use crate::errors::{Error, Result};
12use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
13use greentic_secrets_spec::GeneratedSecretRequirement;
14use greentic_types::secrets::SecretFormat;
15use rand::{Rng, RngExt};
16
17const RAW_TEXT_ALPHABET: &[u8] =
19 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
20
21const MAX_GENERATED_LENGTH: usize = 4096;
27
28pub fn generate_secret_value(
36 generated: &GeneratedSecretRequirement,
37) -> Result<(Vec<u8>, SecretFormat)> {
38 if !generated.policy.eq_ignore_ascii_case("random") {
39 return Err(Error::Invalid(
40 "generated secret policy".to_string(),
41 generated.policy.clone(),
42 ));
43 }
44 if generated.length > MAX_GENERATED_LENGTH {
45 return Err(Error::Invalid(
46 "generated secret length".to_string(),
47 format!(
48 "{} exceeds the maximum supported length of {MAX_GENERATED_LENGTH}",
49 generated.length
50 ),
51 ));
52 }
53 let length = generated.length.max(1);
54 let text = match generated.encoding.as_str() {
55 "raw_text" => random_ascii(length),
56 "base64url" => URL_SAFE_NO_PAD.encode(random_bytes(length)),
57 "hex" => hex_encode(&random_bytes(length)),
58 other => {
59 return Err(Error::Invalid(
60 "generated secret encoding".to_string(),
61 other.to_string(),
62 ));
63 }
64 };
65 Ok((text.into_bytes(), SecretFormat::Text))
66}
67
68fn random_ascii(length: usize) -> String {
69 let mut rng = rand::rng();
70 let mut out = String::with_capacity(length);
71 for _ in 0..length {
72 let idx = rng.random_range(0..RAW_TEXT_ALPHABET.len());
73 out.push(RAW_TEXT_ALPHABET[idx] as char);
74 }
75 out
76}
77
78fn random_bytes(len: usize) -> Vec<u8> {
79 let mut buffer = vec![0u8; len];
80 rand::rng().fill_bytes(&mut buffer);
81 buffer
82}
83
84fn hex_encode(bytes: &[u8]) -> String {
85 const HEX: &[u8; 16] = b"0123456789abcdef";
86 let mut out = String::with_capacity(bytes.len() * 2);
87 for &b in bytes {
88 out.push(HEX[(b >> 4) as usize] as char);
89 out.push(HEX[(b & 0x0f) as usize] as char);
90 }
91 out
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use greentic_secrets_spec::GeneratedSecretScope;
98
99 fn spec(encoding: &str, length: usize) -> GeneratedSecretRequirement {
100 GeneratedSecretRequirement {
101 policy: "random".to_string(),
102 length,
103 encoding: encoding.to_string(),
104 scope: GeneratedSecretScope {
105 level: "tenant".to_string(),
106 team: Some("_".to_string()),
107 },
108 regenerate_if_present: false,
109 }
110 }
111
112 #[test]
113 fn raw_text_has_requested_length_and_charset() {
114 let (bytes, fmt) = generate_secret_value(&spec("raw_text", 20)).unwrap();
115 assert_eq!(fmt, SecretFormat::Text);
116 assert_eq!(bytes.len(), 20);
117 assert!(bytes.iter().all(|b| RAW_TEXT_ALPHABET.contains(b)));
118 }
119
120 #[test]
121 fn hex_is_two_chars_per_byte() {
122 let (bytes, _) = generate_secret_value(&spec("hex", 16)).unwrap();
123 assert_eq!(bytes.len(), 32);
124 assert!(bytes.iter().all(|b| b.is_ascii_hexdigit()));
125 }
126
127 #[test]
128 fn base64url_decodes_to_requested_byte_count() {
129 let (b64, _) = generate_secret_value(&spec("base64url", 24)).unwrap();
130 assert_eq!(URL_SAFE_NO_PAD.decode(&b64).unwrap().len(), 24);
131 assert!(!b64.contains(&b'='));
133 assert!(!b64.contains(&b'+'));
134 assert!(!b64.contains(&b'/'));
135 }
136
137 #[test]
138 fn two_generations_differ() {
139 let (a, _) = generate_secret_value(&spec("raw_text", 20)).unwrap();
140 let (b, _) = generate_secret_value(&spec("raw_text", 20)).unwrap();
141 assert_ne!(a, b);
142 }
143
144 #[test]
145 fn zero_length_is_clamped_to_one() {
146 let (bytes, _) = generate_secret_value(&spec("raw_text", 0)).unwrap();
147 assert_eq!(bytes.len(), 1);
148 }
149
150 #[test]
151 fn unsupported_policy_and_encoding_error() {
152 let mut bad_policy = spec("raw_text", 20);
153 bad_policy.policy = "fixed".to_string();
154 assert!(matches!(
155 generate_secret_value(&bad_policy),
156 Err(Error::Invalid(_, _))
157 ));
158
159 assert!(matches!(
160 generate_secret_value(&spec("uuid", 20)),
161 Err(Error::Invalid(_, _))
162 ));
163 }
164
165 #[test]
166 fn over_max_length_is_rejected_and_boundary_is_accepted() {
167 let mut over = spec("raw_text", 20);
168 over.length = MAX_GENERATED_LENGTH + 1;
169 assert!(matches!(
170 generate_secret_value(&over),
171 Err(Error::Invalid(_, _))
172 ));
173
174 let mut at_max = spec("raw_text", 20);
176 at_max.length = MAX_GENERATED_LENGTH;
177 let (bytes, _) = generate_secret_value(&at_max).unwrap();
178 assert_eq!(bytes.len(), MAX_GENERATED_LENGTH);
179 }
180}