-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
SecureRandom
implementation (#2)
- Loading branch information
Showing
22 changed files
with
1,274 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
23 changes: 23 additions & 0 deletions
23
secure-random/src/commonMain/kotlin/org/kotlincrypto/SecRandomCopyException.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
41 changes: 41 additions & 0 deletions
41
secure-random/src/commonMain/kotlin/org/kotlincrypto/SecureRandom.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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?) | ||
} |
44 changes: 44 additions & 0 deletions
44
secure-random/src/commonMain/kotlin/org/kotlincrypto/internal/-SecureRandomCommon.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
87 changes: 87 additions & 0 deletions
87
secure-random/src/commonTest/kotlin/org/kotlincrypto/EnsureFilledHelper.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
secure-random/src/commonTest/kotlin/org/kotlincrypto/SecureRandomUnitTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
secure-random/src/darwinMain/kotlin/org/kotlincrypto/internal/SecRandomDelegate.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
86
secure-random/src/jsMain/kotlin/org/kotlincrypto/SecureRandom.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)") | ||
} | ||
} |
Oops, something went wrong.