@@ -42,11 +42,69 @@ private const val F_RANDOM_INT = "randomInt"
4242// Cached Int generator to ensure we don't create multiple instances.
4343private val cryptoRandomGenerator by lazy { SecureRandom () }
4444
45- // Safe integer bounds for randomInt generation based on Node.js limits:
46- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
47- // @TODO Remove duplicate definitions
48- private const val MAX_RANDOM_INT_RANGE = (1L shl 48 ) - 1 // 281_474_976_710_655L
49- private val MAX_48_BIT_LIMIT = BigInteger .valueOf(2L ).pow(48 ) // 2^48
45+ // The maximum range (max - min) allowed is 2^48 in Node.js.
46+ private val MAX_48_BIT_LIMIT = BigInteger .valueOf(2L ).pow(48 )
47+
48+ // Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
49+ private fun genRandomInt (min : Long , max : Long ): Long {
50+ try {
51+ return cryptoRandomGenerator.nextLong(min, max)
52+ } catch (e: Throwable ) {
53+ throw TypeError .create(" Error generating random bytes for randomInt: ${e.message} " )
54+ }
55+ }
56+
57+ // Safely converts a Value to a BigInteger, ensuring it is a safe integer within JS limits.
58+ private fun safeValueToBigInt (value : Value , name : String ): BigInteger {
59+ if (value.isNumber) {
60+ val bigIntValue: BigInteger ? = when {
61+ value.fitsInLong() -> {
62+ BigInteger .valueOf(value.asLong())
63+ }
64+ // Reject integers that exceed Long.MAX_VALUE or are less than Long.MIN_VALUE
65+ value.fitsInBigInteger() -> throw RangeError .create(" The \" $name \" argument must be a safe integer. Received an integer that exceeds the max bounds ${MAX_48_BIT_LIMIT } ." )
66+ // Reject non-integer numbers
67+ value.fitsInDouble() -> {
68+ throw TypeError .create(" The \" $name \" argument must be a safe integer. Received a non-integer number: ${value.asDouble()} ." )
69+ }
70+ else -> null // Reject non-integer (e.g. Infinity, NaN, very large BigInts)
71+ }
72+
73+ // Define JS safe integer bounds
74+ val jsMaxSafeInt = BigInteger (" 9007199254740991" ) // 2^53 - 1
75+ val jsMinSafeInt = BigInteger (" -9007199254740991" ) // -(2^53 - 1)
76+
77+ // Final check: even if conversion works, ensure it falls within JS safe limits
78+ if (bigIntValue != null && bigIntValue >= jsMinSafeInt && bigIntValue <= jsMaxSafeInt) {
79+ return bigIntValue
80+ }
81+ }
82+ // Invalid value type, we don't want it
83+ throw TypeError .create(" The \" $name \" argument must be a safe integer. Received ${value} ." )
84+ }
85+
86+ // Validates that the provided min and max values are safe integers and that the range difference does not exceed 2^48.
87+ private fun genSafeRange (min : Value , max : Value ): Pair <Long , Long > {
88+ // Safely convert both inputs to BigInteger
89+ val minBigInt = safeValueToBigInt(min, " min" )
90+ val maxBigInt = safeValueToBigInt(max, " max" )
91+
92+ // Enforce the Min <= Max rule otherwise we throw a RangeError
93+ if (minBigInt >= maxBigInt) {
94+ throw RangeError .create(" The value of \" max\" is out of range. It must be greater than the value of \" min\" (${minBigInt} ). Received ${maxBigInt} ." )
95+ }
96+
97+ val rangeDifference = maxBigInt.subtract(minBigInt)
98+
99+ // If the range difference exceeds 2^48, we throw a RangeError. Node.js has a range limit of 2^48 for randomInt.
100+ if (rangeDifference > MAX_48_BIT_LIMIT ) {
101+ println (" Range difference exceeds 2^48 limit: $rangeDifference " )
102+ throw RangeError .create(" The value of \" max - min\" is out of range. It must be <= 281474976710655. Received ${rangeDifference} ." )
103+ }
104+
105+ // Return the validated safe Long values
106+ return Pair (minBigInt.toLong(), maxBigInt.toLong())
107+ }
50108
51109// Installs the Node crypto module into the intrinsic bindings.
52110@Intrinsic internal class NodeCryptoModule : AbstractNodeBuiltinModule () {
@@ -80,55 +138,61 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
80138 return java.util.UUID .randomUUID().toString()
81139 }
82140
83- @Polyglot override fun randomInt (min : Long , max : Long , callback : RandomIntCallback ? ): Any {
84- if (min >= max) throw RangeError .create(
85- " The value of \" max\" is out of range. It must be greater than the value of \" min\" ($min ). Received $max "
86- )
141+ @Polyglot override fun randomInt (min : Long , max : Long ): Long {
142+ return genRandomInt(min, max)
143+ }
87144
88- val range = max - min
145+ @Polyglot override fun randomInt (min : Long , max : Long , callback : RandomIntCallback ) {
146+ val randomValue = genRandomInt(min, max)
89147
90- if (range > MAX_RANDOM_INT_RANGE ) {
91- throw RangeError .create(
92- " The range (max - min) is out of bounds. It must be <= $MAX_RANDOM_INT_RANGE . " +
93- " Received min=$min , max=$max "
94- )
148+ thread {
149+ try {
150+ callback.invoke(null , randomValue)
151+ } catch (e: Throwable ) {
152+ callback.invoke(TypeError .create(e.message ? : " Unknown error" ), randomValue)
153+ }
95154 }
155+ }
96156
97- var randomValue: Long
98- try {
99- randomValue = cryptoRandomGenerator.nextLong(min, max)
100- } catch (e: Throwable ) {
101- throw TypeError .create(" Error generating random bytes for randomInt: ${e.message} " )
102- }
157+ @Polyglot override fun randomInt (min : Value , max : Value , callback : Value ) {
158+ val (safeMin, safeMax) = genSafeRange(min, max)
103159
104- // Handle callback asynchronously if provided, otherwise we return the value directly to reflect Node.js behavior
105- return if (callback != null ) {
106- thread {
107- try {
108- callback(null , randomValue)
109- } catch (e: Throwable ) {
110- callback(TypeError .create(e.message ? : " Unknown error" ), null )
111- }
160+ val safeCallback: RandomIntCallback = callback.let { cb ->
161+ { err: Throwable ? , value: Long? ->
162+ cb.execute(
163+ err?.let { Value .asValue(it) },
164+ value?.let { Value .asValue(it) }
165+ )
112166 }
113- Unit
114- } else {
115- randomValue
116167 }
168+
169+ return randomInt(safeMin, safeMax, safeCallback)
117170 }
118171
119- @Polyglot override fun randomInt (min : Value , max : Value , callback : Value ? ): Any {
172+ @Polyglot override fun randomInt (min : Value , max : Value ): Long {
120173 val (safeMin, safeMax) = genSafeRange(min, max)
121- return randomInt(safeMin, safeMax, callback as ? RandomIntCallback )
174+ return randomInt(safeMin, safeMax)
122175 }
123176
124- @Polyglot override fun randomInt (max : Value , callback : Value ? ): Any {
177+ @Polyglot override fun randomInt (max : Value , callback : Value ? ) {
125178 val (safeMin, safeMax) = genSafeRange(Value .asValue(0 ), max)
126- return randomInt(safeMin, safeMax, callback as ? RandomIntCallback )
179+ val safeCallback: RandomIntCallback ? = callback?.let { cb ->
180+ { err: Throwable ? , value: Long? ->
181+ cb.execute(
182+ err?.let { Value .asValue(it) },
183+ value?.let { Value .asValue(it) }
184+ )
185+ }
186+ }
187+
188+ if (safeCallback != null ) {
189+ randomInt(safeMin, safeMax, safeCallback)
190+ }
127191 }
128192
129- @Polyglot override fun randomInt (max : Value ): Any {
193+ @Polyglot override fun randomInt (max : Value ): Long {
130194 val (safeMin, safeMax) = genSafeRange(Value .asValue(0 ), max)
131- return randomInt(safeMin, safeMax, null )
195+ return randomInt(safeMin, safeMax)
132196 }
133197
134198 // ProxyObject implementation
@@ -146,7 +210,6 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
146210 F_RANDOM_INT -> ProxyExecutable { args ->
147211 // Check if last argument is a callback function
148212 val lastIsCb = args.lastOrNull()?.canExecute() == true
149- val cb = if (lastIsCb) args.last() else null
150213
151214 when (args.size) {
152215 1 -> {
@@ -156,71 +219,19 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
156219 2 -> {
157220 if (lastIsCb) {
158221 // randomInt(max, callback)
159- this .randomInt(args[0 ], cb )
222+ this .randomInt(args[0 ], args.last() )
160223 } else {
161224 // randomInt(min, max)
162- this .randomInt(args[0 ], args[1 ], null )
225+ this .randomInt(args[0 ], args[1 ])
163226 }
164227 }
165228 3 -> {
166229 // randomInt(min, max, callback)
167- this .randomInt(args[0 ], args[1 ], cb )
230+ this .randomInt(args[0 ], args[1 ], args.last() )
168231 }
169232 else -> throw JsError .typeError(" Invalid number of arguments for crypto.randomInt: ${args.size} " )
170233 }
171234 }
172235 else -> null
173236 }
174-
175- private fun safeValueToBigInt (value : Value , name : String ): BigInteger {
176- if (value.isNumber) {
177- val bigIntValue: BigInteger ? = when {
178- value.fitsInLong() -> {
179- BigInteger .valueOf(value.asLong())
180- }
181- // Reject integers that exceed Long.MAX_VALUE or are less than Long.MIN_VALUE
182- value.fitsInBigInteger() -> throw RangeError .create(" The \" $name \" argument must be a safe integer. Received an integer that exceeds the max bounds ${MAX_48_BIT_LIMIT } ." )
183- // Reject non-integer numbers
184- value.fitsInDouble() -> {
185- throw TypeError .create(" The \" $name \" argument must be a safe integer. Received a non-integer number: ${value.asDouble()} ." )
186- }
187- else -> null // Reject non-integer (e.g. Infinity, NaN, very large BigInts)
188- }
189-
190- // Define JS safe integer bounds
191- val jsMaxSafeInt = BigInteger (" 9007199254740991" ) // 2^53 - 1
192- val jsMinSafeInt = BigInteger (" -9007199254740991" ) // -(2^53 - 1)
193-
194- // Final check: even if conversion works, ensure it falls within JS safe limits
195- if (bigIntValue != null && bigIntValue >= jsMinSafeInt && bigIntValue <= jsMaxSafeInt) {
196- return bigIntValue
197- }
198- }
199- // Invalid value type, we don't want it
200- throw TypeError .create(" The \" $name \" argument must be a safe integer. Received ${value} ." )
201- }
202-
203- // Validates that the provided min and max values are safe integers and that the range difference does not exceed 2^48.
204- private fun genSafeRange (min : Value , max : Value ): Pair <Long , Long > {
205- // Safely convert both inputs to BigInteger
206- val minBigInt = safeValueToBigInt(min, " min" )
207- val maxBigInt = safeValueToBigInt(max, " max" )
208-
209- // Enforce the Min <= Max rule otherwise we throw a RangeError
210- if (minBigInt >= maxBigInt) {
211- throw RangeError .create(" The value of \" max\" is out of range. It must be greater than the value of \" min\" (${minBigInt} ). Received ${maxBigInt} ." )
212- }
213-
214- // Enforce the difference between (max - min) exceeds 2^48
215- val rangeDifference = maxBigInt.subtract(minBigInt) // Since min < max, difference is non-negative
216-
217- // If the range difference exceeds 2^48, we throw a RangeError. Node.JS has a range limit of 2^48 for randomInt.
218- if (rangeDifference > MAX_48_BIT_LIMIT ) {
219- println (" Range difference exceeds 2^48 limit: $rangeDifference " )
220- throw RangeError .create(" The value of \" max - min\" is out of range. It must be <= 281474976710655. Received ${rangeDifference} ." )
221- }
222-
223- // Return the validated safe Long values
224- return Pair (minBigInt.toLong(), maxBigInt.toLong())
225- }
226237}
0 commit comments