Skip to content

Commit

Permalink
Merge pull request #2945 from armanbilge/topic/CVE-2022-28355
Browse files Browse the repository at this point in the history
Securely implement `UUIDGen` for Scala.js
  • Loading branch information
armanbilge committed Apr 5, 2022
2 parents 913367b + d591e21 commit 69fc9d3
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 35 deletions.
107 changes: 79 additions & 28 deletions std/js/src/main/scala/cats/effect/std/JavaSecureRandom.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
}
}
Original file line number Diff line number Diff line change
@@ -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
*
* https://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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
*
* https://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())
}
}
8 changes: 1 addition & 7 deletions std/shared/src/main/scala/cats/effect/std/UUIDGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package cats.effect.std

import cats.Functor
import cats.effect.kernel.Sync
import cats.implicits._

import java.util.UUID
Expand All @@ -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)
}
36 changes: 36 additions & 0 deletions tests/shared/src/test/scala/cats/effect/std/UUIDGenSpec.scala
Original file line number Diff line number Diff line change
@@ -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
*
* https://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))
}
}

}

0 comments on commit 69fc9d3

Please sign in to comment.