diff --git a/.gitignore b/.gitignore index 81612a7ae4..69716753a3 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ metals.sbt # npm node_modules/ + +# nix flake +.direnv/ diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index f2d2c0681c..f57e985528 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -40,7 +40,15 @@ import cats.data.Ior import cats.effect.instances.spawn import cats.effect.kernel.CancelScope import cats.effect.kernel.GenTemporal.handleDuration -import cats.effect.std.{Backpressure, Console, Env, Supervisor, SystemProperties, UUIDGen} +import cats.effect.std.{ + Backpressure, + Console, + Env, + SecureRandom, + Supervisor, + SystemProperties, + UUIDGen +} import cats.effect.tracing.{Tracing, TracingEvent} import cats.effect.unsafe.IORuntime import cats.syntax._ @@ -2180,6 +2188,9 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits with TuplePara // later in the file private[this] val _never: IO[Nothing] = asyncForIO.never + implicit lazy val secureRandom: SecureRandom[IO] = + SecureRandom.unsafeJavaSecuritySecureRandom[IO]() + // implementations private[effect] final case class Pure[+A](value: A) extends IO[A] { diff --git a/std/js-native/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala b/std/js-native/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala index 2d3edfeac5..bcc6245edf 100644 --- a/std/js-native/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala +++ b/std/js-native/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala @@ -30,33 +30,16 @@ package cats.effect.std import cats.effect.kernel.Sync -import java.util.UUID +private[std] trait UUIDGenCompanionPlatform extends UUIDGenCompanionPlatformLowPriority -private[std] trait UUIDGenCompanionPlatform { - implicit def fromSync[F[_]](implicit ev: Sync[F]): UUIDGen[F] = new UUIDGen[F] { - private val csprng = new SecureRandom.JavaSecureRandom() - private val randomUUIDBuffer = new Array[Byte](16) - override final val randomUUID: F[UUID] = - ev.delay { - val buffer = randomUUIDBuffer // local copy +private[std] trait UUIDGenCompanionPlatformLowPriority { - /* 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) - } + @deprecated( + "Put an implicit `SecureRandom.javaSecuritySecureRandom` into scope to get a more efficient `UUIDGen`, or directly call `UUIDGen.fromSecureRandom`", + "3.6.0" + ) + implicit def fromSync[F[_]](implicit ev: Sync[F]): UUIDGen[F] = { + UUIDGen.fromSecureRandom[F](ev, SecureRandom.unsafeJavaSecuritySecureRandom()) } + } diff --git a/std/js/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala b/std/js/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala index cb8e5ee6a2..ded0dc69c6 100644 --- a/std/js/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala +++ b/std/js/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala @@ -28,6 +28,10 @@ package cats.effect.std +import cats.Applicative +import cats.effect.kernel.Sync +import cats.effect.std.Random.ScalaRandom + import scala.scalajs.js import scala.scalajs.js.typedarray._ @@ -110,4 +114,10 @@ private[std] trait SecureRandomCompanionPlatform { } } + def javaSecuritySecureRandom[F[_]: Sync]: F[SecureRandom[F]] = + Sync[F].delay(unsafeJavaSecuritySecureRandom()) + + private[effect] def unsafeJavaSecuritySecureRandom[F[_]: Sync](): SecureRandom[F] = + new ScalaRandom[F](Applicative[F].pure(new JavaSecureRandom())) with SecureRandom[F] {} + } diff --git a/std/jvm/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala b/std/jvm/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala index 0583939ce9..46ca8819b4 100644 --- a/std/jvm/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala +++ b/std/jvm/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala @@ -14,8 +14,107 @@ * limitations under the License. */ -package cats.effect.std +package cats +package effect +package std + +import cats.effect.kernel._ +import cats.effect.std.Random.ScalaRandom + +import scala.util.{Random => SRandom, Try} +import scala.util.control.NonFatal + +import java.util.concurrent.atomic.AtomicInteger private[std] trait SecureRandomCompanionPlatform { private[std] type JavaSecureRandom = java.security.SecureRandom + private def getInstance: JavaSecureRandom = + java.security.SecureRandom.getInstance("NativePRNGNonBlocking") + + private def javaUtilRandom[F[_]: Sync](random: JavaSecureRandom): SecureRandom[F] = + new ScalaRandom[F](Applicative[F].pure(random)) with SecureRandom[F] {} + + /** + * Creates a blocking Random instance. + * + * @param random + * a potentially blocking instance of java.util.Random + */ + private def javaUtilRandomBlocking[F[_]: Sync](random: JavaSecureRandom): SecureRandom[F] = + new ScalaRandom[F](Applicative[F].pure(random), Sync.Type.Blocking) with SecureRandom[F] {} + + def javaSecuritySecureRandom[F[_]: Sync]: F[SecureRandom[F]] = + Sync[F].delay(unsafeJavaSecuritySecureRandom()) + + /** + * Ported from https://github.com/http4s/http4s/. + */ + private[effect] def unsafeJavaSecuritySecureRandom[F[_]: Sync](): SecureRandom[F] = { + // This is a known, non-blocking, threadsafe algorithm + def happyRandom = getInstance + + def fallback = new JavaSecureRandom() + + // Porting JavaSecureRandom.isThreadSafe + def isThreadsafe(rnd: JavaSecureRandom) = + rnd + .getProvider + .getProperty("SecureRandom." + rnd.getAlgorithm + " ThreadSafe", "false") + .toBoolean + + // If we can't sniff out a more optimal solution, we can always + // fall back to a pool of blocking instances + def fallbackPool: SecureRandom[F] = { + val processors = Runtime.getRuntime.availableProcessors() + pool(processors) + } + + javaMajorVersion match { + case Some(major) if major > 8 => + try { + // We are thread safe and non-blocking. This is the + // happy path, and happily, the common path. + javaUtilRandom(happyRandom) + } catch { + case ex if NonFatal(ex) => + fallback match { + case rnd if isThreadsafe(rnd) => + // We avoided the mutex, but not the blocking. Use a + // shared instance from the blocking pool. + javaUtilRandomBlocking(rnd) + case _ => + // We can't prove the instance is threadsafe, so we need + // to pessimistically fall back to a pool. This should + // be exceedingly uncommon. + fallbackPool + } + } + + case Some(_) | None => + // We can't guarantee we're not stuck in a mutex. + fallbackPool + } + } + + private def pool[F[_]: Sync](n: Int): SecureRandom[F] = { + val index = new AtomicInteger(0) + val array = Array.fill(n)(new SRandom(new SecureRandom.JavaSecureRandom)) + + def selectRandom: F[SRandom] = Sync[F].delay { + val currentIndex = index.getAndUpdate(i => (i + 1) % n) + array(currentIndex) + } + + new ScalaRandom[F](selectRandom) with SecureRandom[F] {} + } + + private def javaMajorVersion: Option[Int] = + Option(System.getProperty("java.version")).flatMap(parseJavaMajorVersion) + + private def parseJavaMajorVersion(javaVersion: String): Option[Int] = + if (javaVersion.startsWith("1.")) + Try(javaVersion.split("\\.")(1).toInt).toOption + else + Try(javaVersion.split("\\.")(0).toInt).toOption + } diff --git a/std/jvm/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala b/std/jvm/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala index 7794e51440..2fa50db0f1 100644 --- a/std/jvm/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala +++ b/std/jvm/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala @@ -20,9 +20,17 @@ import cats.effect.kernel.Sync import java.util.UUID -private[std] trait UUIDGenCompanionPlatform { +private[std] trait UUIDGenCompanionPlatform extends UUIDGenCompanionPlatformLowPriority + +private[std] trait UUIDGenCompanionPlatformLowPriority { + + @deprecated( + "Put an implicit `SecureRandom.javaSecuritySecureRandom` into scope to get a more efficient `UUIDGen`, or directly call `UUIDGen.fromSecureRandom`", + "3.6.0" + ) 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/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala b/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala index b8c8cbcb72..f338383b91 100644 --- a/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala +++ b/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala @@ -16,6 +16,10 @@ package cats.effect.std +import cats.Applicative +import cats.effect.kernel.Sync +import cats.effect.std.Random.ScalaRandom + import org.typelevel.scalaccompat.annotation._ import scala.scalanative.libc.errno._ @@ -53,6 +57,12 @@ private[std] trait SecureRandomCompanionPlatform { } + def javaSecuritySecureRandom[F[_]: Sync]: F[SecureRandom[F]] = + Sync[F].delay(unsafeJavaSecuritySecureRandom()) + + private[effect] def unsafeJavaSecuritySecureRandom[F[_]: Sync](): SecureRandom[F] = + new ScalaRandom[F](Applicative[F].pure(new JavaSecureRandom())) with SecureRandom[F] {} + } @extern diff --git a/std/shared/src/main/scala/cats/effect/std/Random.scala b/std/shared/src/main/scala/cats/effect/std/Random.scala index 3268db340b..dd84c45e64 100644 --- a/std/shared/src/main/scala/cats/effect/std/Random.scala +++ b/std/shared/src/main/scala/cats/effect/std/Random.scala @@ -434,17 +434,21 @@ object Random extends RandomCompanionPlatform { } - private[std] abstract class ScalaRandom[F[_]: Sync](f: F[SRandom]) extends RandomCommon[F] { + private[std] abstract class ScalaRandom[F[_]: Sync](f: F[SRandom], hint: Sync.Type) + extends RandomCommon[F] { + + def this(f: F[SRandom]) = this(f, Sync.Type.Delay) + def nextBoolean: F[Boolean] = for { r <- f - out <- Sync[F].delay(r.nextBoolean()) + out <- Sync[F].suspend(hint)(r.nextBoolean()) } yield out def nextBytes(n: Int): F[Array[Byte]] = for { r <- f - out <- Sync[F].delay { + out <- Sync[F].suspend(hint) { val bytes = new Array[Byte](0 max n) r.nextBytes(bytes) bytes @@ -454,61 +458,61 @@ object Random extends RandomCompanionPlatform { def nextDouble: F[Double] = for { r <- f - out <- Sync[F].delay(r.nextDouble()) + out <- Sync[F].suspend(hint)(r.nextDouble()) } yield out def nextFloat: F[Float] = for { r <- f - out <- Sync[F].delay(r.nextFloat()) + out <- Sync[F].suspend(hint)(r.nextFloat()) } yield out def nextGaussian: F[Double] = for { r <- f - out <- Sync[F].delay(r.nextGaussian()) + out <- Sync[F].suspend(hint)(r.nextGaussian()) } yield out def nextInt: F[Int] = for { r <- f - out <- Sync[F].delay(r.nextInt()) + out <- Sync[F].suspend(hint)(r.nextInt()) } yield out def nextIntBounded(n: Int): F[Int] = for { r <- f - out <- Sync[F].delay(r.self.nextInt(n)) + out <- Sync[F].suspend(hint)(r.self.nextInt(n)) } yield out def nextLong: F[Long] = for { r <- f - out <- Sync[F].delay(r.nextLong()) + out <- Sync[F].suspend(hint)(r.nextLong()) } yield out def nextPrintableChar: F[Char] = for { r <- f - out <- Sync[F].delay(r.nextPrintableChar()) + out <- Sync[F].suspend(hint)(r.nextPrintableChar()) } yield out def nextString(length: Int): F[String] = for { r <- f - out <- Sync[F].delay(r.nextString(length)) + out <- Sync[F].suspend(hint)(r.nextString(length)) } yield out def shuffleList[A](l: List[A]): F[List[A]] = for { r <- f - out <- Sync[F].delay(r.shuffle(l)) + out <- Sync[F].suspend(hint)(r.shuffle(l)) } yield out def shuffleVector[A](v: Vector[A]): F[Vector[A]] = for { r <- f - out <- Sync[F].delay(r.shuffle(v)) + out <- Sync[F].suspend(hint)(r.shuffle(v)) } yield out } diff --git a/std/shared/src/main/scala/cats/effect/std/SecureRandom.scala b/std/shared/src/main/scala/cats/effect/std/SecureRandom.scala index 709370dfdc..316fc0949a 100644 --- a/std/shared/src/main/scala/cats/effect/std/SecureRandom.scala +++ b/std/shared/src/main/scala/cats/effect/std/SecureRandom.scala @@ -136,8 +136,7 @@ object SecureRandom extends SecureRandomCompanionPlatform { * on Linux, macOS, and BSD. Unsupported platforms such as Windows will encounter link-time * errors. */ - def javaSecuritySecureRandom[F[_]: Sync]: F[SecureRandom[F]] = - Sync[F] - .delay(new JavaSecureRandom()) - .map(r => new ScalaRandom[F](Applicative[F].pure(r)) with SecureRandom[F] {}) + override def javaSecuritySecureRandom[F[_]: Sync]: F[SecureRandom[F]] = + super.javaSecuritySecureRandom[F] + } 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 a453131269..d23fad1f06 100644 --- a/std/shared/src/main/scala/cats/effect/std/UUIDGen.scala +++ b/std/shared/src/main/scala/cats/effect/std/UUIDGen.scala @@ -39,4 +39,24 @@ object UUIDGen extends UUIDGenCompanionPlatform { def randomUUID[F[_]: UUIDGen]: F[UUID] = UUIDGen[F].randomUUID def randomString[F[_]: UUIDGen: Functor]: F[String] = randomUUID.map(_.toString) + + implicit def fromSecureRandom[F[_]: Functor: SecureRandom]: UUIDGen[F] = + new UUIDGen[F] { + override final val randomUUID: F[UUID] = + SecureRandom[F].nextBytes(16).map(unsafeUUIDBuilder) + + private def unsafeUUIDBuilder(buffer: Array[Byte]): UUID = { + @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) + } + } }