From d6be25b2a46c90fa54e36a988465a6c728ae5a2a Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 5 Apr 2022 01:21:01 +0000 Subject: [PATCH 1/2] Swap in Scala.js SecureRandom implementation --- .../cats/effect/std/JavaSecureRandom.scala | 107 +++++++++++++----- 1 file changed, 79 insertions(+), 28 deletions(-) diff --git a/std/js/src/main/scala/cats/effect/std/JavaSecureRandom.scala b/std/js/src/main/scala/cats/effect/std/JavaSecureRandom.scala index 48a6ee9223..9009241474 100644 --- a/std/js/src/main/scala/cats/effect/std/JavaSecureRandom.scala +++ b/std/js/src/main/scala/cats/effect/std/JavaSecureRandom.scala @@ -14,46 +14,97 @@ * limitations under the License. */ +/* + * scalajs-java-securerandom (https://github.com/scala-js/scala-js-java-securerandom) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + package cats.effect.std import scala.scalajs.js +import scala.scalajs.js.typedarray._ -private final class JavaSecureRandom extends java.util.Random { +// The seed in java.util.Random will be unused, so set to 0L instead of having to generate one +private[std] class JavaSecureRandom() extends java.util.Random(0L) { + // Make sure to resolve the appropriate function no later than the first instantiation + private val getRandomValuesFun = JavaSecureRandom.getRandomValuesFun - private[this] val nextBytes: Int => js.typedarray.Int8Array = - if (js.typeOf(js.Dynamic.global.crypto) != "undefined") // browsers - { numBytes => - val bytes = new js.typedarray.Int8Array(numBytes) - js.Dynamic.global.crypto.getRandomValues(bytes) - bytes - } else { - val crypto = js.Dynamic.global.require("crypto") - - // Node.js - { numBytes => - val bytes = crypto.randomBytes(numBytes).asInstanceOf[js.typedarray.Uint8Array] - new js.typedarray.Int8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength) - - } - } + /* setSeed has no effect. For cryptographically secure PRNGs, giving a seed + * can only ever increase the entropy. It is never allowed to decrease it. + * Given that we don't have access to an API to strengthen the entropy of the + * underlying PRNG, it's fine to ignore it instead. + * + * Note that the doc of `SecureRandom` says that it will seed itself upon + * first call to `nextBytes` or `next`, if it has not been seeded yet. This + * suggests that an *initial* call to `setSeed` would make a `SecureRandom` + * instance deterministic. Experimentally, this does not seem to be the case, + * however, so we don't spend extra effort to make that happen. + */ + override def setSeed(x: Long): Unit = () override def nextBytes(bytes: Array[Byte]): Unit = { - nextBytes(bytes.length).copyToArray(bytes) - () + val len = bytes.length + val buffer = new Int8Array(len) + getRandomValuesFun(buffer) + var i = 0 + while (i != len) { + bytes(i) = buffer(i) + i += 1 + } } override protected final def next(numBits: Int): Int = { - val numBytes = (numBits + 7) / 8 - val b = new js.typedarray.Int8Array(nextBytes(numBytes).buffer) - var next = 0 - - var i = 0 - while (i < numBytes) { - next = (next << 8) + (b(i) & 0xff) - i += 1 + if (numBits <= 0) { + 0 // special case because the formula on the last line is incorrect for numBits == 0 + } else { + val buffer = new Int32Array(1) + getRandomValuesFun(buffer) + val rand32 = buffer(0) + rand32 & (-1 >>> (32 - numBits)) // Clear the (32 - numBits) higher order bits } + } +} - next >>> (numBytes * 8 - numBits) +private[std] object JavaSecureRandom { + private lazy val getRandomValuesFun: js.Function1[ArrayBufferView, Unit] = { + if (js.typeOf(js.Dynamic.global.crypto) != "undefined" && + js.typeOf(js.Dynamic.global.crypto.getRandomValues) == "function") { + { (buffer: ArrayBufferView) => + js.Dynamic.global.crypto.getRandomValues(buffer) + () + } + } else if (js.typeOf(js.Dynamic.global.require) == "function") { + try { + val crypto = js.Dynamic.global.require("crypto") + if (js.typeOf(crypto.randomFillSync) == "function") { + { (buffer: ArrayBufferView) => + crypto.randomFillSync(buffer) + () + } + } else { + notSupported + } + } catch { + case _: Throwable => + notSupported + } + } else { + notSupported + } } + private def notSupported: Nothing = { + throw new UnsupportedOperationException( + "java.security.SecureRandom is not supported on this platform " + + "because it provides neither `crypto.getRandomValues` nor " + + "Node.js' \"crypto\" module." + ) + } } From d591e21a6021e642f869b78184e00973f5c4c0c7 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 5 Apr 2022 02:31:52 +0000 Subject: [PATCH 2/2] Securely implement UUIDGen for Scala.js --- .../effect/std/UUIDGenCompanionPlatform.scala | 62 +++++++++++++++++++ .../effect/std/UUIDGenCompanionPlatform.scala | 28 +++++++++ .../main/scala/cats/effect/std/UUIDGen.scala | 8 +-- .../scala/cats/effect/std/UUIDGenSpec.scala | 36 +++++++++++ 4 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 std/js/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala create mode 100644 std/jvm/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala create mode 100644 tests/shared/src/test/scala/cats/effect/std/UUIDGenSpec.scala diff --git a/std/js/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala b/std/js/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala new file mode 100644 index 0000000000..2b3f5773d2 --- /dev/null +++ b/std/js/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala @@ -0,0 +1,62 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 + * + * http://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. + */ + +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package cats.effect.std + +import cats.effect.kernel.Sync + +import java.util.UUID + +private[std] trait UUIDGenCompanionPlatform { + implicit def fromSync[F[_]](implicit ev: Sync[F]): UUIDGen[F] = new UUIDGen[F] { + private val csprng = new JavaSecureRandom() + private val randomUUIDBuffer = new Array[Byte](16) + override final val randomUUID: F[UUID] = + ev.delay { + val buffer = randomUUIDBuffer // local copy + + /* We use nextBytes() because that is the primitive of most secure RNGs, + * and therefore it allows to perform a unique call to the underlying + * secure RNG. + */ + csprng.nextBytes(randomUUIDBuffer) + + @inline def intFromBuffer(i: Int): Int = + (buffer(i) << 24) | ((buffer(i + 1) & 0xff) << 16) | ((buffer( + i + 2) & 0xff) << 8) | (buffer(i + 3) & 0xff) + + val i1 = intFromBuffer(0) + val i2 = (intFromBuffer(4) & ~0x0000f000) | 0x00004000 + val i3 = (intFromBuffer(8) & ~0xc0000000) | 0x80000000 + val i4 = intFromBuffer(12) + val msb = (i1.toLong << 32) | (i2.toLong & 0xffffffffL) + val lsb = (i3.toLong << 32) | (i4.toLong & 0xffffffffL) + new UUID(msb, lsb) + } + } +} diff --git a/std/jvm/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala b/std/jvm/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala new file mode 100644 index 0000000000..437746ef77 --- /dev/null +++ b/std/jvm/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 + * + * http://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. + */ + +package cats.effect.std + +import cats.effect.kernel.Sync + +import java.util.UUID + +private[std] trait UUIDGenCompanionPlatform { + implicit def fromSync[F[_]](implicit ev: Sync[F]): UUIDGen[F] = new UUIDGen[F] { + override final val randomUUID: F[UUID] = + ev.blocking(UUID.randomUUID()) + } +} diff --git a/std/shared/src/main/scala/cats/effect/std/UUIDGen.scala b/std/shared/src/main/scala/cats/effect/std/UUIDGen.scala index 0e98462057..4e33229037 100644 --- a/std/shared/src/main/scala/cats/effect/std/UUIDGen.scala +++ b/std/shared/src/main/scala/cats/effect/std/UUIDGen.scala @@ -17,7 +17,6 @@ package cats.effect.std import cats.Functor -import cats.effect.kernel.Sync import cats.implicits._ import java.util.UUID @@ -35,14 +34,9 @@ trait UUIDGen[F[_]] { def randomUUID: F[UUID] } -object UUIDGen { +object UUIDGen extends UUIDGenCompanionPlatform { def apply[F[_]](implicit ev: UUIDGen[F]): UUIDGen[F] = ev - implicit def fromSync[F[_]](implicit ev: Sync[F]): UUIDGen[F] = new UUIDGen[F] { - override final val randomUUID: F[UUID] = - ev.blocking(UUID.randomUUID()) - } - def randomUUID[F[_]: UUIDGen]: F[UUID] = UUIDGen[F].randomUUID def randomString[F[_]: UUIDGen: Functor]: F[String] = randomUUID.map(_.toString) } diff --git a/tests/shared/src/test/scala/cats/effect/std/UUIDGenSpec.scala b/tests/shared/src/test/scala/cats/effect/std/UUIDGenSpec.scala new file mode 100644 index 0000000000..77c3e6c82e --- /dev/null +++ b/tests/shared/src/test/scala/cats/effect/std/UUIDGenSpec.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 + * + * http://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. + */ + +package cats.effect +package std + +class UUIDGenSpec extends BaseSpec { + + "UUIDGen" should { + "securely generate UUIDs" in real { + for { + left <- UUIDGen.randomUUID[IO] + right <- UUIDGen.randomUUID[IO] + } yield left != right + } + "use the correct variant and version" in real { + for { + uuid <- UUIDGen.randomUUID[IO] + } yield (uuid.variant should be_==(2)) and (uuid.version should be_==(4)) + } + } + +}