Skip to content

Commit

Permalink
Add SecureRandom implementation (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
05nelsonm authored Mar 7, 2023
1 parent 9bab74e commit 57e71f0
Show file tree
Hide file tree
Showing 22 changed files with 1,274 additions and 0 deletions.
12 changes: 12 additions & 0 deletions secure-random/api/secure-random.api
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
public final class org/kotlincrypto/SecRandomCopyException : java/lang/RuntimeException {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun <init> (Ljava/lang/Throwable;)V
}

public final class org/kotlincrypto/SecureRandom : java/security/SecureRandom {
public fun <init> ()V
public final fun nextBytesCopyTo ([B)V
public final fun nextBytesOf (I)[B
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2023 Matthew Nelson
*
* 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

public class SecRandomCopyException: RuntimeException {
public constructor(): super()
public constructor(message: String?): super(message)
public constructor(message: String?, cause: Throwable?): super(message, cause)
public constructor(cause: Throwable?): super(cause)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2023 Matthew Nelson
*
* 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

/**
* A cryptographically strong random number generator (RNG).
* */
public expect class SecureRandom() {

/**
* Returns a [ByteArray] of size [count], filled with
* securely generated random data.
*
* @throws [IllegalArgumentException] if [count] is negative.
* @throws [SecRandomCopyException] if [nextBytesCopyTo] failed.
* */
@Throws(IllegalArgumentException::class, SecRandomCopyException::class)
public fun nextBytesOf(count: Int): ByteArray

/**
* Fills a [ByteArray] with securely generated random data.
* Does nothing if [bytes] is null or empty.
*
* @throws [SecRandomCopyException] if procurement of securely random data failed.
* */
@Throws(SecRandomCopyException::class)
public fun nextBytesCopyTo(bytes: ByteArray?)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2023 Matthew Nelson
*
* 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("KotlinRedundantDiagnosticSuppress")

package org.kotlincrypto.internal

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import org.kotlincrypto.SecRandomCopyException
import org.kotlincrypto.SecureRandom

@Suppress("NOTHING_TO_INLINE")
@Throws(IllegalArgumentException::class, SecRandomCopyException::class)
internal inline fun SecureRandom.commonNextBytesOf(count: Int): ByteArray {
require(count >= 0) { "count cannot be negative" }
val bytes = ByteArray(count)
nextBytesCopyTo(bytes)
return bytes
}

@OptIn(ExperimentalContracts::class)
@Suppress("NOTHING_TO_INLINE")
internal inline fun ByteArray?.ifNotNullOrEmpty(block: ByteArray.() -> Unit) {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}

if (this == null || this.isEmpty()) return
block.invoke(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2023 Matthew Nelson
*
* 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

import kotlin.test.assertTrue

/**
* Test helper to extend for some platforms which have fallback
* implementations if something is not available.
* */
abstract class EnsureFilledHelper {

protected abstract val sRandom: SecureRandom

// https://github.com/briansmith/ring/blob/main/tests/rand_tests.rs
open fun givenByteArray_whenNextBytes_thenIsFilledWithData() {
val linuxLimit = 256
val webLimit = 65536

val sizes = listOf(
1,
2,
3,
96,
linuxLimit - 1,
linuxLimit,
linuxLimit + 1,
linuxLimit * 2,
511,
512,
513,
4096,
webLimit - 1,
webLimit,
webLimit + 1,
webLimit * 2,
)

for (size in sizes) {
val bytes = ByteArray(size)
val emptyByte = bytes[0]

sRandom.nextBytesCopyTo(bytes)

var emptyCount = 0
bytes.forEach {
if (it == emptyByte) {
emptyCount++
}
}

// Some indices will remain empty so cannot check if all were
// filled. Must adjust our limit depending on size to mitigate
// false positives.
val emptyLimit = when {
size < 10 -> 0.5f
size < 200 -> 0.04F
size < 1000 -> 0.03F
size < 10000 -> 0.01F
else -> 0.0075F
}.let { pctErr ->
(size * pctErr).toInt()
}

val message = "size=$size,emptyLimit=$emptyLimit,emptyCount=$emptyCount"
// println(message)

assertTrue(
actual = emptyCount <= emptyLimit,
message = message
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2023 Matthew Nelson
*
* 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

import kotlin.test.Test
import kotlin.test.assertTrue
import kotlin.test.fail

/**
* See [EnsureFilledHelper]
* */
class SecureRandomUnitTest: EnsureFilledHelper() {

override val sRandom = SecureRandom()

@Test
fun givenNextBytesOf_whenCountNegative_thenThrows() {
try {
sRandom.nextBytesOf(-1)
fail()
} catch (_: IllegalArgumentException) {
// pass
}
}

@Test
fun givenNextBytesOf_whenCount0_thenReturnsEmpty() {
assertTrue(sRandom.nextBytesOf(0).isEmpty())
}

@Test
override fun givenByteArray_whenNextBytes_thenIsFilledWithData() {
super.givenByteArray_whenNextBytes_thenIsFilledWithData()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2023 Matthew Nelson
*
* 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.internal

import kotlinx.cinterop.Pinned
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.UnsafeNumber
import kotlinx.cinterop.convert
import platform.Security.SecRandomCopyBytes
import platform.Security.kSecRandomDefault
import org.kotlincrypto.SecRandomCopyException

/**
* https://developer.apple.com/documentation/security/1399291-secrandomcopybytes
* */
internal actual abstract class SecRandomDelegate private actual constructor() {

@Throws(SecRandomCopyException::class)
internal actual abstract fun nextBytesCopyTo(bytes: Pinned<ByteArray>, size: Int)

internal actual companion object: SecRandomDelegate() {

@OptIn(UnsafeNumber::class)
@Throws(SecRandomCopyException::class)
actual override fun nextBytesCopyTo(bytes: Pinned<ByteArray>, size: Int) {
// kSecRandomDefault is synonymous to NULL
val errno: Int = SecRandomCopyBytes(kSecRandomDefault, size.toUInt().convert(), bytes.addressOf(0))
if (errno != 0) {
throw errnoToSecRandomCopyException(errno)
}
}
}
}
86 changes: 86 additions & 0 deletions secure-random/src/jsMain/kotlin/org/kotlincrypto/SecureRandom.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2023 Matthew Nelson
*
* 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

import org.kotlincrypto.internal.commonNextBytesOf
import org.kotlincrypto.internal.ifNotNullOrEmpty

/**
* A cryptographically strong random number generator (RNG).
* */
public actual class SecureRandom public actual constructor() {

/**
* Returns a [ByteArray] of size [count], filled with
* securely generated random data.
*
* @throws [IllegalArgumentException] if [count] is negative.
* @throws [SecRandomCopyException] if [nextBytesCopyTo] failed.
* */
public actual fun nextBytesOf(count: Int): ByteArray = commonNextBytesOf(count)

/**
* Fills a [ByteArray] with securely generated random data.
* Does nothing if [bytes] is null or empty.
*
* Node: https://nodejs.org/api/crypto.html#cryptorandomfillsyncbuffer-offset-size
* Browser: https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues
*
* @throws [SecRandomCopyException] if procurement of securely random data failed.
* */
public actual fun nextBytesCopyTo(bytes: ByteArray?) {
bytes.ifNotNullOrEmpty {
try {
if (isNode) {
_require("crypto").randomFillSync(this)
} else {
global.crypto.getRandomValues(this)
}

Unit
} catch (t: Throwable) {
throw SecRandomCopyException("Failed to obtain bytes", t)
}
}
}

private companion object {
private val isNode: Boolean by lazy {
val runtime: String? = try {
// May not be available, but should be preferred
// method of determining runtime environment.
js("(globalThis.process.release.name)") as String
} catch (_: Throwable) {
null
}

when (runtime) {
null -> {
js("(typeof global !== 'undefined' && ({}).toString.call(global) == '[object global]')") as Boolean
}
"node" -> true
else -> false
}
}

private val global: dynamic by lazy {
js("((typeof global !== 'undefined') ? global : self)")
}

@Suppress("FunctionName", "UNUSED_PARAMETER")
private fun _require(name: String): dynamic = js("require(name)")
}
}
Loading

0 comments on commit 57e71f0

Please sign in to comment.