uselesskey_core/srp/
seed.rs1use alloc::string::String;
8use rand_chacha10::ChaCha20Rng;
9use rand_core10::{Rng, SeedableRng};
10
11#[derive(Clone, Copy, Eq, PartialEq, Hash)]
13pub struct Seed(pub(crate) [u8; 32]);
14
15impl Seed {
16 pub fn new(bytes: [u8; 32]) -> Self {
18 Self(bytes)
19 }
20
21 pub fn bytes(&self) -> &[u8; 32] {
23 &self.0
24 }
25
26 pub fn from_text(text: &str) -> Self {
32 Self(*blake3::hash(text.as_bytes()).as_bytes())
33 }
34
35 pub fn fill_bytes(&self, dest: &mut [u8]) {
40 let mut rng = ChaCha20Rng::from_seed(self.0);
41 rng.fill_bytes(dest);
42 }
43
44 pub fn from_env_value(value: &str) -> Result<Self, String> {
50 let v = value.trim();
51
52 if let Some(hex) = hex_seed_candidate(v) {
53 return parse_hex_32(hex).map(Self);
54 }
55
56 Ok(Self::from_text(v))
57 }
58}
59
60fn hex_seed_candidate(value: &str) -> Option<&str> {
61 let hex = value
62 .strip_prefix("0x")
63 .or_else(|| value.strip_prefix("0X"))
64 .unwrap_or(value);
65
66 (hex.len() == 64).then_some(hex)
67}
68
69impl core::fmt::Debug for Seed {
70 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71 f.write_str("Seed(**redacted**)")
72 }
73}
74
75fn parse_hex_32(hex: &str) -> Result<[u8; 32], String> {
76 fn val(c: u8) -> Option<u8> {
77 match c {
78 b'0'..=b'9' => Some(c - b'0'),
79 b'a'..=b'f' => Some(c - b'a' + 10),
80 b'A'..=b'F' => Some(c - b'A' + 10),
81 _ => None,
82 }
83 }
84
85 if hex.len() != 64 {
86 return Err(alloc::format!("expected 64 hex chars, got {}", hex.len()));
87 }
88
89 let bytes = hex.as_bytes();
90 let mut out = [0u8; 32];
91
92 for (i, chunk) in bytes.chunks_exact(2).enumerate() {
93 let hi = val(chunk[0])
94 .ok_or_else(|| alloc::format!("invalid hex char: {}", chunk[0] as char))?;
95 let lo = val(chunk[1])
96 .ok_or_else(|| alloc::format!("invalid hex char: {}", chunk[1] as char))?;
97 out[i] = (hi << 4) | lo;
98 }
99
100 Ok(out)
101}
102
103#[cfg(all(test, feature = "std"))]
104mod tests {
105 use super::{Seed, parse_hex_32};
106
107 #[test]
108 fn seed_debug_is_redacted() {
109 let seed = Seed::new([7u8; 32]);
110 assert_eq!(format!("{:?}", seed), "Seed(**redacted**)");
111 }
112
113 #[test]
114 fn parse_hex_32_rejects_wrong_length() {
115 let err = parse_hex_32("abcd").unwrap_err();
116 assert!(err.contains("expected 64 hex chars"));
117 }
118
119 #[test]
120 fn parse_hex_32_rejects_invalid_char() {
121 let mut s = "0".repeat(64);
122 s.replace_range(10..11, "g");
123
124 let err = parse_hex_32(&s).unwrap_err();
125 assert!(err.contains("invalid hex char"));
126 }
127
128 #[test]
129 fn seed_from_env_value_parses_hex_with_prefix_and_whitespace() {
130 let hex = "0x0000000000000000000000000000000000000000000000000000000000000001";
131 let seed = Seed::from_env_value(&format!(" {hex} ")).unwrap();
132 assert_eq!(seed.bytes()[31], 1);
133 assert!(seed.bytes()[..31].iter().all(|b| *b == 0));
134 }
135
136 #[test]
137 fn seed_from_env_value_parses_uppercase_0x_prefix() {
138 let hex = "0X0000000000000000000000000000000000000000000000000000000000000001";
139 let seed = Seed::from_env_value(hex).unwrap();
140 assert_eq!(seed.bytes()[31], 1);
141 assert!(seed.bytes()[..31].iter().all(|b| *b == 0));
142 }
143
144 #[test]
145 fn seed_from_env_value_parses_uppercase_hex() {
146 let hex = "F".repeat(64);
147 let seed = Seed::from_env_value(&hex).unwrap();
148 assert!(seed.bytes().iter().all(|b| *b == 0xFF));
149 }
150
151 #[test]
152 fn string_seed_is_hashed_with_blake3() {
153 let seed = Seed::from_env_value(" deterministic-seed-value ").unwrap();
154 let expected = blake3::hash("deterministic-seed-value".as_bytes());
155 assert_eq!(seed.bytes(), expected.as_bytes());
156 }
157
158 #[test]
159 fn from_text_hashes_verbatim_input() {
160 let text = " deterministic-seed-value ";
161 let seed = Seed::from_text(text);
162 let expected = blake3::hash(text.as_bytes());
163 assert_eq!(seed.bytes(), expected.as_bytes());
164 assert_ne!(seed, Seed::from_env_value(text).unwrap());
165 }
166
167 #[test]
168 fn from_text_does_not_parse_hex_shaped_strings() {
169 let text = "ab".repeat(32);
170 let seed = Seed::from_text(&text);
171 let expected = blake3::hash(text.as_bytes());
172 assert_eq!(seed.bytes(), expected.as_bytes());
173 assert_ne!(seed, Seed::from_env_value(&text).unwrap());
174 }
175
176 #[test]
177 fn parse_hex_32_lowercase_valid() {
178 let hex = "aa".repeat(32);
179 let result = parse_hex_32(&hex).unwrap();
180 assert!(result.iter().all(|b| *b == 0xAA));
181 }
182
183 #[test]
184 fn parse_hex_32_mixed_case_valid() {
185 let hex = "aAbBcCdDeEfF".repeat(5);
186 let hex = format!("{hex}0000");
188 assert_eq!(hex.len(), 64);
189 assert!(parse_hex_32(&hex).is_ok());
190 }
191
192 #[test]
193 fn parse_hex_32_invalid_lo_nibble() {
194 let mut hex = "0".repeat(64);
196 hex.replace_range(1..2, "z");
197 let err = parse_hex_32(&hex).unwrap_err();
198 assert!(err.contains("invalid hex char: z"));
199 }
200
201 #[test]
202 fn seed_equality_and_clone() {
203 let a = Seed::new([42u8; 32]);
204 let b = a;
205 assert_eq!(a, b);
206 assert_eq!(a.bytes(), b.bytes());
207 }
208
209 #[test]
210 fn seed_inequality() {
211 let a = Seed::new([1u8; 32]);
212 let b = Seed::new([2u8; 32]);
213 assert_ne!(a, b);
214 }
215
216 #[test]
217 fn seed_hash_consistent() {
218 use core::hash::{Hash, Hasher};
219 let seed = Seed::new([99u8; 32]);
220
221 let mut h1 = std::collections::hash_map::DefaultHasher::new();
222 seed.hash(&mut h1);
223 let hash1 = h1.finish();
224
225 let mut h2 = std::collections::hash_map::DefaultHasher::new();
226 seed.hash(&mut h2);
227 assert_eq!(hash1, h2.finish());
228 }
229
230 #[test]
231 fn fill_bytes_is_seed_stable() {
232 let seed = Seed::new([7u8; 32]);
233 let mut a = [0u8; 16];
234 let mut b = [0u8; 16];
235
236 seed.fill_bytes(&mut a);
237 seed.fill_bytes(&mut b);
238
239 assert_eq!(a, b);
240 }
241
242 #[test]
243 fn fill_bytes_overwrites_destination_buffer() {
244 let seed = Seed::new([7u8; 32]);
245 let mut out = [0xAA; 16];
246
247 seed.fill_bytes(&mut out);
248
249 assert_ne!(out, [0xAA; 16]);
250 }
251
252 #[test]
253 fn from_env_value_short_string_uses_blake3() {
254 let seed = Seed::from_env_value("abc").unwrap();
255 let expected = blake3::hash(b"abc");
256 assert_eq!(seed.bytes(), expected.as_bytes());
257 }
258
259 #[test]
260 fn from_env_value_63_char_non_hex_uses_blake3() {
261 let input = "a".repeat(63);
263 let seed = Seed::from_env_value(&input).unwrap();
264 let expected = blake3::hash(input.as_bytes());
265 assert_eq!(seed.bytes(), expected.as_bytes());
266 }
267
268 #[test]
269 fn from_env_value_65_char_non_hex_uses_blake3() {
270 let input = "a".repeat(65);
272 let seed = Seed::from_env_value(&input).unwrap();
273 let expected = blake3::hash(input.as_bytes());
274 assert_eq!(seed.bytes(), expected.as_bytes());
275 }
276
277 #[test]
278 fn from_env_value_short_0x_prefixed_string_hashes_original_input() -> Result<(), String> {
279 let input = "0xabc";
280 let seed = Seed::from_env_value(input)?;
281 let expected = blake3::hash(input.as_bytes());
282 assert_eq!(seed.bytes(), expected.as_bytes());
283 Ok(())
284 }
285
286 #[test]
287 fn from_env_value_invalid_length_0x_prefixed_hex_hashes_original_input() -> Result<(), String> {
288 let input = format!("0x{}", "a".repeat(63));
289 let seed = Seed::from_env_value(&input)?;
290 let expected = blake3::hash(input.as_bytes());
291 assert_eq!(seed.bytes(), expected.as_bytes());
292 Ok(())
293 }
294
295 #[test]
296 fn from_env_value_64_char_invalid_hex_returns_error() {
297 let input = "g".repeat(64);
299 assert!(Seed::from_env_value(&input).is_err());
300 }
301}