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