Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Doesn't work when using ESModule #8

Open
fiatjaf opened this issue Sep 18, 2022 · 8 comments
Open

Doesn't work when using ESModule #8

fiatjaf opened this issue Sep 18, 2022 · 8 comments
Labels
bug Something isn't working

Comments

@fiatjaf
Copy link

fiatjaf commented Sep 18, 2022

How to reproduce

Given the following files:

// package.json
{
  "type": "module"
}
// build.sbt
enablePlugins(ScalaJSPlugin)

scalaVersion := "3.1.3"

libraryDependencies ++= Seq(
  ("org.scala-js" %%% "scalajs-java-securerandom" % "1.0.0").cross(CrossVersion.for3Use2_13)
)

scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) }

scalaJSUseMainModuleInitializer := tru
// src/main/scala/Main.scala

import java.security.SecureRandom
  
object Main {  
  def main(args: Array[String]): Unit = {  
    println(randomBytes(16).map(_.toInt).mkString("")) 
  }                                                    
                                         
  private val secureRandom = new SecureRandom
                                             
  def randomBytes(length: Int): Array[Byte] = { 
    val buffer = new Array[Byte](length)       
    secureRandom.nextBytes(buffer)             
    buffer                              
  }                               
}

Do

sbt fastLinkJS
node target/scala-3.1.3/*-fastopt/main.js

The result will be

file:///home/fiatjaf/comp/misc/target/scala-3.1.3/misc-fastopt/main.js:2716
  throw new $c_jl_UnsupportedOperationException("java.security.SecureRandom is not supported on this platform because it provides neither `crypto.getRandomValues` nor Node.js' \"crypto\" module.")
        ^

<ref *1> java.lang.UnsupportedOperationException: java.security.SecureRandom is not supported on this platform because it provides neither `crypto.getRandomValues` nor Node.js' "crypto" module.
    at $p_Ljava_security_SecureRandom$__notSupported__E (file:///home/fiatjaf/comp/misc/target/scala-3.1.3/misc-fastopt/main.js:2716:9)
    at $p_Ljava_security_SecureRandom$__getRandomValuesFun$lzycompute__sjs_js_Function1 (file:///home/fiatjaf/comp/misc/target/scala-3.1.3/misc-fastopt/main.js:2710:128)
    at $c_Ljava_security_SecureRandom$.java$security$SecureRandom$$getRandomValuesFun__sjs_js_Function1 (file:///home/fiatjaf/comp/misc/target/scala-3.1.3/misc-fastopt/main.js:2746:62)
    at new $c_Ljava_security_SecureRandom (file:///home/fiatjaf/comp/misc/target/scala-3.1.3/misc-fastopt/main.js:3323:94)
    at new $c_LMain$ (file:///home/fiatjaf/comp/misc/target/scala-3.1.3/misc-fastopt/main.js:904:33)
    at $m_LMain$ (file:///home/fiatjaf/comp/misc/target/scala-3.1.3/misc-fastopt/main.js:1024:17)
    at $s_LMain__main__AT__V (file:///home/fiatjaf/comp/misc/target/scala-3.1.3/misc-fastopt/main.js:898:3)
    at file:///home/fiatjaf/comp/misc/target/scala-3.1.3/misc-fastopt/main.js:5965:1
    at ModuleJob.run (node:internal/modules/esm/module_job:198:25)
    at async Promise.all (index 0) {
  jl_Throwable__f_s: 'java.security.SecureRandom is not supported on this platform because it provides neither `crypto.getRandomValues` nor Node.js\' "crypto" module.',
  jl_Throwable__f_e: null,
  jl_Throwable__f_enableSuppression: true,
  jl_Throwable__f_writableStackTrace: true,
  jl_Throwable__f_stackTraceStateInternal: [Circular *1],
  jl_Throwable__f_stackTrace: null,
  jl_Throwable__f_suppressed: null
}

Node.js v18.4.0
@fiatjaf
Copy link
Author

fiatjaf commented Sep 18, 2022

It works if I manually put the crypto on the global scope (since I am running this on Node only) with:

object Main {
  @js.native
  @JSImport("crypto", JSImport.Namespace)
  val crypto: js.Dynamic = js.native
  private val g = scalajs.js.Dynamic.global.globalThis
  g.crypto = crypto

...

@sjrd
Copy link
Member

sjrd commented Sep 19, 2022

Thanks for the report.

I don't know how to fix this while retaining compatibility with other scenarios.

  • We cannot use a dynamic import() because we need a synchronous result.
  • We cannot use a static import because that would crash loading of the entire .js file if crypto is not available at all, for example in browsers.
  • We cannot (really) use createRequire to obtain a synchronous require, because we have to pass it import.meta.url. We can read import.meta from Scala.js, but it was only standardized in ES 2020. If we use it, loading the entire .js file will crash with a SyntaxError on engines that don't support it.

The least bad solution I have so far would be use createRequire anyway, using an import.meta.url obtained using run-time code generation, i.e., a new js.Function("return import.meta.url;"). That won't work in environments that forbid dynamic code evaluation, but it seems better than the status quo.

@sjrd sjrd added the bug Something isn't working label Sep 19, 2022
@sjrd
Copy link
Member

sjrd commented Sep 19, 2022

Apparently this cannot be reproduced through run/test. For some reason, when run through a NodeJSEnv, even ES modules receive require. I don't know why this happens. I tried with "type": "module" as well as with an explicit .mjs extension. In both cases, the tests succeed as is, using randomFillSync.

@armanbilge
Copy link
Member

Possibly related to the VM thing used by the Node.js JSEnv? See also scala-js/scala-js-js-envs#20 (comment).

@sjrd
Copy link
Member

sjrd commented Sep 19, 2022

We don't use the VM thing for ES modules. We import them through a dynamic import() call. We only use the VM thing for Scripts.

@fiatjaf
Copy link
Author

fiatjaf commented Sep 19, 2022

Maybe leave it as it is and tell ESModule users to do the manual loading like I did above?

At least until ES2020 lands on more platforms and that thing you mentioned becomes less prone to breaking?

@armanbilge
Copy link
Member

  • We cannot use a dynamic import() because we need a synchronous result.

If you don't mind expanding on this: hypothetically, what would this solution look like? I understand it's not helpful for Java SecureRandom, but it would be helpful for userland APIs that can do async things.


It won't necessarily help us today, but the Web Crypto API is now available in global scope as of Node.js 19+. Earlier versions can also enable this behavior with --experimental-global-webcrypto.

https://nodejs.org/en/blog/announcements/v19-release-announce/#stable-webcrypto

That would mean that a single API can be used for all platforms and the feature-test would no longer be needed, which I believe would solve this issue.

@sjrd
Copy link
Member

sjrd commented Nov 6, 2022

That would mean that a single API can be used for all platforms and the feature-test would no longer be needed, which I believe would solve this issue.

I think it even means that the existing release of scalajs-java-securerandom will work out of the box in Node.js 19, even with the ES module setup of this issue. The first thing it tries is the global crypto.

  • We cannot use a dynamic import() because we need a synchronous result.

If you don't mind expanding on this: hypothetically, what would this solution look like? I understand it's not helpful for Java SecureRandom, but it would be helpful for userland APIs that can do async things.

Something like

import scala.concurrent.ExecutionContext.Implicits.global
import scala.scalajs.js.Thenable.Implicits._
import scala.scalajs.js.JSConverters._

def randomBytes(n: Int): Future[Array[Byte]] = {
  for (cryptoMod <- js.`import`("crypto").toFuture) yield {
    val ta = new Int8Array(n)
    cryptoMod.randomFillSync(ta)
    ta.toArray
  }
}

(not tested nor even compiled; expect changes to be required)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants