Skip to content

Commit f7dc829

Browse files
committed
Migrate webcrypto module to use shared web declarations
1 parent a0cfb21 commit f7dc829

File tree

17 files changed

+202
-497
lines changed

17 files changed

+202
-497
lines changed

build-logic/src/main/kotlin/ckbuild/OptIns.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ object OptIns {
1111

1212
const val ExperimentalStdlibApi = "kotlin.ExperimentalStdlibApi"
1313
const val ExperimentalForeignApi = "kotlinx.cinterop.ExperimentalForeignApi"
14+
const val ExperimentalWasmJsInterop = "kotlin.js.ExperimentalWasmJsInterop"
1415
}

cryptography-providers/tests/src/wasmJsMain/kotlin/TestPlatform.wasmJs.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Copyright (c) 2023-2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5+
@file:OptIn(kotlin.js.ExperimentalWasmJsInterop::class)
6+
57
package dev.whyoleg.cryptography.providers.tests
68

79
internal actual val currentTestPlatform: TestPlatform = jsPlatform().run {

cryptography-providers/webcrypto/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023-2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright (c) 2023-2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
import ckbuild.*
@@ -20,6 +20,8 @@ kotlin {
2020
optIn.addAll(
2121
OptIns.DelicateCryptographyApi,
2222
OptIns.CryptographyProviderApi,
23+
24+
OptIns.ExperimentalWasmJsInterop,
2325
)
2426
freeCompilerArgs.add("-Xexpect-actual-classes")
2527
}
Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,107 @@
11
/*
2-
* Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright (c) 2024-2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
@file:Suppress("FunctionName")
66

77
package dev.whyoleg.cryptography.providers.webcrypto.internal
88

9-
// marker interface
10-
internal expect interface Algorithm
9+
import kotlin.js.*
1110

12-
internal expect fun Algorithm(name: String): Algorithm
11+
internal external interface Algorithm
1312

14-
internal expect fun AesKeyGenerationAlgorithm(name: String, length: Int): Algorithm
13+
internal fun Algorithm(name: String): Algorithm =
14+
js("({ name: name })")
1515

16-
internal expect fun AesCbcCipherAlgorithm(iv: ByteArray): Algorithm
16+
internal fun AesKeyGenerationAlgorithm(name: String, length: Int): Algorithm =
17+
js("({ name: name, length: length })")
1718

18-
internal expect fun AesCtrCipherAlgorithm(counter: ByteArray, length: Int): Algorithm
19+
internal fun AesCbcCipherAlgorithm(iv: ByteArray): Algorithm =
20+
jsAesCbcCipherAlgorithm(iv.toInt8Array())
1921

20-
internal expect fun AesGcmCipherAlgorithm(additionalData: ByteArray?, iv: ByteArray, tagLength: Int): Algorithm
22+
private fun jsAesCbcCipherAlgorithm(iv: Int8Array): Algorithm =
23+
js("({ name: 'AES-CBC', iv: iv })")
2124

22-
internal expect fun HmacKeyAlgorithm(hash: String, length: Int?): Algorithm
25+
internal fun AesCtrCipherAlgorithm(counter: ByteArray, length: Int): Algorithm =
26+
jsAesCtrCipherAlgorithm(counter.toInt8Array(), length)
2327

24-
internal expect fun EcKeyAlgorithm(
25-
name: String, //ECDSA | ECDH
26-
namedCurve: String, //P-256, P-384, P-521
27-
): Algorithm
28+
private fun jsAesCtrCipherAlgorithm(counter: Int8Array, length: Int): Algorithm =
29+
js("({ name: 'AES-CTR', counter: counter, length: length })")
2830

29-
internal expect val Algorithm.ecKeyAlgorithmNamedCurve: String
31+
internal fun AesGcmCipherAlgorithm(additionalData: ByteArray?, iv: ByteArray, tagLength: Int): Algorithm = when (additionalData) {
32+
null -> jsAesGcmCipherAlgorithm(iv.toInt8Array(), tagLength)
33+
else -> jsAesGcmCipherAlgorithm(iv.toInt8Array(), tagLength, additionalData.toInt8Array())
34+
}
3035

31-
internal expect fun EcdsaSignatureAlgorithm(hash: String): Algorithm
36+
private fun jsAesGcmCipherAlgorithm(iv: Int8Array, tagLength: Int): Algorithm =
37+
js("({ name: 'AES-GCM', iv: iv, tagLength: tagLength })")
3238

33-
internal expect fun EcdhKeyDeriveAlgorithm(publicKey: CryptoKey): Algorithm
39+
private fun jsAesGcmCipherAlgorithm(iv: Int8Array, tagLength: Int, additionalData: Int8Array): Algorithm =
40+
js("({ name: 'AES-GCM', iv: iv, tagLength: tagLength, additionalData: additionalData })")
3441

35-
internal expect fun Pbkdf2DeriveAlgorithm(hash: String, iterations: Int, salt: ByteArray): Algorithm
42+
internal fun HmacKeyAlgorithm(hash: String, length: Int?): Algorithm = when (length) {
43+
null -> jsHmacKeyAlgorithm(hash)
44+
else -> jsHmacKeyAlgorithm(hash, length)
45+
}
3646

37-
internal expect fun HkdfDeriveAlgorithm(hash: String, salt: ByteArray, info: ByteArray): Algorithm
47+
private fun jsHmacKeyAlgorithm(hash: String): Algorithm =
48+
js("({ name: 'HMAC', hash: hash })")
3849

39-
internal expect fun RsaKeyGenerationAlgorithm(
40-
name: String, //RSA-PSS | RSA-OAEP
41-
modulusLength: Int,
42-
publicExponent: ByteArray,
43-
hash: String,
44-
): Algorithm
50+
private fun jsHmacKeyAlgorithm(hash: String, length: Int): Algorithm =
51+
js("({ name: 'HMAC', hash: hash, length: length })")
4552

46-
internal expect fun RsaKeyImportAlgorithm(
47-
name: String, //RSA-PSS | RSA-OAEP
48-
hash: String,
49-
): Algorithm
53+
internal fun EcKeyAlgorithm(name: String, namedCurve: String): Algorithm =
54+
js("({ name: name, namedCurve: namedCurve })")
5055

51-
internal expect fun RsaOaepCipherAlgorithm(label: ByteArray?): Algorithm
56+
internal val Algorithm.ecKeyAlgorithmNamedCurve: String get() = ecKeyAlgorithmNamedCurve(this)
5257

53-
internal expect fun RsaPssSignatureAlgorithm(saltLength: Int): Algorithm
58+
@Suppress("UNUSED_PARAMETER")
59+
private fun ecKeyAlgorithmNamedCurve(algorithm: Algorithm): String = js("algorithm.namedCurve")
5460

55-
internal expect val Algorithm.rsaKeyAlgorithmHashName: String
61+
internal fun EcdsaSignatureAlgorithm(hash: String): Algorithm =
62+
js("({ name: 'ECDSA', hash: hash })")
63+
64+
internal fun EcdhKeyDeriveAlgorithm(publicKey: CryptoKey): Algorithm =
65+
js("({ name: 'ECDH', public: publicKey })")
66+
67+
internal fun Pbkdf2DeriveAlgorithm(hash: String, iterations: Int, salt: ByteArray): Algorithm =
68+
jsPbkdf2DeriveAlgorithm(hash, iterations, salt.toInt8Array())
69+
70+
private fun jsPbkdf2DeriveAlgorithm(hash: String, iterations: Int, salt: Int8Array): Algorithm =
71+
js("({ name: 'PBKDF2', hash: hash, iterations: iterations, salt: salt })")
72+
73+
internal fun HkdfDeriveAlgorithm(hash: String, salt: ByteArray, info: ByteArray): Algorithm =
74+
jsHkdfDeriveAlgorithm(hash, salt.toInt8Array(), info.toInt8Array())
75+
76+
private fun jsHkdfDeriveAlgorithm(hash: String, salt: Int8Array, info: Int8Array): Algorithm =
77+
js("({ name: 'HKDF', hash: hash, salt: salt, info: info })")
78+
79+
internal fun RsaKeyGenerationAlgorithm(name: String, modulusLength: Int, publicExponent: ByteArray, hash: String): Algorithm {
80+
val publicExponent2 = publicExponent.toInt8Array().let { Uint8Array(it.buffer, it.byteOffset, it.length) }
81+
return jsRsaKeyGenerationAlgorithm(name, modulusLength, publicExponent2, hash)
82+
}
83+
84+
private fun jsRsaKeyGenerationAlgorithm(name: String, modulusLength: Int, publicExponent: Uint8Array, hash: String): Algorithm =
85+
js("({ name: name, modulusLength: modulusLength, publicExponent: publicExponent, hash: hash })")
86+
87+
internal fun RsaKeyImportAlgorithm(name: String, hash: String): Algorithm =
88+
js("({ name: name, hash: hash })")
89+
90+
internal fun RsaOaepCipherAlgorithm(label: ByteArray?): Algorithm = when (label) {
91+
null -> jsRsaOaepCipherAlgorithm()
92+
else -> jsRsaOaepCipherAlgorithm(label.toInt8Array())
93+
}
94+
95+
private fun jsRsaOaepCipherAlgorithm(): Algorithm =
96+
js("({ name: 'RSA-OAEP' })")
97+
98+
private fun jsRsaOaepCipherAlgorithm(label: Int8Array): Algorithm =
99+
js("({ name: 'RSA-OAEP', label: label })")
100+
101+
internal fun RsaPssSignatureAlgorithm(saltLength: Int): Algorithm =
102+
js("({ name: 'RSA-PSS', saltLength: saltLength })")
103+
104+
internal val Algorithm.rsaKeyAlgorithmHashName: String get() = rsaKeyAlgorithmHashName(this)
105+
106+
@Suppress("UNUSED_PARAMETER")
107+
private fun rsaKeyAlgorithmHashName(algorithm: Algorithm): String = js("algorithm.hash.name")

cryptography-providers/webcrypto/src/commonMain/kotlin/internal/Keys.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44

55
package dev.whyoleg.cryptography.providers.webcrypto.internal
66

7-
internal expect interface CryptoKey
7+
import kotlin.js.*
88

9-
internal expect interface CryptoKeyPair {
9+
internal external interface CryptoKey : JsAny
10+
11+
internal external interface CryptoKeyPair : JsAny {
1012
val privateKey: CryptoKey
1113
val publicKey: CryptoKey
1214
}
1315

14-
internal expect val CryptoKey.algorithm: Algorithm
16+
internal val CryptoKey.algorithm: Algorithm get() = keyAlgorithm(this)
17+
18+
@Suppress("UNUSED_PARAMETER")
19+
private fun keyAlgorithm(key: CryptoKey): Algorithm = js("key.algorithm")
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
package dev.whyoleg.cryptography.providers.webcrypto.internal
66

7-
import kotlin.js.Promise
7+
import kotlin.js.*
88

99
internal external interface SubtleCrypto {
1010
fun digest(algorithmName: String, data: Int8Array): Promise<ArrayBuffer>
Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,71 @@
11
/*
2-
* Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright (c) 2024-2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package dev.whyoleg.cryptography.providers.webcrypto.internal
66

7-
internal expect object WebCrypto {
8-
suspend fun digest(algorithmName: String, data: ByteArray): ByteArray
7+
import kotlin.js.*
8+
import kotlin.text.decodeToString
9+
import kotlin.text.encodeToByteArray
910

10-
suspend fun encrypt(algorithm: Algorithm, key: CryptoKey, data: ByteArray): ByteArray
11-
suspend fun decrypt(algorithm: Algorithm, key: CryptoKey, data: ByteArray): ByteArray
11+
internal object WebCrypto {
12+
private val subtle = getSubtleCrypto()
1213

13-
suspend fun sign(algorithm: Algorithm, key: CryptoKey, data: ByteArray): ByteArray
14-
suspend fun verify(algorithm: Algorithm, key: CryptoKey, signature: ByteArray, data: ByteArray): Boolean
1514

16-
suspend fun deriveBits(algorithm: Algorithm, baseKey: CryptoKey, length: Int): ByteArray
15+
suspend fun digest(algorithmName: String, data: ByteArray): ByteArray {
16+
return subtle.digest(algorithmName, data.toInt8Array()).await().toByteArray()
17+
}
18+
19+
suspend fun encrypt(algorithm: Algorithm, key: CryptoKey, data: ByteArray): ByteArray {
20+
return subtle.encrypt(algorithm, key, data.toInt8Array()).await().toByteArray()
21+
}
22+
23+
suspend fun decrypt(algorithm: Algorithm, key: CryptoKey, data: ByteArray): ByteArray {
24+
return subtle.decrypt(algorithm, key, data.toInt8Array()).await().toByteArray()
25+
}
26+
27+
suspend fun sign(algorithm: Algorithm, key: CryptoKey, data: ByteArray): ByteArray {
28+
return subtle.sign(algorithm, key, data.toInt8Array()).await().toByteArray()
29+
}
30+
31+
suspend fun verify(algorithm: Algorithm, key: CryptoKey, signature: ByteArray, data: ByteArray): Boolean {
32+
return subtle.verify(algorithm, key, signature.toInt8Array(), data.toInt8Array()).await().toBoolean()
33+
}
34+
35+
suspend fun deriveBits(algorithm: Algorithm, baseKey: CryptoKey, length: Int): ByteArray {
36+
return subtle.deriveBits(algorithm, baseKey, length).await().toByteArray()
37+
}
1738

1839
suspend fun importKey(
1940
format: String,
2041
keyData: ByteArray,
2142
algorithm: Algorithm,
2243
extractable: Boolean,
2344
keyUsages: Array<String>,
24-
): CryptoKey
45+
): CryptoKey {
46+
val key = when (format) {
47+
"jwk" -> jsonParse(keyData.decodeToString())
48+
else -> keyData.toInt8Array()
49+
}
50+
return subtle.importKey(format, key, algorithm, extractable, keyUsages.toJsArray()).await()
51+
}
2552

26-
suspend fun exportKey(format: String, key: CryptoKey): ByteArray
53+
suspend fun exportKey(format: String, key: CryptoKey): ByteArray {
54+
val keyData = subtle.exportKey(format, key).await()
55+
return when (format) {
56+
"jwk" -> jsonStringify(keyData).encodeToByteArray()
57+
else -> keyData.unsafeCast<ArrayBuffer>().toByteArray()
58+
}
59+
}
2760

28-
suspend fun generateKey(algorithm: Algorithm, extractable: Boolean, keyUsages: Array<String>): CryptoKey
61+
suspend fun generateKey(algorithm: Algorithm, extractable: Boolean, keyUsages: Array<String>): CryptoKey {
62+
return subtle.generateKey(algorithm, extractable, keyUsages.toJsArray()).await()
63+
}
2964

30-
suspend fun generateKeyPair(algorithm: Algorithm, extractable: Boolean, keyUsages: Array<String>): CryptoKeyPair
65+
suspend fun generateKeyPair(algorithm: Algorithm, extractable: Boolean, keyUsages: Array<String>): CryptoKeyPair {
66+
return subtle.generateKeyPair(algorithm, extractable, keyUsages.toJsArray()).await()
67+
}
3168
}
69+
70+
private fun jsonParse(string: String): JsAny = js("JSON.parse(string)")
71+
private fun jsonStringify(any: JsAny): String = js("JSON.stringify(any)")
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package dev.whyoleg.cryptography.providers.webcrypto.internal
6+
7+
import kotlin.js.*
8+
9+
internal external class ArrayBuffer : JsAny
10+
11+
internal external class Int8Array : JsAny {
12+
constructor(length: Int)
13+
constructor(buffer: ArrayBuffer)
14+
15+
val length: Int
16+
val buffer: ArrayBuffer
17+
val byteOffset: Int
18+
val byteLength: Int
19+
fun subarray(start: Int, end: Int): Int8Array
20+
}
21+
22+
internal external class Uint8Array : JsAny {
23+
constructor(buffer: ArrayBuffer, byteOffset: Int, length: Int)
24+
}
25+
26+
internal expect fun ByteArray.toInt8Array(): Int8Array
27+
28+
internal expect fun ArrayBuffer.toByteArray(): ByteArray
29+
30+
internal expect fun Int8Array.toByteArray(): ByteArray
31+
32+
internal expect suspend fun <T : JsAny> Promise<T>.await(): T
33+
34+
internal fun Array<String>.toJsArray(): JsArray<JsString> = JsArray<JsString>().also {
35+
forEachIndexed { index, value ->
36+
it[index] = value.toJsString()
37+
}
38+
}

0 commit comments

Comments
 (0)