Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,26 @@
**/
package org.kotlincrypto.random.internal.js

import kotlin.math.min

internal actual val IS_NODE_JS: Boolean by lazy { isNodeJs() }

internal actual fun jsCryptoBrowser(): JsCrypto = js(CODE_JS_CRYPTO_BROWSER)
internal actual fun jsCryptoNode(): JsCrypto = js(CODE_JS_CRYPTO_NODE)

private fun isNodeJs(): Boolean = js(CODE_IS_NODE_JS)

internal actual fun ByteArray.cryptoRandFill(procure: (JsUint8Array) -> Unit) {
val a = asJsInt8Array().asJsUint8Array()
if (a.length < JS_CRYPTO_MAX_FILL) return procure(a)

var remainder = size
var offset = 0
while (remainder > 0) {
val len = min(JS_CRYPTO_MAX_FILL, remainder)
val s = a.subarray(start = offset, end = offset + len)
procure(s)
offset += len
remainder -= len
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2025 KotlinCrypto
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
@file:Suppress("NOTHING_TO_INLINE")

package org.kotlincrypto.random.internal.js

import kotlin.js.unsafeCast

internal actual fun jsInt8Array(length: Int): JsInt8Array = js(CODE_JS_NEW_INT8_LENGTH)
internal actual fun jsInt8Array(buffer: JsArrayBufferLike): JsInt8Array = js(CODE_JS_NEW_INT8_BUFFER)

internal actual fun jsInt8ArrayGet(array: JsInt8Array, index: Int): Byte = js(CODE_JS_ARRAY_GET)
internal actual fun jsInt8ArraySet(array: JsInt8Array, index: Int, value: Byte) { js(CODE_JS_ARRAY_SET) }

internal inline fun ByteArray.asJsInt8Array(): JsInt8Array = unsafeCast<JsInt8Array>()
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@
**/
package org.kotlincrypto.random.internal.js

internal actual fun jsUint8ArrayGet(array: JsUint8Array, index: Int): Byte = js("array[index]")
internal actual fun jsUint8ArraySet(array: JsUint8Array, index: Int, value: Byte) { js("array[index] = value") }
internal actual fun jsUint8Array(length: Int): JsUint8Array = js(CODE_JS_NEW_UINT8_LENGTH)
internal actual fun jsUint8Array(buffer: JsArrayBufferLike): JsUint8Array = js(CODE_JS_NEW_UINT8_BUFFER)

internal actual fun jsUint8ArrayGet(array: JsUint8Array, index: Int): Short = js(CODE_JS_ARRAY_GET)
internal actual fun jsUint8ArraySet(array: JsUint8Array, index: Int, value: Byte) { js(CODE_JS_ARRAY_SET) }
Original file line number Diff line number Diff line change
Expand Up @@ -18,65 +18,19 @@ package org.kotlincrypto.random.internal
import org.kotlincrypto.random.RandomnessProcurementException
import org.kotlincrypto.random.internal.js.IS_NODE_JS
import org.kotlincrypto.random.internal.js.JsCrypto
import org.kotlincrypto.random.internal.js.JsUint8Array
import org.kotlincrypto.random.internal.js.get
import org.kotlincrypto.random.internal.js.jsCryptoBrowser
import org.kotlincrypto.random.internal.js.cryptoRandFill
import org.kotlincrypto.random.internal.js.jsCryptoNode
import org.kotlincrypto.random.internal.js.set

private const val BUFFER_SIZE = 1024 * 8

private val JS_CRYPTO: JsCrypto by lazy { if (IS_NODE_JS) jsCryptoNode() else jsCryptoBrowser() }

@Throws(RandomnessProcurementException::class)
internal actual fun ByteArray.cryptoRandFill() {
try {
val jsCryptoFill = if (IS_NODE_JS) JS_CRYPTO::randomFillSync else JS_CRYPTO::getRandomValues

// Cannot simply use the ByteArray when calling the supplied Crypto function.
// Must utilize Uint8Array and then copy over results (See Issue #8). Also,
// by chunking in a size less than 65536, it avoids hitting the ceiling imposed
// on JS Browser (See issue #9).
if (size <= BUFFER_SIZE) {
// 1 shot it
val buf = JsUint8Array(size)
jsCryptoFill(buf)
for (i in indices) {
this[i] = buf[i]
buf[i] = 0 // Always ensure buffer is zeroed out
}
return
}

val buf = JsUint8Array(BUFFER_SIZE)

var needed = size
var pos = 0

// chunk
while (needed > BUFFER_SIZE) {
jsCryptoFill(buf)
for (i in 0 until BUFFER_SIZE) {
this[pos++] = buf[i]
}
needed -= BUFFER_SIZE
}

// remainder
if (needed > 0) {
jsCryptoFill(buf.subarray(0, needed))

for (i in 0 until needed) {
this[pos++] = buf[i]
buf[i] = 0 // Always ensure buffer is zeroed out
}
}

for (i in needed until BUFFER_SIZE) {
buf[i] = 0 // Always ensure buffer is zeroed out
}
cryptoRandFill(procure = jsCryptoFill)
} catch (t: Throwable) {
val fn = if (IS_NODE_JS) "randomFillSync" else "getRandomValues"
throw RandomnessProcurementException("Failed to obtain bytes from [crypto.$fn]", t)
val name = if (IS_NODE_JS) "randomFillSync" else "getRandomValues"
throw RandomnessProcurementException("Failed to obtain bytes from [crypto.$name]", t)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2026 KotlinCrypto
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package org.kotlincrypto.random.internal.js

internal sealed external interface JsArrayBufferLike

internal sealed external interface JsTypedArrayLike {
val buffer: JsArrayBufferLike
val length: Int
fun subarray(start: Int, end: Int): JsTypedArrayLike
}

internal const val CODE_JS_ARRAY_GET: String = "array[index]"
internal const val CODE_JS_ARRAY_SET: String = "array[index] = value"
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ internal external interface JsCrypto {
fun randomFillSync(buf: JsUint8Array)
}

internal const val JS_CRYPTO_MAX_FILL: Int = 65536

internal const val CODE_IS_NODE_JS: String =
"""
(typeof process !== 'undefined'
Expand All @@ -43,3 +45,5 @@ internal expect val IS_NODE_JS: Boolean

internal expect fun jsCryptoBrowser(): JsCrypto
internal expect fun jsCryptoNode(): JsCrypto

internal expect fun ByteArray.cryptoRandFill(procure: (JsUint8Array) -> Unit)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2025 KotlinCrypto
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
@file:Suppress("NOTHING_TO_INLINE", "UNUSED")

package org.kotlincrypto.random.internal.js

import kotlin.js.JsName

@JsName("Int8Array")
internal open external class JsInt8Array: JsTypedArrayLike {
override val buffer: JsArrayBufferLike
override val length: Int
override fun subarray(start: Int, end: Int): JsInt8Array
internal companion object
}

internal inline fun JsInt8Array.Companion.new(length: Int): JsInt8Array = jsInt8Array(length)
internal inline fun JsInt8Array.Companion.new(buffer: JsArrayBufferLike): JsInt8Array = jsInt8Array(buffer)

internal inline operator fun JsInt8Array.get(index: Int): Byte = jsInt8ArrayGet(this, index)
internal inline operator fun JsInt8Array.set(index: Int, value: Byte) { jsInt8ArraySet(this, index, value) }

internal inline fun JsInt8Array.asJsUint8Array(): JsUint8Array = JsUint8Array.new(buffer)

internal const val CODE_JS_NEW_INT8_LENGTH = "new Int8Array(length)"
internal const val CODE_JS_NEW_INT8_BUFFER = "new Int8Array(buffer)"

internal expect fun jsInt8Array(length: Int): JsInt8Array
internal expect fun jsInt8Array(buffer: JsArrayBufferLike): JsInt8Array

internal expect fun jsInt8ArrayGet(array: JsInt8Array, index: Int): Byte
internal expect fun jsInt8ArraySet(array: JsInt8Array, index: Int, value: Byte)
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,26 @@ package org.kotlincrypto.random.internal.js
import kotlin.js.JsName

@JsName("Uint8Array")
internal open external class JsUint8Array(length: Int) {
fun subarray(start: Int, end: Int): JsUint8Array
internal open external class JsUint8Array: JsTypedArrayLike {
override val buffer: JsArrayBufferLike
override val length: Int
override fun subarray(start: Int, end: Int): JsUint8Array
internal companion object
}

internal inline operator fun JsUint8Array.get(index: Int): Byte = jsUint8ArrayGet(this, index)
internal inline operator fun JsUint8Array.set(index: Int, value: Byte) { jsUint8ArraySet(this, index, value) }
internal inline fun JsUint8Array.Companion.new(length: Int): JsUint8Array = jsUint8Array(length)
internal inline fun JsUint8Array.Companion.new(buffer: JsArrayBufferLike): JsUint8Array = jsUint8Array(buffer)

internal expect fun jsUint8ArrayGet(array: JsUint8Array, index: Int): Byte
internal inline operator fun JsUint8Array.get(index: Int): Byte = jsUint8ArrayGet(this, index).toByte()
internal inline operator fun JsUint8Array.set(index: Int, value: UByte) { jsUint8ArraySet(this, index, value.toByte()) }

internal inline fun JsUint8Array.asJsInt8Array(): JsInt8Array = JsInt8Array.new(buffer)

internal const val CODE_JS_NEW_UINT8_LENGTH = "new Uint8Array(length)"
internal const val CODE_JS_NEW_UINT8_BUFFER = "new Uint8Array(buffer)"

internal expect fun jsUint8Array(length: Int): JsUint8Array
internal expect fun jsUint8Array(buffer: JsArrayBufferLike): JsUint8Array

internal expect fun jsUint8ArrayGet(array: JsUint8Array, index: Int): Short
internal expect fun jsUint8ArraySet(array: JsUint8Array, index: Int, value: Byte)
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2026 KotlinCrypto
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package org.kotlincrypto.random.internal.js

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class CryptoRandFillJsWasmJsUnitTest {

@Test
fun givenArray_whenLessThanMax_thenFillIsInvokedOnce() {
val ba = ByteArray(20)
var invocations = 0
ba.cryptoRandFill { jsArray ->
for (i in 0 until jsArray.length) { jsArray[i] = 1.toUByte() }
invocations++
}
assertEquals(1, invocations)
ba.forEachIndexed { i, b ->
assertEquals(1.toByte(), b, "index[$i]")
}
}

@Test
fun givenArray_whenGreaterThanLimit_thenFillIsMultipleTimes() {
val ba = ByteArray((JS_CRYPTO_MAX_FILL * 3) + 25)
var invocations = 0
ba.cryptoRandFill { jsArray ->
for (i in 0 until jsArray.length) { jsArray[i] = 1.toUByte() }
invocations++
}
assertTrue(invocations > 3, "invocations[$invocations]")
ba.forEachIndexed { i, b ->
assertEquals(1.toByte(), b, "index[$i]")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,43 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
@file:Suppress("OPT_IN_USAGE")

package org.kotlincrypto.random.internal.js

import kotlin.math.min

internal actual val IS_NODE_JS: Boolean by lazy { isNodeJs() }

internal actual fun jsCryptoBrowser(): JsCrypto = js(CODE_JS_CRYPTO_BROWSER)
internal actual fun jsCryptoNode(): JsCrypto = js(CODE_JS_CRYPTO_NODE)

private fun isNodeJs(): Boolean = js(CODE_IS_NODE_JS)

private const val BUFFER_SIZE = 1024 * 8

internal actual fun ByteArray.cryptoRandFill(procure: (JsUint8Array) -> Unit) {
// Kotlin/WasmJs does not provide access to ByteArray.storage
// (i.e. the WasmByteArray), so we must always use a buffer and
// copy back and forth.
val buf = JsUint8Array.new(length = min(size, BUFFER_SIZE))

try {
var remainder = size
var offset = 0
while (remainder > 0) {
val len = if (remainder >= buf.length) {
procure(buf)
buf.length
} else {
procure(buf.subarray(0, remainder))
remainder
}
repeat(len) { i -> this[offset++] = buf[i] }
remainder -= len
}
} finally {
// Always ensure the buffer gets zeroed out when done.
repeat(buf.length) { i -> buf[i] = 0u }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 KotlinCrypto
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
@file:Suppress("OPT_IN_USAGE")

package org.kotlincrypto.random.internal.js

internal actual fun jsInt8Array(length: Int): JsInt8Array = js(CODE_JS_NEW_INT8_LENGTH)
internal actual fun jsInt8Array(buffer: JsArrayBufferLike): JsInt8Array = js(CODE_JS_NEW_INT8_BUFFER)

internal actual fun jsInt8ArrayGet(array: JsInt8Array, index: Int): Byte = js(CODE_JS_ARRAY_GET)
internal actual fun jsInt8ArraySet(array: JsInt8Array, index: Int, value: Byte) { js(CODE_JS_ARRAY_SET) }
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
@file:Suppress("OPT_IN_USAGE")

package org.kotlincrypto.random.internal.js

internal actual fun jsUint8ArrayGet(array: JsUint8Array, index: Int): Byte = js("array[index]")
internal actual fun jsUint8ArraySet(array: JsUint8Array, index: Int, value: Byte) { js("array[index] = value") }
internal actual fun jsUint8Array(length: Int): JsUint8Array = js(CODE_JS_NEW_UINT8_LENGTH)
internal actual fun jsUint8Array(buffer: JsArrayBufferLike): JsUint8Array = js(CODE_JS_NEW_UINT8_BUFFER)

internal actual fun jsUint8ArrayGet(array: JsUint8Array, index: Int): Short = js(CODE_JS_ARRAY_GET)
internal actual fun jsUint8ArraySet(array: JsUint8Array, index: Int, value: Byte) { js(CODE_JS_ARRAY_SET) }
Loading