@@ -40,17 +40,48 @@ export function secureRandomString(len: number): string {
40
40
41
41
/**
42
42
* Generate a cryptographically secure random string using characters given
43
- * @param len The length of the string to generate
44
- * @param chars The characters to use in the random string.
43
+ * @param len The length of the string to generate (must be positive and less than 32768)
44
+ * @param chars The characters to use in the random string (between 2 and 256 characters long) .
45
45
* @returns Random string of characters of length `len`
46
46
*/
47
47
export function secureRandomStringFrom ( len : number , chars : string ) : string {
48
- const positions = new Uint32Array ( chars . length ) ;
49
- let ret = "" ;
50
- crypto . getRandomValues ( positions ) ;
51
- for ( let i = 0 ; i < len ; i ++ ) {
52
- const currentCharPlace = positions [ i % chars . length ] % chars . length ;
53
- ret += chars [ currentCharPlace ] ;
48
+ // This is intended for latin strings so 256 possibilities should be more than enough and
49
+ // means we can use random bytes, minimising the amount of entropy we need to ask for.
50
+ if ( chars . length < 2 || chars . length > 256 ) {
51
+ throw new Error ( "Character set must be between 2 and 256 characters long" ) ;
52
+ }
53
+
54
+ if ( len < 1 || len > 32768 ) {
55
+ throw new Error ( "Requested random string length must be between 1 and 32768" ) ;
56
+ }
57
+
58
+ // We'll generate random unsigned bytes, so get the largest number less than 256 that is a multiple
59
+ // of the length of the character set: We'll need to discard any random values that are larger than
60
+ // this as we can't possibly map them onto the character set while keeping each character equally
61
+ // likely to be chosen (minus 1 to convert to indices in a string). (Essentially, we're using a d8
62
+ // to choose between 7 possibilities and re-rolling on an 8, keeping all 7 outcomes equally likely.)
63
+ const maxRandValue = Math . floor ( 255 / chars . length ) * chars . length - 1 ;
64
+
65
+ // Grab 30% more entropy than we need. This should be enough that we can discard the values that are
66
+ // too high without having to go back and grab more unless we're super unlucky.
67
+ const entropyBuffer = new Uint8Array ( Math . floor ( len * 1.3 ) ) ;
68
+ // Mark all of this buffer as used to start with (we haven't populated it with entropy yet) so it will
69
+ // be filled on the first iteration.
70
+ let entropyBufferPos = entropyBuffer . length ;
71
+
72
+ const result = [ ] ;
73
+ while ( result . length < len ) {
74
+ if ( entropyBufferPos === entropyBuffer . length ) {
75
+ globalThis . crypto . getRandomValues ( entropyBuffer ) ;
76
+ entropyBufferPos = 0 ;
77
+ }
78
+
79
+ const randomByte = entropyBuffer [ entropyBufferPos ++ ] ;
80
+
81
+ if ( randomByte < maxRandValue ) {
82
+ result . push ( chars [ randomByte % chars . length ] ) ;
83
+ }
54
84
}
55
- return ret ;
85
+
86
+ return result . join ( "" ) ;
56
87
}
0 commit comments