fix(dashboard): rejection-sample password-gen to remove modulo bias
Switches to Uint8 rejection sampling against the largest multiple of the charset length that fits in a byte. Eliminates the ~16 ppm overweight the previous `% N` over Uint32 would otherwise leave on the first 38 chars. Adds a vitest distribution check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,11 @@
|
||||
// entropy at 16 chars (~95 bits) to be uncopyable by hand mistakes,
|
||||
// avoidant of characters that ship awkwardly through chat clients
|
||||
// (no quotes, slashes, or backticks).
|
||||
//
|
||||
// Sampling: rejection sampling against a Uint8 stream. The naive
|
||||
// `byte % CHARSET.length` would slightly overweight the first
|
||||
// (256 mod N) chars; with N = 71 that's ~16 ppm of bias which is
|
||||
// safe at 16 chars but easy to remove.
|
||||
|
||||
const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&*+-?@';
|
||||
|
||||
@@ -15,11 +20,18 @@ export function generatePassword(length = 16): string {
|
||||
if (length < 8) {
|
||||
throw new Error('password length must be at least 8');
|
||||
}
|
||||
const buf = new Uint32Array(length);
|
||||
crypto.getRandomValues(buf);
|
||||
const n = CHARSET.length;
|
||||
// Largest multiple of `n` that fits in a Uint8 — bytes ≥ MAX get
|
||||
// rejected to remove modulo bias.
|
||||
const max = 256 - (256 % n);
|
||||
const buf = new Uint8Array(length);
|
||||
let out = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
out += CHARSET[buf[i] % CHARSET.length];
|
||||
while (out.length < length) {
|
||||
crypto.getRandomValues(buf);
|
||||
for (let i = 0; i < buf.length && out.length < length; i++) {
|
||||
const byte = buf[i];
|
||||
if (byte < max) out += CHARSET[byte % n];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user