1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

Use a random impl with rejection sampling

This commit is contained in:
David Baker
2025-01-20 18:13:26 +00:00
parent 86494c3a96
commit 35fe7bc60a
2 changed files with 81 additions and 9 deletions

View File

@@ -40,17 +40,48 @@ export function secureRandomString(len: number): string {
/**
* Generate a cryptographically secure random string using characters given
* @param len The length of the string to generate
* @param chars The characters to use in the random string.
* @param len The length of the string to generate (must be positive and less than 32768)
* @param chars The characters to use in the random string (between 2 and 256 characters long).
* @returns Random string of characters of length `len`
*/
export function secureRandomStringFrom(len: number, chars: string): string {
const positions = new Uint32Array(chars.length);
let ret = "";
crypto.getRandomValues(positions);
for (let i = 0; i < len; i++) {
const currentCharPlace = positions[i % chars.length] % chars.length;
ret += chars[currentCharPlace];
// This is intended for latin strings so 256 possibilities should be more than enough and
// means we can use random bytes, minimising the amount of entropy we need to ask for.
if (chars.length < 2 || chars.length > 256) {
throw new Error("Character set must be between 2 and 256 characters long");
}
return ret;
if (len < 1 || len > 32768) {
throw new Error("Requested random string length must be between 1 and 32768");
}
// We'll generate random unsigned bytes, so get the largest number less than 256 that is a multiple
// of the length of the character set: We'll need to discard any random values that are larger than
// this as we can't possibly map them onto the character set while keeping each character equally
// likely to be chosen (minus 1 to convert to indices in a string). (Essentially, we're using a d8
// to choose between 7 possibilities and re-rolling on an 8, keeping all 7 outcomes equally likely.)
const maxRandValue = Math.floor(255 / chars.length) * chars.length - 1;
// Grab 30% more entropy than we need. This should be enough that we can discard the values that are
// too high without having to go back and grab more unless we're super unlucky.
const entropyBuffer = new Uint8Array(Math.floor(len * 1.3));
// Mark all of this buffer as used to start with (we haven't populated it with entropy yet) so it will
// be filled on the first iteration.
let entropyBufferPos = entropyBuffer.length;
const result = [];
while (result.length < len) {
if (entropyBufferPos === entropyBuffer.length) {
globalThis.crypto.getRandomValues(entropyBuffer);
entropyBufferPos = 0;
}
const randomByte = entropyBuffer[entropyBufferPos++];
if (randomByte < maxRandValue) {
result.push(chars[randomByte % chars.length]);
}
}
return result.join("");
}