Skip to content
Draft
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
10 changes: 10 additions & 0 deletions packages/graalvm/api/graalvm.api
Original file line number Diff line number Diff line change
Expand Up @@ -4366,6 +4366,7 @@ public abstract interface class elide/runtime/intrinsics/js/node/ConsoleAPI : el
}

public abstract interface class elide/runtime/intrinsics/js/node/CryptoAPI : elide/runtime/intrinsics/js/node/NodeAPI {
public abstract fun createHash (Ljava/lang/String;)Lelide/runtime/node/crypto/NodeHash;
public abstract fun randomUUID (Lorg/graalvm/polyglot/Value;)Ljava/lang/String;
public static synthetic fun randomUUID$default (Lelide/runtime/intrinsics/js/node/CryptoAPI;Lorg/graalvm/polyglot/Value;ILjava/lang/Object;)Ljava/lang/String;
}
Expand Down Expand Up @@ -8890,6 +8891,15 @@ public final synthetic class elide/runtime/node/crypto/$NodeCryptoModule$Introsp
public fun isBuildable ()Z
}

public final class elide/runtime/node/crypto/NodeHash {
public fun <init> (Ljava/lang/String;Ljava/security/MessageDigest;Z)V
public synthetic fun <init> (Ljava/lang/String;Ljava/security/MessageDigest;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun copy ()Lelide/runtime/node/crypto/NodeHash;
public final fun digest ()Ljava/lang/Object;
public final fun digest (Ljava/lang/String;)Ljava/lang/Object;
public final fun update (Ljava/lang/Object;)Lelide/runtime/node/crypto/NodeHash;
}

public synthetic class elide/runtime/node/dgram/$NodeDatagramModule$Definition : io/micronaut/context/AbstractInitializableBeanDefinitionAndReference {
public static final field $ANNOTATION_METADATA Lio/micronaut/core/annotation/AnnotationMetadata;
public fun <init> ()V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package elide.runtime.intrinsics.js.node

import org.graalvm.polyglot.Value
import elide.annotations.API
import elide.runtime.node.crypto.NodeHash
import elide.vm.annotations.Polyglot

/**
Expand All @@ -30,4 +31,18 @@ import elide.vm.annotations.Polyglot
* @return A randomly generated 36 character UUID c4 string in lowercase format (e.g. "5cb34cef-5fc2-47e4-a3ac-4bb055fa2025")
*/
@Polyglot public fun randomUUID(options: Value? = null): String


/**
* ## Crypto: createHash
* Creates and returns a [NodeHash] object that can be used to update and generate hash digests using the specified algorithm.
*
* See also: [Node Crypto API: `createHash`](https://nodejs.org/api/crypto.html#cryptocreatehashalgorithm-options)
*
* @param algorithm The hash algorithm to use (e.g. "sha256", "md5", etc.)
* @return A [NodeHash] instance configured to use the specified algorithm.
*
* @TODO(elijahkotyluk) Support optional options parameter
*/
@Polyglot public fun createHash(algorithm: String): NodeHash
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ private const val CRYPTO_MODULE_SYMBOL = "node_${NodeModuleName.CRYPTO}"

// Functiopn name for randomUUID
private const val F_RANDOM_UUID = "randomUUID"
private val F_CREATE_HASH = "createHash"

// Installs the Node crypto module into the intrinsic bindings.
@Intrinsic internal class NodeCryptoModule : AbstractNodeBuiltinModule() {
Expand All @@ -54,6 +55,7 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
// Module members
private val moduleMembers = arrayOf(
F_RANDOM_UUID,
F_CREATE_HASH,
).apply { sort() }
}

Expand All @@ -63,6 +65,10 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
// It supports { disableEntropyCache: boolean } which is not applicable to our implementation
return java.util.UUID.randomUUID().toString()
}

@Polyglot override fun createHash(algorithm: String): NodeHash {
return NodeHash(algorithm)
}

// ProxyObject implementation
override fun getMemberKeys(): Array<String> = moduleMembers
Expand All @@ -76,6 +82,10 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
val options = args.getOrNull(0)
randomUUID(options)
}
F_CREATE_HASH -> ProxyExecutable { args ->
val algorithm = args.getOrNull(0)?.asString() ?: throw IllegalArgumentException("Algorithm required")
createHash(algorithm)
}
else -> null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2024-2025 Elide Technologies, Inc.
*
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://opensource.org/license/mit/
*
* 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 elide.runtime.node.crypto

import org.graalvm.polyglot.Value
import java.security.MessageDigest
import java.util.Base64
import elide.runtime.node.buffer.NodeHostBuffer
import elide.vm.annotations.Polyglot

// Map Node.js hash algorithm names to the JVM equivalent
private val NODE_TO_JVM_ALGORITHM = mapOf(
"md5" to "MD5",
"sha1" to "SHA-1",
"sha256" to "SHA-256",
"sha512" to "SHA-512",
"sha3-256" to "SHA3-256",
)

// @TODO(elijahkotyluk) Add support for an optional options parameter to configure output format, etc.
/**
* ## Node API: Hash
* Implements the Node.js `Hash` class for feeding data into the Hash object and creating hash digests.
* See also: [Node.js Crypto API: `Hash`](https://nodejs.org/api/crypto.html#class-hash)
*/
public class NodeHash(
private val algorithm: String,
md: MessageDigest? = null,
private var digested: Boolean = false
) {
private val md: MessageDigest = md ?: MessageDigest.getInstance(resolveAlgorithm(algorithm))

// @TODO(elijahkotyluk) add support for transform options
public fun copy(): NodeHash {
if (digested) throw IllegalStateException("Digest already called, cannot copy a finalized Hash.")

val mdClone = try {
md.clone() as MessageDigest // @TODO(elijahkotyluk) see if we can avoid having to cast
} catch (e: CloneNotSupportedException) {
// @TODO(elijahkotyluk) validate the error messaging and change as needed.
throw IllegalStateException(e.message ?: "Failed to clone MessageDigest instance")
}

// Create new NodeHash with the cloned digest
return NodeHash(
algorithm = this.algorithm,
md = mdClone,
digested = false
)
}
// Update the current hash with new data
public fun update(data: Any): NodeHash {
if (digested) throw IllegalStateException("Digest already called")

val bytes = when (data) {
is String -> data.toByteArray(Charsets.UTF_8)
is ByteArray -> data
is Value -> {
when {
data.hasArrayElements() -> {
val arr = ByteArray(data.arraySize.toInt())
for (i in arr.indices) {
arr[i] = (data.getArrayElement(i.toLong()).asInt() and 0xFF).toByte()
}
arr
}
data.isString -> data.asString().toByteArray(Charsets.UTF_8)
else -> throw IllegalArgumentException("Unsupported item type, must be of type string or an instance of Buffer, TypedArray, or Dataview. Received: ${data.javaClass.name}")
}
}
is Iterable<*> -> { // @TODO(elijahkotyluk) Handle Polyglot lists as an iterable, may need to revisit
val arr = data.map {
when (it) {
is Number -> it.toByte()
else -> throw IllegalArgumentException("Unsupported item type, must be of type string or an instance of Buffer, TypedArray, or Dataview. Received: ${it?.javaClass?.name}")
}
}.toByteArray()
arr
}
else -> throw IllegalArgumentException("The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or Dataview. Received an instance of: ${data::class}")
}

md.update(bytes)

return this
}

/**
* Public overload of [digestInternal].
* This is equivalent to calling [digestInternal] with a `null` encoding.
* @return The computed digest as a [ByteArray].
*/
@Polyglot public fun digest(): Any = digestInternal(null)

/**
* Public overload of [digestInternal].
* This is equivalent to calling [digestInternal] with the specified [encoding].
* @param encoding Encoding for the output digest.
* @return The computed digest in the specified encoding.
*/
@Polyglot public fun digest(encoding: String?): Any = digestInternal(encoding)

/**
* Compute the digest of the data passed to [update], returning it in the specified [encoding].
* @param encoding Optional encoding for the output digest. Supported values are:
* - `null` or `"buffer"`: returns a [ByteArray]
* - `"hex"`: returns a hexadecimal [String]
* - `"base64"`: returns a Base64-encoded [String]
* - `"latin1"`: returns a ISO-8859-1 encoded [String]
* @return The computed digest in the specified encoding.
*/
private fun digestInternal(encoding: String? = null): Any {
if (digested) throw IllegalStateException("Digest has already been called on this Hash instance.")
digested = true
val result = md.digest()

return when (encoding?.lowercase()) {
null, "buffer" -> NodeHostBuffer.wrap(result)
"hex" -> result.joinToString("") { "%02x".format(it) }
"base64" -> Base64.getEncoder().encodeToString(result)
"latin1" -> result.toString(Charsets.ISO_8859_1)
else -> throw IllegalArgumentException("Encoding: ${encoding} is not currently supported.")
}
}

// @TODO(elijahkotyluk) better error messaging should be added here
private fun resolveAlgorithm(nodeAlgo: String): String =
NODE_TO_JVM_ALGORITHM[nodeAlgo.lowercase()] ?: throw IllegalArgumentException("Unsupported algorithm: $nodeAlgo")
}
Loading
Loading