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
1,475 changes: 1,458 additions & 17 deletions .kotlin-js-store/yarn.lock

Large diffs are not rendered by default.

23 changes: 20 additions & 3 deletions build-logic/src/main/kotlin/-KmpConfigurationExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import io.matthewnelson.kmp.configuration.extension.KmpConfigurationExtension
import io.matthewnelson.kmp.configuration.extension.container.target.KmpConfigurationContainerDsl
import org.gradle.api.Action
import org.gradle.api.JavaVersion
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl

fun KmpConfigurationExtension.configureShared(
publish: Boolean = false,
Expand All @@ -32,9 +33,25 @@ fun KmpConfigurationExtension.configureShared(
compileTargetCompatibility = JavaVersion.VERSION_1_8
}

js()
// wasmJs {}
// wasmWasi {}
js {
target {
browser()
nodejs()
}
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
target {
browser()
nodejs()
}
}
@OptIn(ExperimentalWasmDsl::class)
wasmWasi {
target {
nodejs()
}
}

androidNativeAll()

Expand Down
52 changes: 15 additions & 37 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension

plugins {
alias(libs.plugins.multiplatform) apply(false)
alias(libs.plugins.binaryCompat)
alias(libs.plugins.gradleVersions)
}

allprojects {
Expand All @@ -39,44 +40,21 @@ plugins.withType<YarnPlugin> {
the<YarnRootExtension>().lockFileDirectory = rootDir.resolve(".kotlin-js-store")
}

@Suppress("LocalVariableName")
apiValidation {
val CHECK_PUBLICATION = findProperty("CHECK_PUBLICATION") as? String

if (CHECK_PUBLICATION != null) {
ignoredProjects.add("check-publication")
} else {
ignoredProjects.add("sample")
plugins.withType<NodeJsRootPlugin> {
the<NodeJsRootExtension>().apply {
nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2"
nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary"
}
}

fun isNonStable(version: String): Boolean {
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) }
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
val isStable = stableKeyword || regex.matches(version)
return isStable.not()
}

tasks.withType<DependencyUpdatesTask> {
// Example 1: reject all non stable versions
rejectVersionIf {
isNonStable(candidate.version)
}

// Example 2: disallow release candidates as upgradable versions from stable versions
rejectVersionIf {
isNonStable(candidate.version) && !isNonStable(currentVersion)
tasks.withType<KotlinNpmInstallTask>().configureEach {
args.add("--ignore-engines")
}
}

// Example 3: using the full syntax
resolutionStrategy {
componentSelection {
@Suppress("RedundantSamConstructor")
all(Action {
if (isNonStable(candidate.version) && !isNonStable(currentVersion)) {
reject("Release candidate")
}
})
}
apiValidation {
if (findProperty("CHECK_PUBLICATION") != null) {
ignoredProjects.add("check-publication")
} else {
ignoredProjects.add("sample")
}
}
12 changes: 5 additions & 7 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
[versions]
binaryCompat = "0.13.2"
configuration = "0.1.5"
coroutines = "1.7.3"
gradleVersions = "0.50.0"
kotlin = "1.9.21"
publish = "0.25.3"
binaryCompat = "0.14.0"
configuration = "0.2.1"
coroutines = "1.8.0"
kotlin = "1.9.23"
publish = "0.27.0"

[libraries]
gradle-kmp-configuration = { module = "io.matthewnelson:gradle-kmp-configuration-plugin", version.ref = "configuration" }
Expand All @@ -16,5 +15,4 @@ kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-te

[plugins]
binaryCompat = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompat" }
gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" }
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

# https://gradle.org/release-checksums/
distributionSha256Sum=f2b9ed0faf8472cbe469255ae6c86eddb77076c75191741b4a462f33128dd419
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
distributionSha256Sum=85719317abd2112f021d4f41f09ec370534ba288432065f4b477b6a3b652910d
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
68 changes: 38 additions & 30 deletions secure-random/src/jsMain/kotlin/org/kotlincrypto/SecureRandom.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/*
* Copyright (c) 2023 Matthew Nelson
* Copyright (c) 2024 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
* 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,
Expand All @@ -17,6 +17,7 @@

package org.kotlincrypto

import org.khronos.webgl.Int8Array
import org.kotlincrypto.internal.commonNextBytesOf
import org.kotlincrypto.internal.ifNotNullOrEmpty

Expand All @@ -38,51 +39,58 @@ public actual class SecureRandom public actual constructor() {
* 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
* - [docs-node](https://nodejs.org/api/crypto.html#cryptorandomfillsyncbuffer-offset-size)
* - [docs-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 {
val array = unsafeCast<Int8Array>()

if (isNode) {
_require("crypto").randomFillSync(this)
crypto.randomFillSync(array)
} else {
global.crypto.getRandomValues(this)
var offset = 0
while (offset < size) {
val len = if (size > 65536) 65536 else size
crypto.getRandomValues(array.subarray(offset, offset + len))
offset += len
}
}

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
}
private val isNode: Boolean by lazy { isNodeJs() }
private val crypto: Crypto by lazy { if (isNode) cryptoNode() else cryptoBrowser() }
}
}

when (runtime) {
null -> {
js("(typeof global !== 'undefined' && ({}).toString.call(global) == '[object global]')") as Boolean
}
"node" -> true
else -> false
}
}
private fun cryptoNode(): Crypto = js("eval('require')('crypto')")
.unsafeCast<Crypto>()
private fun cryptoBrowser(): Crypto = js("(window ? (window.crypto ? window.crypto : window.msCrypto) : self.crypto)")
.unsafeCast<Crypto>()

private val global: dynamic by lazy {
js("((typeof global !== 'undefined') ? global : self)")
}
private fun isNodeJs(): Boolean = js(
"""
(typeof process !== 'undefined'
&& process.versions != null
&& process.versions.node != null) ||
(typeof window !== 'undefined'
&& typeof window.process !== 'undefined'
&& window.process.versions != null
&& window.process.versions.node != null)
"""
) as Boolean

@Suppress("FunctionName", "UNUSED_PARAMETER")
private fun _require(name: String): dynamic = js("require(name)")
}
private external class Crypto {
// Browser
fun getRandomValues(array: Int8Array)
// Node.js
fun randomFillSync(array: Int8Array)
}
101 changes: 101 additions & 0 deletions secure-random/src/wasmJsMain/kotlin/org/kotlincrypto/SecureRandom.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2024 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("ACTUAL_ANNOTATIONS_NOT_MATCH_EXPECT", "EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")

package org.kotlincrypto

import org.khronos.webgl.Int8Array
import org.khronos.webgl.get
import org.khronos.webgl.set
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.
*
* - [docs-node](https://nodejs.org/api/crypto.html#cryptorandomfillsyncbuffer-offset-size)
* - [docs-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 {
val array = Int8Array(size)

if (isNode) {
crypto.randomFillSync(array)
} else {
var offset = 0
while (offset < size) {
val len = if (size > 65536) 65536 else size
crypto.getRandomValues(array.subarray(offset, offset + len))
offset += len
}
}

for (i in indices) {
this[i] = array[i]
array[i] = 0
}
} catch (t: Throwable) {
throw SecRandomCopyException("Failed to obtain bytes", t)
}
}
}

private companion object {
private val isNode: Boolean by lazy { isNodeJs() }
private val crypto: Crypto by lazy { if (isNode) cryptoNode() else cryptoBrowser() }
}
}

private fun cryptoNode(): Crypto = js("eval('require')('crypto')")
private fun cryptoBrowser(): Crypto = js("(window ? (window.crypto ? window.crypto : window.msCrypto) : self.crypto)")

private fun isNodeJs(): Boolean = js(
"""
(typeof process !== 'undefined'
&& process.versions != null
&& process.versions.node != null) ||
(typeof window !== 'undefined'
&& typeof window.process !== 'undefined'
&& window.process.versions != null
&& window.process.versions.node != null)
"""
)

private external class Crypto: JsAny {
// Browser
fun getRandomValues(array: Int8Array)
// Node.js
fun randomFillSync(array: Int8Array)
}
Loading