Skip to content
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 @@ -28,8 +28,9 @@ import elide.vm.annotations.Polyglot
// Internal symbol where the Node built-in module is installed.
private const val CRYPTO_MODULE_SYMBOL = "node_${NodeModuleName.CRYPTO}"

// Functiopn name for randomUUID
// Function name for randomUUID
private const val F_RANDOM_UUID = "randomUUID"
private const 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("The \"algorithm\" argument must be of type string. received: ${args[0]}")
createHash(algorithm)
}
else -> null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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
} catch (e: CloneNotSupportedException) {
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 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