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