uselesskey_token/srp/
base62.rs1use rand_chacha10::ChaCha20Rng;
7use rand_core10::{Rng, SeedableRng};
8use uselesskey_core::Seed;
9
10pub const BASE62_ALPHABET: &[u8; 62] =
12 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
13
14const ACCEPT_MAX: u8 = 248; pub fn random_base62(seed: Seed, len: usize) -> String {
22 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
23 random_base62_with_rng(&mut rng, len)
24}
25
26fn random_base62_with_rng(rng: &mut impl Rng, len: usize) -> String {
27 let mut out = String::with_capacity(len);
28 let mut buf = [0u8; 64];
29
30 while out.len() < len {
31 rng.fill_bytes(&mut buf);
32 let before = out.len();
33
34 for &b in &buf {
35 if b < ACCEPT_MAX {
36 out.push(BASE62_ALPHABET[(b % 62) as usize] as char);
37 if out.len() == len {
38 break;
39 }
40 }
41 }
42
43 if out.len() == before {
44 for &b in &buf {
45 out.push(BASE62_ALPHABET[(b as usize) % 62] as char);
46 if out.len() == len {
47 break;
48 }
49 }
50 }
51 }
52
53 out
54}
55
56#[cfg(test)]
57mod tests {
58 use super::{BASE62_ALPHABET, random_base62, random_base62_with_rng};
59 use rand_chacha10::ChaCha20Rng;
60 use rand_core10::{Infallible, SeedableRng, TryRng};
61 use uselesskey_core::Seed;
62
63 #[test]
64 fn generates_requested_length() {
65 assert_eq!(random_base62(Seed::new([1u8; 32]), 0).len(), 0);
66 assert_eq!(random_base62(Seed::new([1u8; 32]), 73).len(), 73);
67 }
68
69 #[test]
70 fn uses_only_base62_chars() {
71 let value = random_base62(Seed::new([2u8; 32]), 256);
72 assert!(value.bytes().all(|b| BASE62_ALPHABET.contains(&b)));
73 }
74
75 #[test]
76 fn deterministic_for_seeded_rng() {
77 let seed = [7u8; 32];
78 let a = random_base62(Seed::new(seed), 96);
79 let b = random_base62(Seed::new(seed), 96);
80 assert_eq!(a, b);
81 }
82
83 #[test]
84 fn fallback_path_terminates_for_constant_rng() {
85 struct ConstantRng;
86
87 impl TryRng for ConstantRng {
88 type Error = Infallible;
89
90 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
91 Ok(u32::from_le_bytes([255, 255, 255, 255]))
92 }
93
94 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
95 Ok(u64::from_le_bytes([255, 255, 255, 255, 255, 255, 255, 255]))
96 }
97
98 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
99 self.fill_bytes(dest);
100 Ok(())
101 }
102 }
103
104 impl ConstantRng {
105 fn fill_bytes(&mut self, dest: &mut [u8]) {
106 dest.fill(255);
107 }
108 }
109
110 let mut rng = ConstantRng;
111 let value = random_base62_with_rng(&mut rng, 32);
112 assert_eq!(value.len(), 32);
113 assert!(value.chars().all(|c| c.is_ascii_alphanumeric()));
114 }
115
116 #[test]
117 fn output_uses_diverse_alphabet() {
118 let mut rng = ChaCha20Rng::from_seed([42u8; 32]);
121 let out = random_base62_with_rng(&mut rng, 256);
122 let unique: std::collections::HashSet<char> = out.chars().collect();
123 assert!(
124 unique.len() > 10,
125 "expected diverse output, got {} unique chars",
126 unique.len()
127 );
128 }
129
130 #[test]
131 fn fallback_path_uses_modulo_not_division() {
132 struct AllMaxRng;
136
137 impl TryRng for AllMaxRng {
138 type Error = Infallible;
139
140 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
141 Ok(u32::MAX)
142 }
143
144 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
145 Ok(u64::MAX)
146 }
147
148 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
149 self.fill_bytes(dest);
150 Ok(())
151 }
152 }
153
154 impl AllMaxRng {
155 fn fill_bytes(&mut self, dest: &mut [u8]) {
156 dest.fill(255);
157 }
158 }
159
160 let mut rng = AllMaxRng;
161 let out = random_base62_with_rng(&mut rng, 4);
162 assert!(
164 out.chars().all(|c| c == 'H'),
165 "expected all 'H' from fallback, got {out}"
166 );
167 }
168
169 #[test]
170 fn acceptance_boundary_rejects_248_and_keeps_batch_semantics() {
171 struct BoundaryRng {
172 fills: usize,
173 }
174
175 impl TryRng for BoundaryRng {
176 type Error = Infallible;
177
178 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
179 Ok(0)
180 }
181
182 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
183 Ok(0)
184 }
185
186 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
187 self.fill_bytes(dest);
188 Ok(())
189 }
190 }
191
192 impl BoundaryRng {
193 fn fill_bytes(&mut self, dest: &mut [u8]) {
194 dest.fill(255);
195 if self.fills == 0 {
196 dest[0] = 0;
197 dest[1] = 247;
198 dest[2] = 248;
199 } else {
200 dest[0] = 1;
201 }
202 self.fills += 1;
203 }
204 }
205
206 let mut rng = BoundaryRng { fills: 0 };
207 let out = random_base62_with_rng(&mut rng, 3);
208
209 assert_eq!(out, "A9B");
210 }
211}