From 502c96d948051180495db3349c73d13a8326bfcc Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Mon, 7 Aug 2023 18:53:33 +0100 Subject: [PATCH 01/17] Introduces incremental hash. Fixes #181. Each API is subtly different, but in general we have a new/update/finalize/reset. The WebCrypto API for browsers doesn't support incremental hashes yet; we could either shim it (i.e. build up the `ByteVector` in memory) or just throw. I feel that it is better to throw so that the user does not have an expectation of memory usage, but it might be a PITA for anyone cross-compling. --- .../src/main/scala/bobcats/HashPlatform.scala | 25 ++++++- .../src/main/scala/bobcats/HashPlatform.scala | 21 +++++- .../src/main/scala/bobcats/HashPlatform.scala | 67 +++++++++++++++++-- .../src/main/scala/bobcats/openssl/evp.scala | 5 ++ core/shared/src/main/scala/bobcats/Hash.scala | 29 ++++++++ .../src/test/scala/bobcats/HashSuite.scala | 25 ++++++- 6 files changed, 160 insertions(+), 12 deletions(-) diff --git a/core/js/src/main/scala/bobcats/HashPlatform.scala b/core/js/src/main/scala/bobcats/HashPlatform.scala index 4129565..a89fa67 100644 --- a/core/js/src/main/scala/bobcats/HashPlatform.scala +++ b/core/js/src/main/scala/bobcats/HashPlatform.scala @@ -16,14 +16,31 @@ package bobcats -import cats.effect.kernel.Async +import cats.effect.kernel.{Async, Resource, Sync} import cats.syntax.all._ import scodec.bits.ByteVector +private final class NodeCryptoDigest[F[_]](var hash: facade.node.Hash, algorithm: String)( + implicit F: Sync[F]) + extends UnsealedDigest[F] { + override def update(data: ByteVector): F[Unit] = F.delay(hash.update(data.toUint8Array)) + override val reset = F.delay { + hash = facade.node.crypto.createHash(algorithm) + } + override def get: F[ByteVector] = F.delay(ByteVector.view(hash.digest())) +} + private[bobcats] trait HashCompanionPlatform { implicit def forAsync[F[_]](implicit F: Async[F]): Hash[F] = if (facade.isNodeJSRuntime) new UnsealedHash[F] { + + override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = + Resource.make(F.catchNonFatal { + val alg = algorithm.toStringNodeJS + val hash = facade.node.crypto.createHash(alg) + new NodeCryptoDigest(hash, alg) + })(_ => F.unit) override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = F.catchNonFatal { val hash = facade.node.crypto.createHash(algorithm.toStringNodeJS) @@ -34,6 +51,12 @@ private[bobcats] trait HashCompanionPlatform { else new UnsealedHash[F] { import facade.browser._ + override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = { + val err = F.raiseError[Digest[F]]( + new UnsupportedOperationException("WebCrypto does not support incremental hashing")) + Resource.make(err)(_ => err.void) + } + override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = F.fromPromise( F.delay( diff --git a/core/jvm/src/main/scala/bobcats/HashPlatform.scala b/core/jvm/src/main/scala/bobcats/HashPlatform.scala index 0c825ae..d82ece7 100644 --- a/core/jvm/src/main/scala/bobcats/HashPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/HashPlatform.scala @@ -16,13 +16,22 @@ package bobcats -import cats.effect.kernel.Async +import cats.effect.kernel.{Async, Resource, Sync} import scodec.bits.ByteVector import java.security.MessageDigest +private final class JavaSecurityDigest[F[_]](val hash: MessageDigest)(implicit F: Sync[F]) + extends UnsealedDigest[F] { + + override def update(data: ByteVector): F[Unit] = F.delay(hash.update(data.toByteBuffer)) + override val reset = F.delay(hash.reset()) + override def get: F[ByteVector] = F.delay(ByteVector.view(hash.digest())) +} + private[bobcats] trait HashCompanionPlatform { - implicit def forAsync[F[_]](implicit F: Async[F]): Hash[F] = + + private[bobcats] def forSync[F[_]](implicit F: Sync[F]): Hash[F] = new UnsealedHash[F] { override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = F.catchNonFatal { @@ -30,5 +39,13 @@ private[bobcats] trait HashCompanionPlatform { hash.update(data.toByteBuffer) ByteVector.view(hash.digest()) } + override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = { + Resource.make(F.catchNonFatal { + val hash = MessageDigest.getInstance(algorithm.toStringJava) + new JavaSecurityDigest[F](hash)(F) + })(_.reset) + } } + + implicit def forAsync[F[_]](implicit F: Async[F]): Hash[F] = forSync(F) } diff --git a/core/native/src/main/scala/bobcats/HashPlatform.scala b/core/native/src/main/scala/bobcats/HashPlatform.scala index 8ae6e5c..35124e0 100644 --- a/core/native/src/main/scala/bobcats/HashPlatform.scala +++ b/core/native/src/main/scala/bobcats/HashPlatform.scala @@ -16,7 +16,8 @@ package bobcats -import cats.effect.kernel.Async +import scala.scalanative.annotation.alwaysinline +import cats.effect.kernel.{Async, Resource, Sync} import scodec.bits.ByteVector import scalanative.unsafe._ import scalanative.unsigned._ @@ -24,21 +25,75 @@ import openssl._ import openssl.err._ import openssl.evp._ +private final class NativeEvpDigest[F[_]](val ctx: Ptr[EVP_MD_CTX], digest: Ptr[EVP_MD])( + implicit F: Sync[F]) + extends UnsealedDigest[F] { + + override def update(data: ByteVector): F[Unit] = F.delay { + + val d = data.toArrayUnsafe + val len = d.length + + if (EVP_DigestUpdate(ctx, if (len == 0) null else d.at(0), d.length.toULong) != 1) { + throw Error("EVP_DigestUpdate", ERR_get_error()) + } + } + + override val reset = F.delay { + if (EVP_MD_CTX_reset(ctx) != 1) { + throw Error("EVP_MD_Ctx_reset", ERR_get_error()) + } + if (EVP_DigestInit_ex(ctx, digest, null) != 1) { + throw Error("EVP_DigestInit_ex", ERR_get_error()) + } + } + + override def get: F[ByteVector] = F.delay { + val md = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE) + val s = stackalloc[CInt]() + + if (EVP_DigestFinal_ex(ctx, md, s) != 1) { + throw Error("EVP_DigestFinal_ex", ERR_get_error()) + } + ByteVector.fromPtr(md.asInstanceOf[Ptr[Byte]], s(0).toLong) + } + + def free: F[Unit] = F.delay(EVP_MD_CTX_free(ctx)) + +} + private[bobcats] trait HashCompanionPlatform { - implicit def forAsync[F[_]](implicit F: Async[F]): Hash[F] = + implicit def forAsync[F[_]](implicit F: Async[F]): Hash[F] = forSync + + private[bobcats] def forSync[F[_]](implicit F: Sync[F]): Hash[F] = new UnsealedHash[F] { - override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = { + @alwaysinline private def evpAlgorithm(algorithm: HashAlgorithm): Ptr[EVP_MD] = { import HashAlgorithm._ - - val digest = algorithm match { + algorithm match { case MD5 => EVP_md5() case SHA1 => EVP_sha1() case SHA256 => EVP_sha256() case SHA512 => EVP_sha512() } + } + + override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = { + val digest = evpAlgorithm(algorithm) + Resource.make(F.catchNonFatal { + val ctx = EVP_MD_CTX_new() + if (EVP_DigestInit_ex(ctx, digest, null) != 1) { + throw Error("EVP_DigestInit_ex", ERR_get_error()) + } + new NativeEvpDigest(ctx, digest)(F) + })(_.free) + } + + override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = { + + val digest = evpAlgorithm(algorithm) - F.delay { + F.catchNonFatal { val ctx = EVP_MD_CTX_new() val d = data.toArrayUnsafe try { diff --git a/core/native/src/main/scala/bobcats/openssl/evp.scala b/core/native/src/main/scala/bobcats/openssl/evp.scala index 84adaf3..4ab75df 100644 --- a/core/native/src/main/scala/bobcats/openssl/evp.scala +++ b/core/native/src/main/scala/bobcats/openssl/evp.scala @@ -36,6 +36,11 @@ private[bobcats] object evp { */ def EVP_MD_CTX_new(): Ptr[EVP_MD_CTX] = extern + /** + * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_CTX_reset.html]] + */ + def EVP_MD_CTX_reset(ctx: Ptr[EVP_MD_CTX]): CInt = extern + /** * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_CTX_free.html]] */ diff --git a/core/shared/src/main/scala/bobcats/Hash.scala b/core/shared/src/main/scala/bobcats/Hash.scala index 5863835..9656595 100644 --- a/core/shared/src/main/scala/bobcats/Hash.scala +++ b/core/shared/src/main/scala/bobcats/Hash.scala @@ -17,9 +17,38 @@ package bobcats import scodec.bits.ByteVector +import cats.effect.kernel.Resource + +/** + * Used for incremental hashing. + */ +sealed trait Digest[F[_]] { + + /** + * Updates the digest context with the provided data. + */ + def update(data: ByteVector): F[Unit] + + /** + * Returns the final digest. + */ + def get: F[ByteVector] + + /** + * Resets the digest to be used again. + */ + def reset: F[Unit] +} + +private[bobcats] trait UnsealedDigest[F[_]] extends Digest[F] sealed trait Hash[F[_]] { def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] + + /** + * Create a digest with can be updated incrementally. + */ + def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] } private[bobcats] trait UnsealedHash[F[_]] extends Hash[F] diff --git a/core/shared/src/test/scala/bobcats/HashSuite.scala b/core/shared/src/test/scala/bobcats/HashSuite.scala index eafaac0..d873ff1 100644 --- a/core/shared/src/test/scala/bobcats/HashSuite.scala +++ b/core/shared/src/test/scala/bobcats/HashSuite.scala @@ -16,8 +16,8 @@ package bobcats +import cats.effect.{IO, MonadCancelThrow} import cats.Functor -import cats.effect.IO import cats.syntax.all._ import munit.CatsEffectSuite import scodec.bits.ByteVector @@ -39,12 +39,26 @@ class HashSuite extends CatsEffectSuite { ByteVector.fromHex(expect).get ) } + } + def testHashIncremental[F[_]: Hash: MonadCancelThrow]( + algorithm: HashAlgorithm, + expect: String)(implicit ct: ClassTag[F[Nothing]]) = + test(s"incremental $algorithm with ${ct.runtimeClass.getSimpleName()}") { + Hash[F].incremental(algorithm).use { digest => + (digest.update(data) *> digest.reset *> digest.update(data) *> digest.get) + .map { obtained => + assertEquals( + obtained, + ByteVector.fromHex(expect).get + ) + } + } } def testEmpty[F[_]: Hash: Functor](algorithm: HashAlgorithm, expect: String)( implicit ct: ClassTag[F[Nothing]]) = - test(s"$algorithm with ${ct.runtimeClass.getSimpleName()}") { + test(s"empty $algorithm with ${ct.runtimeClass.getSimpleName()}") { Hash[F].digest(algorithm, ByteVector.empty).map { obtained => assertEquals( obtained, @@ -53,10 +67,15 @@ class HashSuite extends CatsEffectSuite { } } - def tests[F[_]: Hash: Functor](implicit ct: ClassTag[F[Nothing]]) = { + def tests[F[_]: Hash: MonadCancelThrow](implicit ct: ClassTag[F[Nothing]]) = { if (Set("JVM", "NodeJS", "Native").contains(BuildInfo.runtime)) testHash[F](MD5, "9e107d9d372bb6826bd81d3542a419d6") + if (Set("JVM", "NodeJS", "Native").contains(BuildInfo.runtime)) { + testHashIncremental[F](MD5, "9e107d9d372bb6826bd81d3542a419d6") + testHashIncremental[F](SHA1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") + } + testHash[F](SHA1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") testEmpty[F](SHA1, "da39a3ee5e6b4b0d3255bfef95601890afd80709") From bf9832e056274f27ecb98bd5f0708649ae5bc2d9 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Mon, 7 Aug 2023 20:53:28 +0100 Subject: [PATCH 02/17] Wrap definitely side-effecting code. --- core/js/src/main/scala/bobcats/HashPlatform.scala | 2 +- core/jvm/src/main/scala/bobcats/HashPlatform.scala | 2 +- core/native/src/main/scala/bobcats/HashPlatform.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/js/src/main/scala/bobcats/HashPlatform.scala b/core/js/src/main/scala/bobcats/HashPlatform.scala index a89fa67..3338175 100644 --- a/core/js/src/main/scala/bobcats/HashPlatform.scala +++ b/core/js/src/main/scala/bobcats/HashPlatform.scala @@ -36,7 +36,7 @@ private[bobcats] trait HashCompanionPlatform { new UnsealedHash[F] { override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = - Resource.make(F.catchNonFatal { + Resource.make(F.delay { val alg = algorithm.toStringNodeJS val hash = facade.node.crypto.createHash(alg) new NodeCryptoDigest(hash, alg) diff --git a/core/jvm/src/main/scala/bobcats/HashPlatform.scala b/core/jvm/src/main/scala/bobcats/HashPlatform.scala index d82ece7..cbd23f4 100644 --- a/core/jvm/src/main/scala/bobcats/HashPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/HashPlatform.scala @@ -40,7 +40,7 @@ private[bobcats] trait HashCompanionPlatform { ByteVector.view(hash.digest()) } override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = { - Resource.make(F.catchNonFatal { + Resource.make(F.delay { val hash = MessageDigest.getInstance(algorithm.toStringJava) new JavaSecurityDigest[F](hash)(F) })(_.reset) diff --git a/core/native/src/main/scala/bobcats/HashPlatform.scala b/core/native/src/main/scala/bobcats/HashPlatform.scala index 35124e0..61e65fc 100644 --- a/core/native/src/main/scala/bobcats/HashPlatform.scala +++ b/core/native/src/main/scala/bobcats/HashPlatform.scala @@ -80,7 +80,7 @@ private[bobcats] trait HashCompanionPlatform { override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = { val digest = evpAlgorithm(algorithm) - Resource.make(F.catchNonFatal { + Resource.make(F.delay { val ctx = EVP_MD_CTX_new() if (EVP_DigestInit_ex(ctx, digest, null) != 1) { throw Error("EVP_DigestInit_ex", ERR_get_error()) From 13d57f405daef74e494cdd8338b8003c2f3eaabb Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Mon, 7 Aug 2023 21:26:56 +0100 Subject: [PATCH 03/17] wrap possibly side-effecting code --- core/js/src/main/scala/bobcats/HashPlatform.scala | 2 +- core/jvm/src/main/scala/bobcats/HashPlatform.scala | 2 +- core/native/src/main/scala/bobcats/HashPlatform.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/js/src/main/scala/bobcats/HashPlatform.scala b/core/js/src/main/scala/bobcats/HashPlatform.scala index 3338175..35aaeff 100644 --- a/core/js/src/main/scala/bobcats/HashPlatform.scala +++ b/core/js/src/main/scala/bobcats/HashPlatform.scala @@ -42,7 +42,7 @@ private[bobcats] trait HashCompanionPlatform { new NodeCryptoDigest(hash, alg) })(_ => F.unit) override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = - F.catchNonFatal { + F.delay { val hash = facade.node.crypto.createHash(algorithm.toStringNodeJS) hash.update(data.toUint8Array) ByteVector.view(hash.digest()) diff --git a/core/jvm/src/main/scala/bobcats/HashPlatform.scala b/core/jvm/src/main/scala/bobcats/HashPlatform.scala index cbd23f4..1db0831 100644 --- a/core/jvm/src/main/scala/bobcats/HashPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/HashPlatform.scala @@ -34,7 +34,7 @@ private[bobcats] trait HashCompanionPlatform { private[bobcats] def forSync[F[_]](implicit F: Sync[F]): Hash[F] = new UnsealedHash[F] { override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = - F.catchNonFatal { + F.delay { val hash = MessageDigest.getInstance(algorithm.toStringJava) hash.update(data.toByteBuffer) ByteVector.view(hash.digest()) diff --git a/core/native/src/main/scala/bobcats/HashPlatform.scala b/core/native/src/main/scala/bobcats/HashPlatform.scala index 61e65fc..fec4c9b 100644 --- a/core/native/src/main/scala/bobcats/HashPlatform.scala +++ b/core/native/src/main/scala/bobcats/HashPlatform.scala @@ -93,7 +93,7 @@ private[bobcats] trait HashCompanionPlatform { val digest = evpAlgorithm(algorithm) - F.catchNonFatal { + F.delay { val ctx = EVP_MD_CTX_new() val d = data.toArrayUnsafe try { From c429339ead4531dd7deaaab08f01621e14ae949a Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Tue, 8 Aug 2023 18:49:01 +0100 Subject: [PATCH 04/17] Sketch of allowing extensibility for `HashAlgorithm`. --- build.sbt | 2 + .../main/scala/bobcats/CryptoPlatform.scala | 2 +- .../main/scala/bobcats/CryptoPlatform.scala | 4 +- .../main/scala/bobcats/Hash1Platform.scala | 59 +++++++++++++++++++ .../src/main/scala/bobcats/HashPlatform.scala | 30 +++------- .../src/main/scala/bobcats/Crypto.scala | 4 ++ core/shared/src/main/scala/bobcats/Hash.scala | 34 ++--------- .../shared/src/main/scala/bobcats/Hash1.scala | 35 +++++++++++ .../src/test/scala/bobcats/HashSuite.scala | 45 ++++++++------ 9 files changed, 142 insertions(+), 73 deletions(-) create mode 100644 core/jvm/src/main/scala/bobcats/Hash1Platform.scala create mode 100644 core/shared/src/main/scala/bobcats/Hash1.scala diff --git a/build.sbt b/build.sbt index 18e138d..7108793 100644 --- a/build.sbt +++ b/build.sbt @@ -78,6 +78,7 @@ ThisBuild / Test / jsEnv := { } val catsVersion = "2.8.0" +val fs2Version = "3.7.0" val catsEffectVersion = "3.5.1" val scodecBitsVersion = "1.1.35" val munitVersion = "1.0.0-M8" @@ -94,6 +95,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.typelevel" %%% "cats-core" % catsVersion, "org.typelevel" %%% "cats-effect-kernel" % catsEffectVersion, "org.scodec" %%% "scodec-bits" % scodecBitsVersion, + "co.fs2" %%% "fs2-core" % fs2Version, "org.scalameta" %%% "munit" % munitVersion % Test, "org.typelevel" %%% "cats-laws" % catsVersion % Test, "org.typelevel" %%% "cats-effect" % catsEffectVersion % Test, diff --git a/core/js/src/main/scala/bobcats/CryptoPlatform.scala b/core/js/src/main/scala/bobcats/CryptoPlatform.scala index 6ec18d0..eb9634a 100644 --- a/core/js/src/main/scala/bobcats/CryptoPlatform.scala +++ b/core/js/src/main/scala/bobcats/CryptoPlatform.scala @@ -19,7 +19,7 @@ package bobcats import cats.effect.kernel.Async private[bobcats] trait CryptoCompanionPlatform { - implicit def forAsync[F[_]](implicit F: Async[F]): Crypto[F] = + def forAsync[F[_]](implicit F: Async[F]): Crypto[F] = new UnsealedCrypto[F] { override def hash: Hash[F] = Hash[F] override def hmac: Hmac[F] = Hmac[F] diff --git a/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala b/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala index c836505..e9925ed 100644 --- a/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala @@ -19,9 +19,9 @@ package bobcats import cats.effect.kernel.Async private[bobcats] trait CryptoCompanionPlatform { - implicit def forAsync[F[_]: Async]: Crypto[F] = + def forAsync[F[_]: Async]: Crypto[F] = new UnsealedCrypto[F] { - override def hash: Hash[F] = Hash[F] + override def hash: Hash[F] = Hash.forAsync[F] override def hmac: Hmac[F] = Hmac[F] } } diff --git a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala new file mode 100644 index 0000000..7ccc6f4 --- /dev/null +++ b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2021 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 bobcats + +import java.security.MessageDigest +import scodec.bits.ByteVector +import cats.Applicative +import cats.effect.Sync +import fs2.{Chunk, Stream} + +private final class JavaSecurityDigest[F[_]](val hash: MessageDigest)( + implicit F: Applicative[F]) + extends UnsealedHash1[F] { + + override def digest(data: ByteVector): F[ByteVector] = F.pure { + val h = hash.clone().asInstanceOf[MessageDigest] + h.update(data.toByteBuffer) + ByteVector.view(h.digest()) + } + override def digest(data: Stream[F, Byte]): Stream[F, Byte] = + data + .chunks + .fold(hash.clone().asInstanceOf[MessageDigest]) { (h, data) => + h.update(data.toByteBuffer) + h + } + .flatMap { h => Stream.chunk(Chunk.array(h.digest())) } + + override def toString = hash.toString +} + +private[bobcats] trait Hash1CompanionPlatform { + + private[bobcats] def fromMessageDigestUnsafe[F[_]: Applicative]( + digest: MessageDigest): Hash1[F] = new JavaSecurityDigest(digest) + + def fromName[F[_]](name: String)(implicit F: Sync[F]): F[Hash1[F]] = F.delay { + val hash = MessageDigest.getInstance(name) + fromMessageDigestUnsafe(hash) + } + + def apply[F[_]](algorithm: HashAlgorithm)(implicit F: Sync[F]): F[Hash1[F]] = fromName( + algorithm.toStringJava) + +} diff --git a/core/jvm/src/main/scala/bobcats/HashPlatform.scala b/core/jvm/src/main/scala/bobcats/HashPlatform.scala index 1db0831..0aba5ed 100644 --- a/core/jvm/src/main/scala/bobcats/HashPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/HashPlatform.scala @@ -16,36 +16,20 @@ package bobcats -import cats.effect.kernel.{Async, Resource, Sync} +import cats.syntax.all._ +import fs2.Stream +import cats.effect.kernel.{Async, Sync} import scodec.bits.ByteVector -import java.security.MessageDigest - -private final class JavaSecurityDigest[F[_]](val hash: MessageDigest)(implicit F: Sync[F]) - extends UnsealedDigest[F] { - - override def update(data: ByteVector): F[Unit] = F.delay(hash.update(data.toByteBuffer)) - override val reset = F.delay(hash.reset()) - override def get: F[ByteVector] = F.delay(ByteVector.view(hash.digest())) -} - private[bobcats] trait HashCompanionPlatform { private[bobcats] def forSync[F[_]](implicit F: Sync[F]): Hash[F] = new UnsealedHash[F] { override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = - F.delay { - val hash = MessageDigest.getInstance(algorithm.toStringJava) - hash.update(data.toByteBuffer) - ByteVector.view(hash.digest()) - } - override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = { - Resource.make(F.delay { - val hash = MessageDigest.getInstance(algorithm.toStringJava) - new JavaSecurityDigest[F](hash)(F) - })(_.reset) - } + Hash1[F](algorithm).flatMap(_.digest(data)) + override def digest(algorithm: HashAlgorithm)(data: Stream[F, Byte]): Stream[F, Byte] = + Stream.eval(Hash1[F](algorithm)).flatMap(_.digest(data)) } - implicit def forAsync[F[_]](implicit F: Async[F]): Hash[F] = forSync(F) + def forAsync[F[_]](implicit F: Async[F]): Hash[F] = forSync(F) } diff --git a/core/shared/src/main/scala/bobcats/Crypto.scala b/core/shared/src/main/scala/bobcats/Crypto.scala index f5a1165..63fca7f 100644 --- a/core/shared/src/main/scala/bobcats/Crypto.scala +++ b/core/shared/src/main/scala/bobcats/Crypto.scala @@ -16,6 +16,8 @@ package bobcats +import cats.effect.IO + sealed trait Crypto[F[_]] { def hash: Hash[F] def hmac: Hmac[F] @@ -25,6 +27,8 @@ private[bobcats] trait UnsealedCrypto[F[_]] extends Crypto[F] object Crypto extends CryptoCompanionPlatform { + implicit def forIO: Crypto[IO] = forAsync + def apply[F[_]](implicit crypto: Crypto[F]): crypto.type = crypto } diff --git a/core/shared/src/main/scala/bobcats/Hash.scala b/core/shared/src/main/scala/bobcats/Hash.scala index 9656595..009d7c4 100644 --- a/core/shared/src/main/scala/bobcats/Hash.scala +++ b/core/shared/src/main/scala/bobcats/Hash.scala @@ -16,45 +16,21 @@ package bobcats +import cats.effect.IO import scodec.bits.ByteVector -import cats.effect.kernel.Resource - -/** - * Used for incremental hashing. - */ -sealed trait Digest[F[_]] { - - /** - * Updates the digest context with the provided data. - */ - def update(data: ByteVector): F[Unit] - - /** - * Returns the final digest. - */ - def get: F[ByteVector] - - /** - * Resets the digest to be used again. - */ - def reset: F[Unit] -} - -private[bobcats] trait UnsealedDigest[F[_]] extends Digest[F] +import fs2.Stream sealed trait Hash[F[_]] { def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] - - /** - * Create a digest with can be updated incrementally. - */ - def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] + def digest(algorithm: HashAlgorithm)(stream: Stream[F, Byte]): Stream[F, Byte] } private[bobcats] trait UnsealedHash[F[_]] extends Hash[F] object Hash extends HashCompanionPlatform { + implicit def forIO: Hash[IO] = forAsync + def apply[F[_]](implicit hash: Hash[F]): hash.type = hash } diff --git a/core/shared/src/main/scala/bobcats/Hash1.scala b/core/shared/src/main/scala/bobcats/Hash1.scala new file mode 100644 index 0000000..2160bf5 --- /dev/null +++ b/core/shared/src/main/scala/bobcats/Hash1.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2021 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 bobcats + +import fs2.Stream +import scodec.bits.ByteVector + +/** + * Hash for a single algorithm. + * + * Use this class if you have a specific `HashAlgorithm` known in advance or you're using a + * customized algorithm not covered by the `HashAlgorithm` class. + */ +sealed trait Hash1[F[_]] { + def digest(data: ByteVector): F[ByteVector] + def digest(data: Stream[F, Byte]): Stream[F, Byte] +} + +private[bobcats] trait UnsealedHash1[F[_]] extends Hash1[F] + +object Hash1 extends Hash1CompanionPlatform diff --git a/core/shared/src/test/scala/bobcats/HashSuite.scala b/core/shared/src/test/scala/bobcats/HashSuite.scala index d873ff1..bb0da08 100644 --- a/core/shared/src/test/scala/bobcats/HashSuite.scala +++ b/core/shared/src/test/scala/bobcats/HashSuite.scala @@ -30,7 +30,7 @@ class HashSuite extends CatsEffectSuite { val data = ByteVector.encodeAscii("The quick brown fox jumps over the lazy dog").toOption.get - def testHash[F[_]: Hash: Functor](algorithm: HashAlgorithm, expect: String)( + def testHash[F[_]: Hash: cats.Monad](algorithm: HashAlgorithm, expect: String)( implicit ct: ClassTag[F[Nothing]]) = test(s"$algorithm with ${ct.runtimeClass.getSimpleName()}") { Hash[F].digest(algorithm, data).map { obtained => @@ -39,23 +39,32 @@ class HashSuite extends CatsEffectSuite { ByteVector.fromHex(expect).get ) } - } - def testHashIncremental[F[_]: Hash: MonadCancelThrow]( - algorithm: HashAlgorithm, - expect: String)(implicit ct: ClassTag[F[Nothing]]) = - test(s"incremental $algorithm with ${ct.runtimeClass.getSimpleName()}") { - Hash[F].incremental(algorithm).use { digest => - (digest.update(data) *> digest.reset *> digest.update(data) *> digest.get) - .map { obtained => - assertEquals( - obtained, - ByteVector.fromHex(expect).get - ) - } + Hash1[cats.effect.IO](algorithm).flatMap { digest => + digest.digest(data).map { obtained => + assertEquals( + obtained, + ByteVector.fromHex(expect).get + ) + } } } + // def testHashIncremental[F[_]: Hash: MonadCancelThrow]( + // algorithm: HashAlgorithm, + // expect: String)(implicit ct: ClassTag[F[Nothing]]) = + // test(s"incremental $algorithm with ${ct.runtimeClass.getSimpleName()}") { + // Hash[F].incremental(algorithm).use { digest => + // (digest.update(data) *> digest.reset *> digest.update(data) *> digest.get) + // .map { obtained => + // assertEquals( + // obtained, + // ByteVector.fromHex(expect).get + // ) + // } + // } + // } + def testEmpty[F[_]: Hash: Functor](algorithm: HashAlgorithm, expect: String)( implicit ct: ClassTag[F[Nothing]]) = test(s"empty $algorithm with ${ct.runtimeClass.getSimpleName()}") { @@ -71,10 +80,10 @@ class HashSuite extends CatsEffectSuite { if (Set("JVM", "NodeJS", "Native").contains(BuildInfo.runtime)) testHash[F](MD5, "9e107d9d372bb6826bd81d3542a419d6") - if (Set("JVM", "NodeJS", "Native").contains(BuildInfo.runtime)) { - testHashIncremental[F](MD5, "9e107d9d372bb6826bd81d3542a419d6") - testHashIncremental[F](SHA1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") - } + // if (Set("JVM", "NodeJS", "Native").contains(BuildInfo.runtime)) { + // testHashIncremental[F](MD5, "9e107d9d372bb6826bd81d3542a419d6") + // testHashIncremental[F](SHA1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") + // } testHash[F](SHA1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") testEmpty[F](SHA1, "da39a3ee5e6b4b0d3255bfef95601890afd80709") From 8f5e14492dc057e9363ab72b0a71f402609281de Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Tue, 8 Aug 2023 20:18:41 +0100 Subject: [PATCH 05/17] switch to pipe --- .../main/scala/bobcats/Hash1Platform.scala | 59 ++++++++++++------- .../src/main/scala/bobcats/HashPlatform.scala | 6 +- core/shared/src/main/scala/bobcats/Hash.scala | 4 +- .../shared/src/main/scala/bobcats/Hash1.scala | 4 +- 4 files changed, 46 insertions(+), 27 deletions(-) diff --git a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala index 7ccc6f4..bf26bc8 100644 --- a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala +++ b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala @@ -16,41 +16,60 @@ package bobcats +import cats.syntax.all._ import java.security.MessageDigest import scodec.bits.ByteVector -import cats.Applicative +import cats.Functor import cats.effect.Sync -import fs2.{Chunk, Stream} +import fs2.{Chunk, Pipe, Stream} -private final class JavaSecurityDigest[F[_]](val hash: MessageDigest)( - implicit F: Applicative[F]) +private abstract class JavaSecurityDigest[F[_]](implicit F: Functor[F]) extends UnsealedHash1[F] { - override def digest(data: ByteVector): F[ByteVector] = F.pure { - val h = hash.clone().asInstanceOf[MessageDigest] - h.update(data.toByteBuffer) - ByteVector.view(h.digest()) - } - override def digest(data: Stream[F, Byte]): Stream[F, Byte] = - data - .chunks - .fold(hash.clone().asInstanceOf[MessageDigest]) { (h, data) => - h.update(data.toByteBuffer) - h - } - .flatMap { h => Stream.chunk(Chunk.array(h.digest())) } + def hash: F[MessageDigest] + + override def digest(data: ByteVector): F[ByteVector] = + hash.map { h => + h.update(data.toByteBuffer) + ByteVector.view(h.digest()) + } + + override val pipe: Pipe[F, Byte, Byte] = + in => + Stream + .eval(hash) + .flatMap { h => + in.chunks.fold(h) { (h, data) => + h.update(data.toByteBuffer) + h + } + } + .flatMap { h => Stream.chunk(Chunk.array(h.digest())) } override def toString = hash.toString } private[bobcats] trait Hash1CompanionPlatform { - private[bobcats] def fromMessageDigestUnsafe[F[_]: Applicative]( - digest: MessageDigest): Hash1[F] = new JavaSecurityDigest(digest) + /** + * Wraps a `MessageDigest` which is assumed to be `Cloneable`. + */ + private[bobcats] def fromMessageDigestCloneableUnsafe[F[_]](messageDigest: MessageDigest)( + implicit F: Sync[F]): Hash1[F] = new JavaSecurityDigest { + override val hash: F[MessageDigest] = + F.delay(messageDigest.clone().asInstanceOf[MessageDigest]) + } def fromName[F[_]](name: String)(implicit F: Sync[F]): F[Hash1[F]] = F.delay { val hash = MessageDigest.getInstance(name) - fromMessageDigestUnsafe(hash) + try { + fromMessageDigestCloneableUnsafe(hash.clone().asInstanceOf[MessageDigest]) + } catch { + case _: CloneNotSupportedException => + new JavaSecurityDigest { + override val hash: F[MessageDigest] = F.delay(MessageDigest.getInstance(name)) + } + } } def apply[F[_]](algorithm: HashAlgorithm)(implicit F: Sync[F]): F[Hash1[F]] = fromName( diff --git a/core/jvm/src/main/scala/bobcats/HashPlatform.scala b/core/jvm/src/main/scala/bobcats/HashPlatform.scala index 0aba5ed..9186795 100644 --- a/core/jvm/src/main/scala/bobcats/HashPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/HashPlatform.scala @@ -17,7 +17,7 @@ package bobcats import cats.syntax.all._ -import fs2.Stream +import fs2.{Pipe, Stream} import cats.effect.kernel.{Async, Sync} import scodec.bits.ByteVector @@ -27,8 +27,8 @@ private[bobcats] trait HashCompanionPlatform { new UnsealedHash[F] { override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = Hash1[F](algorithm).flatMap(_.digest(data)) - override def digest(algorithm: HashAlgorithm)(data: Stream[F, Byte]): Stream[F, Byte] = - Stream.eval(Hash1[F](algorithm)).flatMap(_.digest(data)) + override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = + in => Stream.eval(Hash1[F](algorithm)).flatMap(_.pipe(in)) } def forAsync[F[_]](implicit F: Async[F]): Hash[F] = forSync(F) diff --git a/core/shared/src/main/scala/bobcats/Hash.scala b/core/shared/src/main/scala/bobcats/Hash.scala index 009d7c4..90b7e0a 100644 --- a/core/shared/src/main/scala/bobcats/Hash.scala +++ b/core/shared/src/main/scala/bobcats/Hash.scala @@ -18,11 +18,11 @@ package bobcats import cats.effect.IO import scodec.bits.ByteVector -import fs2.Stream +import fs2.Pipe sealed trait Hash[F[_]] { def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] - def digest(algorithm: HashAlgorithm)(stream: Stream[F, Byte]): Stream[F, Byte] + def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] } private[bobcats] trait UnsealedHash[F[_]] extends Hash[F] diff --git a/core/shared/src/main/scala/bobcats/Hash1.scala b/core/shared/src/main/scala/bobcats/Hash1.scala index 2160bf5..1c98bac 100644 --- a/core/shared/src/main/scala/bobcats/Hash1.scala +++ b/core/shared/src/main/scala/bobcats/Hash1.scala @@ -16,7 +16,7 @@ package bobcats -import fs2.Stream +import fs2.Pipe import scodec.bits.ByteVector /** @@ -27,7 +27,7 @@ import scodec.bits.ByteVector */ sealed trait Hash1[F[_]] { def digest(data: ByteVector): F[ByteVector] - def digest(data: Stream[F, Byte]): Stream[F, Byte] + def pipe: Pipe[F, Byte, Byte] } private[bobcats] trait UnsealedHash1[F[_]] extends Hash1[F] From 4062517f53432d5d524d7ff492cf3e30a88d019b Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Tue, 8 Aug 2023 23:33:18 +0100 Subject: [PATCH 06/17] switch to caching the `Provider` --- .../main/scala/bobcats/Hash1Platform.scala | 55 +++++++------------ .../src/main/scala/bobcats/HashPlatform.scala | 31 +++++++++-- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala index bf26bc8..f0eb257 100644 --- a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala +++ b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala @@ -17,59 +17,44 @@ package bobcats import cats.syntax.all._ -import java.security.MessageDigest +import java.security.{MessageDigest, NoSuchAlgorithmException, Provider, Security} import scodec.bits.ByteVector -import cats.Functor +import cats.Applicative import cats.effect.Sync import fs2.{Chunk, Pipe, Stream} -private abstract class JavaSecurityDigest[F[_]](implicit F: Functor[F]) +private final class JavaSecurityDigest[F[_]](algorithm: String, provider: Provider)( + implicit F: Applicative[F]) extends UnsealedHash1[F] { - def hash: F[MessageDigest] - - override def digest(data: ByteVector): F[ByteVector] = - hash.map { h => - h.update(data.toByteBuffer) - ByteVector.view(h.digest()) - } + override def digest(data: ByteVector): F[ByteVector] = F.pure { + val h = MessageDigest.getInstance(algorithm, provider) + h.update(data.toByteBuffer) + ByteVector.view(h.digest()) + } override val pipe: Pipe[F, Byte, Byte] = in => - Stream - .eval(hash) - .flatMap { h => - in.chunks.fold(h) { (h, data) => - h.update(data.toByteBuffer) - h - } + in.chunks + .fold(MessageDigest.getInstance(algorithm, provider)) { (h, data) => + h.update(data.toByteBuffer) + h } .flatMap { h => Stream.chunk(Chunk.array(h.digest())) } - override def toString = hash.toString + override def toString = s"JavaSecurityDigest(${algorithm}, ${provider.getName})" } private[bobcats] trait Hash1CompanionPlatform { - /** - * Wraps a `MessageDigest` which is assumed to be `Cloneable`. - */ - private[bobcats] def fromMessageDigestCloneableUnsafe[F[_]](messageDigest: MessageDigest)( - implicit F: Sync[F]): Hash1[F] = new JavaSecurityDigest { - override val hash: F[MessageDigest] = - F.delay(messageDigest.clone().asInstanceOf[MessageDigest]) - } + private[bobcats] def providerForName(ps: Array[Provider], name: String): Option[Provider] = + ps.find(provider => provider.getService("MessageDigest", name) != null) def fromName[F[_]](name: String)(implicit F: Sync[F]): F[Hash1[F]] = F.delay { - val hash = MessageDigest.getInstance(name) - try { - fromMessageDigestCloneableUnsafe(hash.clone().asInstanceOf[MessageDigest]) - } catch { - case _: CloneNotSupportedException => - new JavaSecurityDigest { - override val hash: F[MessageDigest] = F.delay(MessageDigest.getInstance(name)) - } - } + // `Security#getProviders` is a mutable array, so cache the `Provider` + val p = providerForName(Security.getProviders(), name) + .getOrElse(throw new NoSuchAlgorithmException(s"${name} MessageDigest not available")) + new JavaSecurityDigest(name, p) } def apply[F[_]](algorithm: HashAlgorithm)(implicit F: Sync[F]): F[Hash1[F]] = fromName( diff --git a/core/jvm/src/main/scala/bobcats/HashPlatform.scala b/core/jvm/src/main/scala/bobcats/HashPlatform.scala index 9186795..eabde77 100644 --- a/core/jvm/src/main/scala/bobcats/HashPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/HashPlatform.scala @@ -16,20 +16,39 @@ package bobcats -import cats.syntax.all._ +import java.security.{NoSuchAlgorithmException, Security} import fs2.{Pipe, Stream} import cats.effect.kernel.{Async, Sync} import scodec.bits.ByteVector private[bobcats] trait HashCompanionPlatform { - private[bobcats] def forSync[F[_]](implicit F: Sync[F]): Hash[F] = + private[bobcats] def forSync[F[_]](implicit F: Sync[F]): Hash[F] = { + // TODO: What to do with this? + val providers = Security.getProviders().clone() new UnsealedHash[F] { - override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = - Hash1[F](algorithm).flatMap(_.digest(data)) - override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = - in => Stream.eval(Hash1[F](algorithm)).flatMap(_.pipe(in)) + override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = { + val name = algorithm.toStringJava + Hash1.providerForName(providers, name) match { + case None => + F.raiseError(new NoSuchAlgorithmException(s"${name} MessageDigest not available")) + case Some(p) => new JavaSecurityDigest(name, p).digest(data) + } + } + + override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = { + val name = algorithm.toStringJava + Hash1.providerForName(providers, name) match { + case None => + _ => + Stream.eval( + F.raiseError( + new NoSuchAlgorithmException(s"${name} MessageDigest not available"))) + case Some(p) => new JavaSecurityDigest(name, p).pipe + } + } } + } def forAsync[F[_]](implicit F: Async[F]): Hash[F] = forSync(F) } From dfad6218c17ee98c3a9d621f2725e77b154ac1e3 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 9 Aug 2023 19:18:34 +0100 Subject: [PATCH 07/17] Wrap it in a `F`. --- .../main/scala/bobcats/CryptoPlatform.scala | 14 +- .../main/scala/bobcats/Hash1Platform.scala | 24 ++-- .../src/main/scala/bobcats/HashPlatform.scala | 28 ++-- .../src/main/scala/bobcats/Providers.scala | 35 +++++ .../src/main/scala/bobcats/Crypto.scala | 6 - core/shared/src/main/scala/bobcats/Hash.scala | 5 - .../src/test/scala/bobcats/CryptoSuite.scala | 36 ++++++ .../src/test/scala/bobcats/HashSuite.scala | 120 +++++++----------- 8 files changed, 150 insertions(+), 118 deletions(-) create mode 100644 core/jvm/src/main/scala/bobcats/Providers.scala create mode 100644 core/shared/src/test/scala/bobcats/CryptoSuite.scala diff --git a/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala b/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala index e9925ed..445911e 100644 --- a/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala @@ -16,12 +16,18 @@ package bobcats -import cats.effect.kernel.Async +import cats.effect.kernel.{Async, Sync} private[bobcats] trait CryptoCompanionPlatform { - def forAsync[F[_]: Async]: Crypto[F] = + + def forSync[F[_]](implicit F: Sync[F]): F[Crypto[F]] = F.delay { + val providers = Providers.get() new UnsealedCrypto[F] { - override def hash: Hash[F] = Hash.forAsync[F] - override def hmac: Hmac[F] = Hmac[F] + override def hash: Hash[F] = Hash.forProviders(providers) + override def hmac: Hmac[F] = ??? } + } + + def forAsync[F[_]: Async]: F[Crypto[F]] = forSync + } diff --git a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala index f0eb257..1cbd4cc 100644 --- a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala +++ b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala @@ -16,8 +16,7 @@ package bobcats -import cats.syntax.all._ -import java.security.{MessageDigest, NoSuchAlgorithmException, Provider, Security} +import java.security.{MessageDigest, Provider} import scodec.bits.ByteVector import cats.Applicative import cats.effect.Sync @@ -47,15 +46,18 @@ private final class JavaSecurityDigest[F[_]](algorithm: String, provider: Provid private[bobcats] trait Hash1CompanionPlatform { - private[bobcats] def providerForName(ps: Array[Provider], name: String): Option[Provider] = - ps.find(provider => provider.getService("MessageDigest", name) != null) - - def fromName[F[_]](name: String)(implicit F: Sync[F]): F[Hash1[F]] = F.delay { - // `Security#getProviders` is a mutable array, so cache the `Provider` - val p = providerForName(Security.getProviders(), name) - .getOrElse(throw new NoSuchAlgorithmException(s"${name} MessageDigest not available")) - new JavaSecurityDigest(name, p) - } + /** + * Get a hash for a specific name. + */ + def fromName[F[_], G[_]](name: String)(implicit F: Sync[F], G: Applicative[G]): F[Hash1[G]] = + F.delay { + // `Security#getProviders` is a mutable array, so cache the `Provider` + val p = Providers.get().messageDigest(name) match { + case Left(e) => throw e + case Right(p) => p + } + new JavaSecurityDigest(name, p)(G) + } def apply[F[_]](algorithm: HashAlgorithm)(implicit F: Sync[F]): F[Hash1[F]] = fromName( algorithm.toStringJava) diff --git a/core/jvm/src/main/scala/bobcats/HashPlatform.scala b/core/jvm/src/main/scala/bobcats/HashPlatform.scala index eabde77..872169c 100644 --- a/core/jvm/src/main/scala/bobcats/HashPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/HashPlatform.scala @@ -16,39 +16,35 @@ package bobcats -import java.security.{NoSuchAlgorithmException, Security} import fs2.{Pipe, Stream} import cats.effect.kernel.{Async, Sync} import scodec.bits.ByteVector +import cats.ApplicativeThrow private[bobcats] trait HashCompanionPlatform { - private[bobcats] def forSync[F[_]](implicit F: Sync[F]): Hash[F] = { - // TODO: What to do with this? - val providers = Security.getProviders().clone() + private[bobcats] def forProviders[F[_]](providers: Providers)( + implicit F: ApplicativeThrow[F]): Hash[F] = { new UnsealedHash[F] { override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = { val name = algorithm.toStringJava - Hash1.providerForName(providers, name) match { - case None => - F.raiseError(new NoSuchAlgorithmException(s"${name} MessageDigest not available")) - case Some(p) => new JavaSecurityDigest(name, p).digest(data) + providers.messageDigest(name) match { + case Left(e) => F.raiseError(e) + case Right(p) => new JavaSecurityDigest(name, p).digest(data) } } override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = { val name = algorithm.toStringJava - Hash1.providerForName(providers, name) match { - case None => - _ => - Stream.eval( - F.raiseError( - new NoSuchAlgorithmException(s"${name} MessageDigest not available"))) - case Some(p) => new JavaSecurityDigest(name, p).pipe + providers.messageDigest(name) match { + case Left(e) => + _ => Stream.eval(F.raiseError(e)) + case Right(p) => new JavaSecurityDigest(name, p).pipe } } } } - def forAsync[F[_]](implicit F: Async[F]): Hash[F] = forSync(F) + def forSync[F[_]](implicit F: Sync[F]): F[Hash[F]] = F.delay(forProviders(Providers.get())(F)) + def forAsync[F[_]: Async]: F[Hash[F]] = forSync } diff --git a/core/jvm/src/main/scala/bobcats/Providers.scala b/core/jvm/src/main/scala/bobcats/Providers.scala new file mode 100644 index 0000000..0220e5c --- /dev/null +++ b/core/jvm/src/main/scala/bobcats/Providers.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2021 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 bobcats + +import java.security.{NoSuchAlgorithmException, Security, Provider} + +private[bobcats] final class Providers(val ps: Array[Provider]) extends AnyVal { + + private def provider(service: String, name: String): Option[Provider] = + ps.find(provider => provider.getService(service, name) != null) + + def messageDigest(name: String): Either[NoSuchAlgorithmException, Provider] = + provider("MessageDigest", name).toRight(new NoSuchAlgorithmException(s"${name} MessageDigest not available")) + + def messageDigestThrow(name: String): Provider = ??? + +} + +private[bobcats] object Providers { + def get(): Providers = new Providers(Security.getProviders().clone()) +} diff --git a/core/shared/src/main/scala/bobcats/Crypto.scala b/core/shared/src/main/scala/bobcats/Crypto.scala index 63fca7f..e441983 100644 --- a/core/shared/src/main/scala/bobcats/Crypto.scala +++ b/core/shared/src/main/scala/bobcats/Crypto.scala @@ -16,8 +16,6 @@ package bobcats -import cats.effect.IO - sealed trait Crypto[F[_]] { def hash: Hash[F] def hmac: Hmac[F] @@ -26,9 +24,5 @@ sealed trait Crypto[F[_]] { private[bobcats] trait UnsealedCrypto[F[_]] extends Crypto[F] object Crypto extends CryptoCompanionPlatform { - - implicit def forIO: Crypto[IO] = forAsync - def apply[F[_]](implicit crypto: Crypto[F]): crypto.type = crypto - } diff --git a/core/shared/src/main/scala/bobcats/Hash.scala b/core/shared/src/main/scala/bobcats/Hash.scala index 90b7e0a..bb359e6 100644 --- a/core/shared/src/main/scala/bobcats/Hash.scala +++ b/core/shared/src/main/scala/bobcats/Hash.scala @@ -16,7 +16,6 @@ package bobcats -import cats.effect.IO import scodec.bits.ByteVector import fs2.Pipe @@ -28,9 +27,5 @@ sealed trait Hash[F[_]] { private[bobcats] trait UnsealedHash[F[_]] extends Hash[F] object Hash extends HashCompanionPlatform { - - implicit def forIO: Hash[IO] = forAsync - def apply[F[_]](implicit hash: Hash[F]): hash.type = hash - } diff --git a/core/shared/src/test/scala/bobcats/CryptoSuite.scala b/core/shared/src/test/scala/bobcats/CryptoSuite.scala new file mode 100644 index 0000000..59b56c2 --- /dev/null +++ b/core/shared/src/test/scala/bobcats/CryptoSuite.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2021 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 bobcats + +import cats.effect.{IO, Resource} +import munit.CatsEffectSuite + +trait CryptoSuite extends CatsEffectSuite { + + private val cryptoFixture = ResourceSuiteLocalFixture( + "crypto", + // TODO: If we want to pool, which would be quite nice. This _ought_ to return a resouce + Resource.make(Crypto.forAsync[IO])(_ => IO.unit) + ) + + override def munitFixtures = List(cryptoFixture) + + implicit def crypto: Crypto[IO] = cryptoFixture() + implicit def hash: Hash[IO] = crypto.hash + +} + diff --git a/core/shared/src/test/scala/bobcats/HashSuite.scala b/core/shared/src/test/scala/bobcats/HashSuite.scala index bb0da08..f81ed39 100644 --- a/core/shared/src/test/scala/bobcats/HashSuite.scala +++ b/core/shared/src/test/scala/bobcats/HashSuite.scala @@ -16,90 +16,58 @@ package bobcats -import cats.effect.{IO, MonadCancelThrow} -import cats.Functor -import cats.syntax.all._ -import munit.CatsEffectSuite +import cats.effect.IO import scodec.bits.ByteVector +import fs2.{Chunk, Stream} -import scala.reflect.ClassTag - -class HashSuite extends CatsEffectSuite { +class HashSuite extends CryptoSuite { import HashAlgorithm._ val data = ByteVector.encodeAscii("The quick brown fox jumps over the lazy dog").toOption.get - def testHash[F[_]: Hash: cats.Monad](algorithm: HashAlgorithm, expect: String)( - implicit ct: ClassTag[F[Nothing]]) = - test(s"$algorithm with ${ct.runtimeClass.getSimpleName()}") { - Hash[F].digest(algorithm, data).map { obtained => - assertEquals( - obtained, - ByteVector.fromHex(expect).get - ) - } - - Hash1[cats.effect.IO](algorithm).flatMap { digest => - digest.digest(data).map { obtained => - assertEquals( - obtained, - ByteVector.fromHex(expect).get - ) - } - } - } - - // def testHashIncremental[F[_]: Hash: MonadCancelThrow]( - // algorithm: HashAlgorithm, - // expect: String)(implicit ct: ClassTag[F[Nothing]]) = - // test(s"incremental $algorithm with ${ct.runtimeClass.getSimpleName()}") { - // Hash[F].incremental(algorithm).use { digest => - // (digest.update(data) *> digest.reset *> digest.update(data) *> digest.get) - // .map { obtained => - // assertEquals( - // obtained, - // ByteVector.fromHex(expect).get - // ) - // } - // } - // } - - def testEmpty[F[_]: Hash: Functor](algorithm: HashAlgorithm, expect: String)( - implicit ct: ClassTag[F[Nothing]]) = - test(s"empty $algorithm with ${ct.runtimeClass.getSimpleName()}") { - Hash[F].digest(algorithm, ByteVector.empty).map { obtained => - assertEquals( - obtained, - ByteVector.fromHex(expect).get - ) - } + def testHash( + algorithm: HashAlgorithm, + name: String, + data: ByteVector, + expect: String, + ignoredRuntimes: Set[String]) = + test(s"$algorithm for $name vector") { + val runtime = BuildInfo.runtime + assume(!ignoredRuntimes.contains(runtime), s"${runtime} does not support ${algorithm}") + val bytes = ByteVector.fromHex(expect).get + Hash[IO].digest(algorithm, data).assertEquals(bytes) *> + Stream + .chunk(Chunk.byteVector(data)) + .through(Hash[IO].digestPipe(algorithm)) + .compile + .to(ByteVector) + .assertEquals(bytes) } - def tests[F[_]: Hash: MonadCancelThrow](implicit ct: ClassTag[F[Nothing]]) = { - if (Set("JVM", "NodeJS", "Native").contains(BuildInfo.runtime)) - testHash[F](MD5, "9e107d9d372bb6826bd81d3542a419d6") - - // if (Set("JVM", "NodeJS", "Native").contains(BuildInfo.runtime)) { - // testHashIncremental[F](MD5, "9e107d9d372bb6826bd81d3542a419d6") - // testHashIncremental[F](SHA1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") - // } - - testHash[F](SHA1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") - testEmpty[F](SHA1, "da39a3ee5e6b4b0d3255bfef95601890afd80709") - - testHash[F](SHA256, "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592") - testEmpty[F](SHA256, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") - testHash[F]( - SHA512, - "07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6") - - testEmpty[F](SHA256, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") - testEmpty[F]( - SHA512, - "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") - } - - tests[IO] + def testVector( + algorithm: HashAlgorithm, + expect: String, + ignoredRuntimes: Set[String] = Set()) = + testHash(algorithm, "example", data, expect, ignoredRuntimes) + + def testEmpty( + algorithm: HashAlgorithm, + expect: String, + ignoredRuntimes: Set[String] = Set()) = + testHash(algorithm, "empty", ByteVector.empty, expect, ignoredRuntimes) + + testVector(SHA1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") + testEmpty(SHA1, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + testVector(SHA256, "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592") + testEmpty(SHA256, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + testVector( + SHA512, + "07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6") + testEmpty( + SHA512, + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") + + testVector(MD5, "9e107d9d372bb6826bd81d3542a419d6", ignoredRuntimes = Set("Browser")) } From 3e0a99eff38569da20d2f653d8e490bf4b30fe14 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 9 Aug 2023 22:20:39 +0100 Subject: [PATCH 08/17] Add native platform --- .../main/scala/bobcats/CryptoPlatform.scala | 6 +- .../main/scala/bobcats/Hash1Platform.scala | 7 +- .../src/main/scala/bobcats/Providers.scala | 5 +- .../main/scala/bobcats/CryptoPlatform.scala | 17 ++- .../main/scala/bobcats/Hash1Platform.scala | 110 ++++++++++++++++++ .../src/main/scala/bobcats/HashPlatform.scala | 103 +++------------- .../main/scala/bobcats/openssl/crypto.scala | 10 ++ .../src/main/scala/bobcats/openssl/evp.scala | 13 +++ .../src/test/scala/bobcats/CryptoSuite.scala | 6 +- .../src/test/scala/bobcats/HashSuite.scala | 3 + 10 files changed, 179 insertions(+), 101 deletions(-) create mode 100644 core/native/src/main/scala/bobcats/Hash1Platform.scala diff --git a/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala b/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala index 445911e..cb2ad7a 100644 --- a/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala @@ -16,7 +16,7 @@ package bobcats -import cats.effect.kernel.{Async, Sync} +import cats.effect.kernel.{Async, Resource, Sync} private[bobcats] trait CryptoCompanionPlatform { @@ -30,4 +30,8 @@ private[bobcats] trait CryptoCompanionPlatform { def forAsync[F[_]: Async]: F[Crypto[F]] = forSync + def forSyncResource[F[_]: Sync]: Resource[F, Crypto[F]] = Resource.eval(forSync[F]) + + def forAsyncResource[F[_]: Async]: Resource[F, Crypto[F]] = Resource.eval(forSync[F]) + } diff --git a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala index 1cbd4cc..d6921bb 100644 --- a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala +++ b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala @@ -19,7 +19,7 @@ package bobcats import java.security.{MessageDigest, Provider} import scodec.bits.ByteVector import cats.Applicative -import cats.effect.Sync +import cats.effect.{Resource, Sync} import fs2.{Chunk, Pipe, Stream} private final class JavaSecurityDigest[F[_]](algorithm: String, provider: Provider)( @@ -59,7 +59,10 @@ private[bobcats] trait Hash1CompanionPlatform { new JavaSecurityDigest(name, p)(G) } - def apply[F[_]](algorithm: HashAlgorithm)(implicit F: Sync[F]): F[Hash1[F]] = fromName( + def forSync[F[_]](algorithm: HashAlgorithm)(implicit F: Sync[F]): F[Hash1[F]] = fromName( algorithm.toStringJava) + def forSyncResource[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = + Resource.eval(forSync[F](algorithm)) + } diff --git a/core/jvm/src/main/scala/bobcats/Providers.scala b/core/jvm/src/main/scala/bobcats/Providers.scala index 0220e5c..f7079ce 100644 --- a/core/jvm/src/main/scala/bobcats/Providers.scala +++ b/core/jvm/src/main/scala/bobcats/Providers.scala @@ -16,7 +16,7 @@ package bobcats -import java.security.{NoSuchAlgorithmException, Security, Provider} +import java.security.{NoSuchAlgorithmException, Provider, Security} private[bobcats] final class Providers(val ps: Array[Provider]) extends AnyVal { @@ -24,7 +24,8 @@ private[bobcats] final class Providers(val ps: Array[Provider]) extends AnyVal { ps.find(provider => provider.getService(service, name) != null) def messageDigest(name: String): Either[NoSuchAlgorithmException, Provider] = - provider("MessageDigest", name).toRight(new NoSuchAlgorithmException(s"${name} MessageDigest not available")) + provider("MessageDigest", name).toRight( + new NoSuchAlgorithmException(s"${name} MessageDigest not available")) def messageDigestThrow(name: String): Provider = ??? diff --git a/core/native/src/main/scala/bobcats/CryptoPlatform.scala b/core/native/src/main/scala/bobcats/CryptoPlatform.scala index c836505..5625be9 100644 --- a/core/native/src/main/scala/bobcats/CryptoPlatform.scala +++ b/core/native/src/main/scala/bobcats/CryptoPlatform.scala @@ -16,12 +16,19 @@ package bobcats -import cats.effect.kernel.Async +import cats.effect.kernel.{Async, Resource, Sync} + +import openssl.crypto._ private[bobcats] trait CryptoCompanionPlatform { - implicit def forAsync[F[_]: Async]: Crypto[F] = - new UnsealedCrypto[F] { - override def hash: Hash[F] = Hash[F] - override def hmac: Hmac[F] = Hmac[F] + def forSyncResource[F[_]](implicit F: Sync[F]): Resource[F, Crypto[F]] = + Resource.make(F.delay(OSSL_LIB_CTX_new()))(ctx => F.delay(OSSL_LIB_CTX_free(ctx))).map { + ctx => + new UnsealedCrypto[F] { + override def hash: Hash[F] = Hash.forContext(ctx) + override def hmac: Hmac[F] = ??? + } } + + def forAsyncResource[F[_]: Async]: Resource[F, Crypto[F]] = forSyncResource[F] } diff --git a/core/native/src/main/scala/bobcats/Hash1Platform.scala b/core/native/src/main/scala/bobcats/Hash1Platform.scala new file mode 100644 index 0000000..e443e29 --- /dev/null +++ b/core/native/src/main/scala/bobcats/Hash1Platform.scala @@ -0,0 +1,110 @@ +/* + * Copyright 2021 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 bobcats + +import cats.effect.kernel.{Sync, Resource} +import scodec.bits.ByteVector +import scalanative.unsafe._ +import scalanative.unsigned._ +import openssl._ +import openssl.err._ +import openssl.evp._ +import fs2.{Chunk, Pipe, Stream} + +private[bobcats] final class NativeEvpDigest[F[_]](digest: Ptr[EVP_MD])(implicit F: Sync[F]) extends UnsealedHash1[F] { + def digest(data: ByteVector): F[ByteVector] = { + val ctx = EVP_MD_CTX_new() + val d = data.toArrayUnsafe + try { + val md = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE) + val s = stackalloc[CInt]() + if (EVP_DigestInit_ex(ctx, digest, null) != 1) { + throw Error("EVP_DigestInit_ex", ERR_get_error()) + } + val len = d.length + if (EVP_DigestUpdate(ctx, if (len == 0) null else d.at(0), len.toULong) != 1) { + throw Error("EVP_DigestUpdate", ERR_get_error()) + } + if (EVP_DigestFinal_ex(ctx, md, s) != 1) { + throw Error("EVP_DigestFinal_ex", ERR_get_error()) + } + F.pure(ByteVector.fromPtr(md.asInstanceOf[Ptr[Byte]], s(0).toLong)) + } catch { + case e: Error => F.raiseError(e) + } finally { + EVP_MD_CTX_free(ctx) + } + } + + def pipe: Pipe[F, Byte, Byte] = { in => + Stream + .bracket(F.delay { + val ctx = EVP_MD_CTX_new() + if (EVP_DigestInit_ex(ctx, digest, null) != 1) { + throw Error("EVP_DigestInit_ex", ERR_get_error()) + } + ctx + })(ctx => F.delay(EVP_MD_CTX_free(ctx))) + .flatMap { ctx => + in.chunks + .evalMap { chunk => + F.delay { + val d = chunk.toByteVector.toArrayUnsafe + val len = d.length + if (EVP_DigestUpdate(ctx, if (len == 0) null else d.at(0), len.toULong) != 1) { + throw Error("EVP_DigestUpdate", ERR_get_error()) + } + } + } + .drain ++ Stream.eval { + F.delay { + val md = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE) + val s = stackalloc[CInt]() + + if (EVP_DigestFinal_ex(ctx, md, s) != 1) { + throw Error("EVP_DigestFinal_ex", ERR_get_error()) + } + Chunk.byteVector(ByteVector.fromPtr(md.asInstanceOf[Ptr[Byte]], s(0).toLong)) + } + }.unchunks + } + } +} + +private[bobcats] trait Hash1CompanionPlatform { + + private[bobcats] def evpAlgorithm(algorithm: HashAlgorithm): CString = { + import HashAlgorithm._ + algorithm match { + case MD5 => c"MD5" + case SHA1 => c"SHA1" + case SHA256 => c"SHA256" + case SHA512 => c"SHA512" + } + } + + def fromNameResource[F[_]](name: CString)(implicit F: Sync[F]): Resource[F, Hash1[F]] = + Resource.make(F.delay { + val md = EVP_MD_fetch(null, name, null) + md + })(md => F.delay(EVP_MD_free(md))).map { md => + new NativeEvpDigest(md) + } + + def forSyncResource[F[_] : Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = + fromNameResource(evpAlgorithm(algorithm)) +} diff --git a/core/native/src/main/scala/bobcats/HashPlatform.scala b/core/native/src/main/scala/bobcats/HashPlatform.scala index fec4c9b..56b1d42 100644 --- a/core/native/src/main/scala/bobcats/HashPlatform.scala +++ b/core/native/src/main/scala/bobcats/HashPlatform.scala @@ -16,104 +16,33 @@ package bobcats -import scala.scalanative.annotation.alwaysinline -import cats.effect.kernel.{Async, Resource, Sync} +import cats.effect.kernel.Sync import scodec.bits.ByteVector import scalanative.unsafe._ -import scalanative.unsigned._ import openssl._ -import openssl.err._ import openssl.evp._ - -private final class NativeEvpDigest[F[_]](val ctx: Ptr[EVP_MD_CTX], digest: Ptr[EVP_MD])( - implicit F: Sync[F]) - extends UnsealedDigest[F] { - - override def update(data: ByteVector): F[Unit] = F.delay { - - val d = data.toArrayUnsafe - val len = d.length - - if (EVP_DigestUpdate(ctx, if (len == 0) null else d.at(0), d.length.toULong) != 1) { - throw Error("EVP_DigestUpdate", ERR_get_error()) - } - } - - override val reset = F.delay { - if (EVP_MD_CTX_reset(ctx) != 1) { - throw Error("EVP_MD_Ctx_reset", ERR_get_error()) - } - if (EVP_DigestInit_ex(ctx, digest, null) != 1) { - throw Error("EVP_DigestInit_ex", ERR_get_error()) - } - } - - override def get: F[ByteVector] = F.delay { - val md = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE) - val s = stackalloc[CInt]() - - if (EVP_DigestFinal_ex(ctx, md, s) != 1) { - throw Error("EVP_DigestFinal_ex", ERR_get_error()) - } - ByteVector.fromPtr(md.asInstanceOf[Ptr[Byte]], s(0).toLong) - } - - def free: F[Unit] = F.delay(EVP_MD_CTX_free(ctx)) - -} +import fs2.{Pipe, Stream} private[bobcats] trait HashCompanionPlatform { - implicit def forAsync[F[_]](implicit F: Async[F]): Hash[F] = forSync - private[bobcats] def forSync[F[_]](implicit F: Sync[F]): Hash[F] = + private[bobcats] def forContext[F[_]](ctx: Ptr[OSSL_LIB_CTX])(implicit F: Sync[F]): Hash[F] = new UnsealedHash[F] { - @alwaysinline private def evpAlgorithm(algorithm: HashAlgorithm): Ptr[EVP_MD] = { - import HashAlgorithm._ - algorithm match { - case MD5 => EVP_md5() - case SHA1 => EVP_sha1() - case SHA256 => EVP_sha256() - case SHA512 => EVP_sha512() - } - } - - override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = { - val digest = evpAlgorithm(algorithm) - Resource.make(F.delay { - val ctx = EVP_MD_CTX_new() - if (EVP_DigestInit_ex(ctx, digest, null) != 1) { - throw Error("EVP_DigestInit_ex", ERR_get_error()) - } - new NativeEvpDigest(ctx, digest)(F) - })(_.free) - } - override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = { + val md = EVP_MD_fetch(ctx, Hash1.evpAlgorithm(algorithm), null) + // Note, this is eager currently which is why the cleanup is working + val digest = new NativeEvpDigest(md).digest(data) + EVP_MD_free(md) + digest + } - val digest = evpAlgorithm(algorithm) - - F.delay { - val ctx = EVP_MD_CTX_new() - val d = data.toArrayUnsafe - try { - val md = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE) - val s = stackalloc[CInt]() - if (EVP_DigestInit_ex(ctx, digest, null) != 1) { - throw Error("EVP_DigestInit_ex", ERR_get_error()) - } - val len = d.length - if (EVP_DigestUpdate(ctx, if (len == 0) null else d.at(0), len.toULong) != 1) { - throw Error("EVP_DigestUpdate", ERR_get_error()) - } - if (EVP_DigestFinal_ex(ctx, md, s) != 1) { - throw Error("EVP_DigestFinal_ex", ERR_get_error()) - } - ByteVector.fromPtr(md.asInstanceOf[Ptr[Byte]], s(0).toLong) - } finally { - EVP_MD_CTX_free(ctx) - } + override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = + in => { + Stream + .bracket(F.delay { + EVP_MD_fetch(null, Hash1.evpAlgorithm(algorithm), null) + })(md => F.delay(EVP_MD_free(md))) + .flatMap { md => in.through(new NativeEvpDigest(md).pipe) } } - } } } diff --git a/core/native/src/main/scala/bobcats/openssl/crypto.scala b/core/native/src/main/scala/bobcats/openssl/crypto.scala index 4c28fcf..65cb8c4 100644 --- a/core/native/src/main/scala/bobcats/openssl/crypto.scala +++ b/core/native/src/main/scala/bobcats/openssl/crypto.scala @@ -25,6 +25,16 @@ import scala.annotation.nowarn @nowarn("msg=never used") private[bobcats] object crypto { + /** + * See [[https://www.openssl.org/docs/man3.1/man3/OSSL_LIB_CTX_new.html]] + */ + def OSSL_LIB_CTX_new(): Ptr[OSSL_LIB_CTX] = extern + + /** + * See [[https://www.openssl.org/docs/man3.1/man3/OSSL_LIB_CTX_free.html]] + */ + def OSSL_LIB_CTX_free(ctx: Ptr[OSSL_LIB_CTX]): Unit = extern + /** * See [[https://www.openssl.org/docs/man3.1/man3/CRYPTO_memcmp.html]] */ diff --git a/core/native/src/main/scala/bobcats/openssl/evp.scala b/core/native/src/main/scala/bobcats/openssl/evp.scala index 4ab75df..7a981d6 100644 --- a/core/native/src/main/scala/bobcats/openssl/evp.scala +++ b/core/native/src/main/scala/bobcats/openssl/evp.scala @@ -31,6 +31,19 @@ private[bobcats] object evp { final val EVP_MAX_MD_SIZE = 64 + /** + * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_fetch.html]] + */ + def EVP_MD_fetch( + ctx: Ptr[OSSL_LIB_CTX], + algorithm: CString, + properties: CString): Ptr[EVP_MD] = extern + + /** + * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_free.html]] + */ + def EVP_MD_free(ctx: Ptr[EVP_MD]): Unit = extern + /** * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_CTX_new.html]] */ diff --git a/core/shared/src/test/scala/bobcats/CryptoSuite.scala b/core/shared/src/test/scala/bobcats/CryptoSuite.scala index 59b56c2..5a7bc25 100644 --- a/core/shared/src/test/scala/bobcats/CryptoSuite.scala +++ b/core/shared/src/test/scala/bobcats/CryptoSuite.scala @@ -16,15 +16,14 @@ package bobcats -import cats.effect.{IO, Resource} +import cats.effect.IO import munit.CatsEffectSuite trait CryptoSuite extends CatsEffectSuite { private val cryptoFixture = ResourceSuiteLocalFixture( "crypto", - // TODO: If we want to pool, which would be quite nice. This _ought_ to return a resouce - Resource.make(Crypto.forAsync[IO])(_ => IO.unit) + Crypto.forAsyncResource[IO] ) override def munitFixtures = List(cryptoFixture) @@ -33,4 +32,3 @@ trait CryptoSuite extends CatsEffectSuite { implicit def hash: Hash[IO] = crypto.hash } - diff --git a/core/shared/src/test/scala/bobcats/HashSuite.scala b/core/shared/src/test/scala/bobcats/HashSuite.scala index f81ed39..fffc460 100644 --- a/core/shared/src/test/scala/bobcats/HashSuite.scala +++ b/core/shared/src/test/scala/bobcats/HashSuite.scala @@ -42,6 +42,9 @@ class HashSuite extends CryptoSuite { .through(Hash[IO].digestPipe(algorithm)) .compile .to(ByteVector) + .assertEquals(bytes) *> Hash1 + .forSyncResource[IO](algorithm) + .use(_.digest(data)) .assertEquals(bytes) } From 8220dc031adc32537e08374a03f4d73bb036e19b Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 9 Aug 2023 22:21:24 +0100 Subject: [PATCH 09/17] remove uneeded function --- core/jvm/src/main/scala/bobcats/Providers.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/jvm/src/main/scala/bobcats/Providers.scala b/core/jvm/src/main/scala/bobcats/Providers.scala index f7079ce..1ab8107 100644 --- a/core/jvm/src/main/scala/bobcats/Providers.scala +++ b/core/jvm/src/main/scala/bobcats/Providers.scala @@ -27,8 +27,6 @@ private[bobcats] final class Providers(val ps: Array[Provider]) extends AnyVal { provider("MessageDigest", name).toRight( new NoSuchAlgorithmException(s"${name} MessageDigest not available")) - def messageDigestThrow(name: String): Provider = ??? - } private[bobcats] object Providers { From bf4ac500ecf8ee407f9d675ee8e760929a599e84 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Wed, 9 Aug 2023 22:28:07 +0100 Subject: [PATCH 10/17] run `prePR` --- .../main/scala/bobcats/Hash1Platform.scala | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/core/native/src/main/scala/bobcats/Hash1Platform.scala b/core/native/src/main/scala/bobcats/Hash1Platform.scala index e443e29..8bcef18 100644 --- a/core/native/src/main/scala/bobcats/Hash1Platform.scala +++ b/core/native/src/main/scala/bobcats/Hash1Platform.scala @@ -16,7 +16,7 @@ package bobcats -import cats.effect.kernel.{Sync, Resource} +import cats.effect.kernel.{Resource, Sync} import scodec.bits.ByteVector import scalanative.unsafe._ import scalanative.unsigned._ @@ -25,7 +25,8 @@ import openssl.err._ import openssl.evp._ import fs2.{Chunk, Pipe, Stream} -private[bobcats] final class NativeEvpDigest[F[_]](digest: Ptr[EVP_MD])(implicit F: Sync[F]) extends UnsealedHash1[F] { +private[bobcats] final class NativeEvpDigest[F[_]](digest: Ptr[EVP_MD])(implicit F: Sync[F]) + extends UnsealedHash1[F] { def digest(data: ByteVector): F[ByteVector] = { val ctx = EVP_MD_CTX_new() val d = data.toArrayUnsafe @@ -98,13 +99,13 @@ private[bobcats] trait Hash1CompanionPlatform { } def fromNameResource[F[_]](name: CString)(implicit F: Sync[F]): Resource[F, Hash1[F]] = - Resource.make(F.delay { - val md = EVP_MD_fetch(null, name, null) - md - })(md => F.delay(EVP_MD_free(md))).map { md => - new NativeEvpDigest(md) - } + Resource + .make(F.delay { + val md = EVP_MD_fetch(null, name, null) + md + })(md => F.delay(EVP_MD_free(md))) + .map { md => new NativeEvpDigest(md) } - def forSyncResource[F[_] : Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = + def forSyncResource[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = fromNameResource(evpAlgorithm(algorithm)) } From 8e3be193cb9a93c574041bce505322e2ecdb1e8d Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Thu, 10 Aug 2023 15:55:51 +0100 Subject: [PATCH 11/17] Add `Hmac` back in. --- .../main/scala/bobcats/CryptoPlatform.scala | 24 ++-- .../main/scala/bobcats/Hash1Platform.scala | 27 ++--- .../src/main/scala/bobcats/HashPlatform.scala | 10 +- .../src/main/scala/bobcats/HmacPlatform.scala | 12 +- .../src/main/scala/bobcats/Providers.scala | 3 + .../src/main/scala/bobcats/Context.scala | 30 +++++ .../main/scala/bobcats/CryptoPlatform.scala | 18 ++- .../main/scala/bobcats/Hash1Platform.scala | 112 +++++++++--------- .../src/main/scala/bobcats/HashPlatform.scala | 13 +- .../src/main/scala/bobcats/HmacPlatform.scala | 31 ++--- .../src/main/scala/bobcats/Algorithm.scala | 8 ++ .../src/test/scala/bobcats/CryptoSuite.scala | 9 +- .../src/test/scala/bobcats/HashSuite.scala | 31 +++-- .../src/test/scala/bobcats/HmacSuite.scala | 53 +++------ .../src/test/scala/bobcats/HotpSuite.scala | 25 ++-- 15 files changed, 211 insertions(+), 195 deletions(-) create mode 100644 core/native/src/main/scala/bobcats/Context.scala diff --git a/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala b/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala index cb2ad7a..73b61fb 100644 --- a/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/CryptoPlatform.scala @@ -20,18 +20,14 @@ import cats.effect.kernel.{Async, Resource, Sync} private[bobcats] trait CryptoCompanionPlatform { - def forSync[F[_]](implicit F: Sync[F]): F[Crypto[F]] = F.delay { - val providers = Providers.get() - new UnsealedCrypto[F] { - override def hash: Hash[F] = Hash.forProviders(providers) - override def hmac: Hmac[F] = ??? - } - } - - def forAsync[F[_]: Async]: F[Crypto[F]] = forSync - - def forSyncResource[F[_]: Sync]: Resource[F, Crypto[F]] = Resource.eval(forSync[F]) - - def forAsyncResource[F[_]: Async]: Resource[F, Crypto[F]] = Resource.eval(forSync[F]) - + def forSync[F[_]](implicit F: Sync[F]): Resource[F, Crypto[F]] = + Resource.eval(F.delay { + val providers = Providers.get() + new UnsealedCrypto[F] { + override def hash: Hash[F] = Hash.forProviders(providers) + override def hmac: Hmac[F] = Hmac.forProviders(providers) + } + }) + + def forAsync[F[_]: Async]: Resource[F, Crypto[F]] = forSync } diff --git a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala index d6921bb..83ea5c2 100644 --- a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala +++ b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala @@ -18,12 +18,11 @@ package bobcats import java.security.{MessageDigest, Provider} import scodec.bits.ByteVector -import cats.Applicative import cats.effect.{Resource, Sync} import fs2.{Chunk, Pipe, Stream} private final class JavaSecurityDigest[F[_]](algorithm: String, provider: Provider)( - implicit F: Applicative[F]) + implicit F: Sync[F]) extends UnsealedHash1[F] { override def digest(data: ByteVector): F[ByteVector] = F.pure { @@ -34,10 +33,13 @@ private final class JavaSecurityDigest[F[_]](algorithm: String, provider: Provid override val pipe: Pipe[F, Byte, Byte] = in => - in.chunks - .fold(MessageDigest.getInstance(algorithm, provider)) { (h, data) => - h.update(data.toByteBuffer) - h + Stream + .eval(F.delay(MessageDigest.getInstance(algorithm, provider))) + .flatMap { digest => + in.chunks.fold(digest) { (h, data) => + h.update(data.toByteBuffer) + h + } } .flatMap { h => Stream.chunk(Chunk.array(h.digest())) } @@ -47,22 +49,19 @@ private final class JavaSecurityDigest[F[_]](algorithm: String, provider: Provid private[bobcats] trait Hash1CompanionPlatform { /** - * Get a hash for a specific name. + * Get a hash for a specific name used by the Java security providers. */ - def fromName[F[_], G[_]](name: String)(implicit F: Sync[F], G: Applicative[G]): F[Hash1[G]] = + def fromJavaName[F[_]](name: String)(implicit F: Sync[F]): F[Hash1[F]] = F.delay { // `Security#getProviders` is a mutable array, so cache the `Provider` val p = Providers.get().messageDigest(name) match { case Left(e) => throw e case Right(p) => p } - new JavaSecurityDigest(name, p)(G) + new JavaSecurityDigest(name, p)(F) } - def forSync[F[_]](algorithm: HashAlgorithm)(implicit F: Sync[F]): F[Hash1[F]] = fromName( - algorithm.toStringJava) - - def forSyncResource[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = - Resource.eval(forSync[F](algorithm)) + def forSync[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = + Resource.eval(fromJavaName[F](algorithm.toStringJava)) } diff --git a/core/jvm/src/main/scala/bobcats/HashPlatform.scala b/core/jvm/src/main/scala/bobcats/HashPlatform.scala index 872169c..a6e2e13 100644 --- a/core/jvm/src/main/scala/bobcats/HashPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/HashPlatform.scala @@ -17,14 +17,13 @@ package bobcats import fs2.{Pipe, Stream} -import cats.effect.kernel.{Async, Sync} +import cats.effect.kernel.{Async, Resource, Sync} import scodec.bits.ByteVector -import cats.ApplicativeThrow private[bobcats] trait HashCompanionPlatform { private[bobcats] def forProviders[F[_]](providers: Providers)( - implicit F: ApplicativeThrow[F]): Hash[F] = { + implicit F: Sync[F]): Hash[F] = { new UnsealedHash[F] { override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = { val name = algorithm.toStringJava @@ -45,6 +44,7 @@ private[bobcats] trait HashCompanionPlatform { } } - def forSync[F[_]](implicit F: Sync[F]): F[Hash[F]] = F.delay(forProviders(Providers.get())(F)) - def forAsync[F[_]: Async]: F[Hash[F]] = forSync + def forSync[F[_]](implicit F: Sync[F]): Resource[F, Hash[F]] = + Resource.eval(F.delay(forProviders(Providers.get())(F))) + def forAsync[F[_]: Async]: Resource[F, Hash[F]] = forSync } diff --git a/core/jvm/src/main/scala/bobcats/HmacPlatform.scala b/core/jvm/src/main/scala/bobcats/HmacPlatform.scala index d92988b..b8d6aca 100644 --- a/core/jvm/src/main/scala/bobcats/HmacPlatform.scala +++ b/core/jvm/src/main/scala/bobcats/HmacPlatform.scala @@ -16,7 +16,7 @@ package bobcats -import cats.effect.kernel.Async +import cats.effect.kernel.Sync import scodec.bits.ByteVector import javax.crypto @@ -26,12 +26,18 @@ private[bobcats] trait HmacPlatform[F[_]] { } private[bobcats] trait HmacCompanionPlatform { - implicit def forAsync[F[_]](implicit F: Async[F]): Hmac[F] = + + private[bobcats] def forProviders[F[_]](providers: Providers)(implicit F: Sync[F]): Hmac[F] = new UnsealedHmac[F] { override def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] = F.catchNonFatal { - val mac = crypto.Mac.getInstance(key.algorithm.toStringJava) + val alg = key.algorithm.toStringJava + val provider = providers.mac(alg) match { + case Left(e) => throw e + case Right(p) => p + } + val mac = crypto.Mac.getInstance(key.algorithm.toStringJava, provider) val sk = key.toJava mac.init(sk) mac.update(data.toByteBuffer) diff --git a/core/jvm/src/main/scala/bobcats/Providers.scala b/core/jvm/src/main/scala/bobcats/Providers.scala index 1ab8107..d5d1de9 100644 --- a/core/jvm/src/main/scala/bobcats/Providers.scala +++ b/core/jvm/src/main/scala/bobcats/Providers.scala @@ -27,6 +27,9 @@ private[bobcats] final class Providers(val ps: Array[Provider]) extends AnyVal { provider("MessageDigest", name).toRight( new NoSuchAlgorithmException(s"${name} MessageDigest not available")) + def mac(name: String): Either[NoSuchAlgorithmException, Provider] = + provider("Mac", name).toRight(new NoSuchAlgorithmException(s"${name} Mac not available")) + } private[bobcats] object Providers { diff --git a/core/native/src/main/scala/bobcats/Context.scala b/core/native/src/main/scala/bobcats/Context.scala new file mode 100644 index 0000000..0bbab1e --- /dev/null +++ b/core/native/src/main/scala/bobcats/Context.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2021 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 bobcats + +import openssl._ +import openssl.crypto._ +import scala.scalanative.unsafe._ + +import cats.effect.kernel.{Resource, Sync} + +private[bobcats] object Context { + def apply[F[_]](implicit F: Sync[F]): Resource[F, Ptr[OSSL_LIB_CTX]] = + Resource.make(F.delay(OSSL_LIB_CTX_new()))(ctx => F.delay(OSSL_LIB_CTX_free(ctx))) +} + + diff --git a/core/native/src/main/scala/bobcats/CryptoPlatform.scala b/core/native/src/main/scala/bobcats/CryptoPlatform.scala index 5625be9..a8199cf 100644 --- a/core/native/src/main/scala/bobcats/CryptoPlatform.scala +++ b/core/native/src/main/scala/bobcats/CryptoPlatform.scala @@ -18,17 +18,15 @@ package bobcats import cats.effect.kernel.{Async, Resource, Sync} -import openssl.crypto._ - private[bobcats] trait CryptoCompanionPlatform { - def forSyncResource[F[_]](implicit F: Sync[F]): Resource[F, Crypto[F]] = - Resource.make(F.delay(OSSL_LIB_CTX_new()))(ctx => F.delay(OSSL_LIB_CTX_free(ctx))).map { - ctx => - new UnsealedCrypto[F] { - override def hash: Hash[F] = Hash.forContext(ctx) - override def hmac: Hmac[F] = ??? - } + + def forSync[F[_]](implicit F: Sync[F]): Resource[F, Crypto[F]] = + Context[F].map { ctx => + new UnsealedCrypto[F] { + override def hash: Hash[F] = Hash.forContext(ctx) + override def hmac: Hmac[F] = Hmac.forContext(ctx) + } } - def forAsyncResource[F[_]: Async]: Resource[F, Crypto[F]] = forSyncResource[F] + def forAsync[F[_]: Async]: Resource[F, Crypto[F]] = forSync[F] } diff --git a/core/native/src/main/scala/bobcats/Hash1Platform.scala b/core/native/src/main/scala/bobcats/Hash1Platform.scala index 8bcef18..f0b7579 100644 --- a/core/native/src/main/scala/bobcats/Hash1Platform.scala +++ b/core/native/src/main/scala/bobcats/Hash1Platform.scala @@ -28,22 +28,12 @@ import fs2.{Chunk, Pipe, Stream} private[bobcats] final class NativeEvpDigest[F[_]](digest: Ptr[EVP_MD])(implicit F: Sync[F]) extends UnsealedHash1[F] { def digest(data: ByteVector): F[ByteVector] = { - val ctx = EVP_MD_CTX_new() val d = data.toArrayUnsafe + val ctx = EVP_MD_CTX_new() try { - val md = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE) - val s = stackalloc[CInt]() - if (EVP_DigestInit_ex(ctx, digest, null) != 1) { - throw Error("EVP_DigestInit_ex", ERR_get_error()) - } - val len = d.length - if (EVP_DigestUpdate(ctx, if (len == 0) null else d.at(0), len.toULong) != 1) { - throw Error("EVP_DigestUpdate", ERR_get_error()) - } - if (EVP_DigestFinal_ex(ctx, md, s) != 1) { - throw Error("EVP_DigestFinal_ex", ERR_get_error()) - } - F.pure(ByteVector.fromPtr(md.asInstanceOf[Ptr[Byte]], s(0).toLong)) + init(ctx, digest) + update(ctx, d) + F.pure(`final`(ctx)) } catch { case e: Error => F.raiseError(e) } finally { @@ -51,41 +41,46 @@ private[bobcats] final class NativeEvpDigest[F[_]](digest: Ptr[EVP_MD])(implicit } } - def pipe: Pipe[F, Byte, Byte] = { in => - Stream - .bracket(F.delay { - val ctx = EVP_MD_CTX_new() - if (EVP_DigestInit_ex(ctx, digest, null) != 1) { - throw Error("EVP_DigestInit_ex", ERR_get_error()) - } - ctx - })(ctx => F.delay(EVP_MD_CTX_free(ctx))) - .flatMap { ctx => - in.chunks - .evalMap { chunk => - F.delay { - val d = chunk.toByteVector.toArrayUnsafe - val len = d.length - if (EVP_DigestUpdate(ctx, if (len == 0) null else d.at(0), len.toULong) != 1) { - throw Error("EVP_DigestUpdate", ERR_get_error()) - } - } - } - .drain ++ Stream.eval { - F.delay { - val md = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE) - val s = stackalloc[CInt]() + private def update(ctx: Ptr[EVP_MD_CTX], data: Array[Byte]): Unit = { + val len = data.length + if (EVP_DigestUpdate(ctx, if (len == 0) null else data.at(0), len.toULong) != 1) { + throw Error("EVP_DigestUpdate", ERR_get_error()) + } + } + + private def init(ctx: Ptr[EVP_MD_CTX], digest: Ptr[EVP_MD]): Unit = + if (EVP_DigestInit_ex(ctx, digest, null) != 1) { + throw Error("EVP_DigestInit_ex", ERR_get_error()) + } + + private def `final`(ctx: Ptr[EVP_MD_CTX]): ByteVector = { + val md = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE) + val s = stackalloc[CInt]() + if (EVP_DigestFinal_ex(ctx, md, s) != 1) { + throw Error("EVP_DigestFinal_ex", ERR_get_error()) + } + ByteVector.fromPtr(md.asInstanceOf[Ptr[Byte]], s(0).toLong) + } - if (EVP_DigestFinal_ex(ctx, md, s) != 1) { - throw Error("EVP_DigestFinal_ex", ERR_get_error()) - } - Chunk.byteVector(ByteVector.fromPtr(md.asInstanceOf[Ptr[Byte]], s(0).toLong)) - } - }.unchunks - } + private val context: Stream[F, Ptr[EVP_MD_CTX]] = + Stream.bracket(F.delay { + val ctx = EVP_MD_CTX_new() + init(ctx, digest) + ctx + })(ctx => F.delay(EVP_MD_CTX_free(ctx))) + + def pipe: Pipe[F, Byte, Byte] = { in => + context.flatMap { ctx => + // Most of the calls throw, so wrap in a `delay` + in.chunks + .evalMap { chunk => F.delay(update(ctx, chunk.toByteVector.toArrayUnsafe)) } + .drain ++ Stream.eval(F.delay(Chunk.byteVector(`final`(ctx)))) + }.unchunks } } +import java.security.NoSuchAlgorithmException + private[bobcats] trait Hash1CompanionPlatform { private[bobcats] def evpAlgorithm(algorithm: HashAlgorithm): CString = { @@ -98,14 +93,25 @@ private[bobcats] trait Hash1CompanionPlatform { } } - def fromNameResource[F[_]](name: CString)(implicit F: Sync[F]): Resource[F, Hash1[F]] = + private[bobcats] def evpFetch(ctx: Ptr[OSSL_LIB_CTX], name: CString): Ptr[EVP_MD] = { + val md = EVP_MD_fetch(ctx, name, null) + if (md == null) { + throw new NoSuchAlgorithmException( + s"${fromCString(name)} Message Digest not available", + Error("EVP_MD_fetch", ERR_get_error()) + ) + } + md + } + + /** + * Create a hash for a particular name used by `libcrypto`. + */ + def fromCryptoName[F[_]](name: CString)(implicit F: Sync[F]): Resource[F, Hash1[F]] = Resource - .make(F.delay { - val md = EVP_MD_fetch(null, name, null) - md - })(md => F.delay(EVP_MD_free(md))) - .map { md => new NativeEvpDigest(md) } + .make(F.delay(evpFetch(null, name)))(md => F.delay(EVP_MD_free(md))) + .map(new NativeEvpDigest(_)) - def forSyncResource[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = - fromNameResource(evpAlgorithm(algorithm)) + def forSync[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = + fromCryptoName(evpAlgorithm(algorithm)) } diff --git a/core/native/src/main/scala/bobcats/HashPlatform.scala b/core/native/src/main/scala/bobcats/HashPlatform.scala index 56b1d42..4daa1fc 100644 --- a/core/native/src/main/scala/bobcats/HashPlatform.scala +++ b/core/native/src/main/scala/bobcats/HashPlatform.scala @@ -16,7 +16,7 @@ package bobcats -import cats.effect.kernel.Sync +import cats.effect.kernel.{Async, Resource, Sync} import scodec.bits.ByteVector import scalanative.unsafe._ import openssl._ @@ -29,7 +29,7 @@ private[bobcats] trait HashCompanionPlatform { new UnsealedHash[F] { override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = { - val md = EVP_MD_fetch(ctx, Hash1.evpAlgorithm(algorithm), null) + val md = Hash1.evpFetch(ctx, Hash1.evpAlgorithm(algorithm)) // Note, this is eager currently which is why the cleanup is working val digest = new NativeEvpDigest(md).digest(data) EVP_MD_free(md) @@ -38,11 +38,14 @@ private[bobcats] trait HashCompanionPlatform { override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = in => { + val alg = Hash1.evpAlgorithm(algorithm) Stream - .bracket(F.delay { - EVP_MD_fetch(null, Hash1.evpAlgorithm(algorithm), null) - })(md => F.delay(EVP_MD_free(md))) + .bracket(F.delay(Hash1.evpFetch(ctx, alg)))(md => F.delay(EVP_MD_free(md))) .flatMap { md => in.through(new NativeEvpDigest(md).pipe) } } } + + def forSync[F[_]: Sync]: Resource[F, Hash[F]] = Context[F].map(forContext[F]) + def forAsync[F[_]: Async]: Resource[F, Hash[F]] = forSync + } diff --git a/core/native/src/main/scala/bobcats/HmacPlatform.scala b/core/native/src/main/scala/bobcats/HmacPlatform.scala index 42d799f..f470e31 100644 --- a/core/native/src/main/scala/bobcats/HmacPlatform.scala +++ b/core/native/src/main/scala/bobcats/HmacPlatform.scala @@ -16,7 +16,7 @@ package bobcats -import cats.effect.kernel.Async +import cats.effect.kernel.{Async, Resource, Sync} import scodec.bits.ByteVector private[bobcats] trait HmacPlatform[F[_]] {} @@ -31,7 +31,11 @@ import scala.scalanative.libc._ import scala.scalanative.unsigned._ private[bobcats] trait HmacCompanionPlatform { - implicit def forAsync[F[_]](implicit F: Async[F]): Hmac[F] = + + def forAsync[F[_]: Async]: Resource[F, Hmac[F]] = forSync + def forSync[F[_]: Sync]: Resource[F, Hmac[F]] = Context[F].map(forContext[F]) + + private[bobcats] def forContext[F[_]](ctx: Ptr[OSSL_LIB_CTX])(implicit F: Sync[F]): Hmac[F] = new UnsealedHmac[F] { /** @@ -40,26 +44,19 @@ private[bobcats] trait HmacCompanionPlatform { override def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] = { key match { case SecretKeySpec(key, algorithm) => - import HmacAlgorithm._ - - val md = algorithm match { - case SHA1 => EVP_sha1() - case SHA256 => EVP_sha256() - case SHA512 => EVP_sha512() - } - val mdName = EVP_MD_get0_name(md) + val mdName = Hash1.evpAlgorithm(algorithm.hashAlgorithm) val mdLen = string.strlen(mdName) - F.delay { + F.catchNonFatal { val oneshot = stackalloc[CInt]() oneshot(0) = 1 val params = stackalloc[OSSL_PARAM](3) OSSL_MAC_PARAM_DIGEST(params(0), mdName, mdLen) OSSL_MAC_PARAM_DIGEST_ONESHOT(params(1), oneshot) OSSL_PARAM_END(params(2)) - val mac = EVP_MAC_fetch(null, c"HMAC", null) + val mac = EVP_MAC_fetch(ctx, c"HMAC", null) if (mac == null) { - throw new Error("EVP_MAC_fetch") + throw Error("EVP_MAC_fetch", ERR_get_error()) } else { val ctx = EVP_MAC_CTX_new(mac) try { @@ -100,11 +97,11 @@ private[bobcats] trait HmacCompanionPlatform { * See [[https://www.openssl.org/docs/man3.0/man7/EVP_RAND-CTR-DRBG.html]] */ override def generateKey[A <: HmacAlgorithm](algorithm: A): F[SecretKey[A]] = { - F.defer { + F.delay { // See NIST SP 800-90A val rand = EVP_RAND_fetch(null, c"CTR-DRBG", null) if (rand == null) { - F.raiseError[SecretKey[A]](new Error("EVP_RAND_fetch")) + throw Error("EVP_RAND_fetch", ERR_get_error()) } else { val ctx = EVP_RAND_CTX_new(rand, null) val params = stackalloc[OSSL_PARAM](2) @@ -125,9 +122,7 @@ private[bobcats] trait HmacCompanionPlatform { throw Error("EVP_RAND_generate", ERR_get_error()) } val key = ByteVector.fromPtr(out.asInstanceOf[Ptr[Byte]], len.toLong) - F.pure(SecretKeySpec(key, algorithm)) - } catch { - case e: Error => F.raiseError[SecretKey[A]](e) + SecretKeySpec(key, algorithm) } finally { EVP_RAND_CTX_free(ctx) } diff --git a/core/shared/src/main/scala/bobcats/Algorithm.scala b/core/shared/src/main/scala/bobcats/Algorithm.scala index c4a696a..b8159db 100644 --- a/core/shared/src/main/scala/bobcats/Algorithm.scala +++ b/core/shared/src/main/scala/bobcats/Algorithm.scala @@ -52,7 +52,15 @@ object HashAlgorithm { } sealed trait HmacAlgorithm extends Algorithm { + + import HmacAlgorithm._ + private[bobcats] def minimumKeyLength: Int + private[bobcats] def hashAlgorithm: HashAlgorithm = this match { + case SHA1 => HashAlgorithm.SHA1 + case SHA256 => HashAlgorithm.SHA256 + case SHA512 => HashAlgorithm.SHA512 + } } object HmacAlgorithm { diff --git a/core/shared/src/test/scala/bobcats/CryptoSuite.scala b/core/shared/src/test/scala/bobcats/CryptoSuite.scala index 5a7bc25..4e849a2 100644 --- a/core/shared/src/test/scala/bobcats/CryptoSuite.scala +++ b/core/shared/src/test/scala/bobcats/CryptoSuite.scala @@ -19,16 +19,17 @@ package bobcats import cats.effect.IO import munit.CatsEffectSuite -trait CryptoSuite extends CatsEffectSuite { +abstract class CryptoSuite extends CatsEffectSuite { private val cryptoFixture = ResourceSuiteLocalFixture( "crypto", - Crypto.forAsyncResource[IO] + Crypto.forAsync[IO] ) override def munitFixtures = List(cryptoFixture) - implicit def crypto: Crypto[IO] = cryptoFixture() - implicit def hash: Hash[IO] = crypto.hash + implicit protected def crypto: Crypto[IO] = cryptoFixture() + implicit protected def hash: Hash[IO] = crypto.hash + implicit protected def hmac: Hmac[IO] = crypto.hmac } diff --git a/core/shared/src/test/scala/bobcats/HashSuite.scala b/core/shared/src/test/scala/bobcats/HashSuite.scala index fffc460..855c57b 100644 --- a/core/shared/src/test/scala/bobcats/HashSuite.scala +++ b/core/shared/src/test/scala/bobcats/HashSuite.scala @@ -17,7 +17,7 @@ package bobcats import cats.effect.IO -import scodec.bits.ByteVector +import scodec.bits._ import fs2.{Chunk, Stream} class HashSuite extends CryptoSuite { @@ -30,47 +30,46 @@ class HashSuite extends CryptoSuite { algorithm: HashAlgorithm, name: String, data: ByteVector, - expect: String, + expect: ByteVector, ignoredRuntimes: Set[String]) = test(s"$algorithm for $name vector") { val runtime = BuildInfo.runtime assume(!ignoredRuntimes.contains(runtime), s"${runtime} does not support ${algorithm}") - val bytes = ByteVector.fromHex(expect).get - Hash[IO].digest(algorithm, data).assertEquals(bytes) *> + Hash[IO].digest(algorithm, data).assertEquals(expect) *> Stream .chunk(Chunk.byteVector(data)) .through(Hash[IO].digestPipe(algorithm)) .compile .to(ByteVector) - .assertEquals(bytes) *> Hash1 - .forSyncResource[IO](algorithm) + .assertEquals(expect) *> Hash1 + .forSync[IO](algorithm) .use(_.digest(data)) - .assertEquals(bytes) + .assertEquals(expect) } def testVector( algorithm: HashAlgorithm, - expect: String, + expect: ByteVector, ignoredRuntimes: Set[String] = Set()) = testHash(algorithm, "example", data, expect, ignoredRuntimes) def testEmpty( algorithm: HashAlgorithm, - expect: String, + expect: ByteVector, ignoredRuntimes: Set[String] = Set()) = testHash(algorithm, "empty", ByteVector.empty, expect, ignoredRuntimes) - testVector(SHA1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") - testEmpty(SHA1, "da39a3ee5e6b4b0d3255bfef95601890afd80709") - testVector(SHA256, "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592") - testEmpty(SHA256, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + testVector(SHA1, hex"2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") + testEmpty(SHA1, hex"da39a3ee5e6b4b0d3255bfef95601890afd80709") + testVector(SHA256, hex"d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592") + testEmpty(SHA256, hex"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") testVector( SHA512, - "07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6") + hex"07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6") testEmpty( SHA512, - "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") + hex"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") - testVector(MD5, "9e107d9d372bb6826bd81d3542a419d6", ignoredRuntimes = Set("Browser")) + testVector(MD5, hex"9e107d9d372bb6826bd81d3542a419d6", ignoredRuntimes = Set("Browser")) } diff --git a/core/shared/src/test/scala/bobcats/HmacSuite.scala b/core/shared/src/test/scala/bobcats/HmacSuite.scala index 2bbda94..4507ae3 100644 --- a/core/shared/src/test/scala/bobcats/HmacSuite.scala +++ b/core/shared/src/test/scala/bobcats/HmacSuite.scala @@ -16,15 +16,10 @@ package bobcats -import cats.Functor import cats.effect.IO -import cats.syntax.all._ -import munit.CatsEffectSuite import scodec.bits._ -import scala.reflect.ClassTag - -class HmacSuite extends CatsEffectSuite { +class HmacSuite extends CryptoSuite { import HmacAlgorithm._ @@ -79,48 +74,34 @@ class HmacSuite extends CatsEffectSuite { val key = ByteVector.encodeAscii("key").toOption.get val data = ByteVector.encodeAscii("The quick brown fox jumps over the lazy dog").toOption.get - def testHmac[F[_]: Hmac: Functor](algorithm: HmacAlgorithm, expected: ByteVector)( - implicit ct: ClassTag[F[Nothing]]) = - test(s"$algorithm with ${ct.runtimeClass.getSimpleName()}") { - Hmac[F].digest(SecretKeySpec(key, algorithm), data).map { obtained => - assertEquals( - obtained, - expected - ) - } + def testHmac(algorithm: HmacAlgorithm, expected: ByteVector) = + test(s"$algorithm") { + Hmac[IO].digest(SecretKeySpec(key, algorithm), data).assertEquals(expected) } - def testHmacSha1[F[_]: Hmac: Functor](testCases: List[TestCase])( - implicit ct: ClassTag[F[Nothing]]) = + def testHmacSha1(testCases: List[TestCase]) = testCases.zipWithIndex.foreach { case (TestCase(key, data, expected), idx) => - test(s"SHA1 RFC2022 test case ${idx + 1} with ${ct.runtimeClass.getSimpleName()}") { - Hmac[F].digest(SecretKeySpec(key, SHA1), data).map { obtained => - assertEquals(obtained, expected) - } + test(s"SHA1 RFC2022 test case ${idx + 1}") { + Hmac[IO].digest(SecretKeySpec(key, SHA1), data).assertEquals(expected) } } - def tests[F[_]: Hmac: Functor](implicit ct: ClassTag[F[Nothing]]) = { - testHmacSha1[F](sha1TestCases) - testHmac[F](SHA1, hex"de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9") - testHmac[F](SHA256, hex"f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8") - testHmac[F]( - SHA512, - hex"b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a") - } + testHmacSha1(sha1TestCases) + testHmac(SHA1, hex"de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9") + testHmac(SHA256, hex"f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8") + testHmac( + SHA512, + hex"b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a") - def testGenerateKey[F[_]: Functor: Hmac](algorithm: HmacAlgorithm)( - implicit ct: ClassTag[F[Nothing]]) = - test(s"generate key for ${algorithm} with ${ct.runtimeClass.getSimpleName()}") { - Hmac[F].generateKey(algorithm).map { + def testGenerateKey(algorithm: HmacAlgorithm) = + test(s"generate key for ${algorithm}") { + Hmac[IO].generateKey(algorithm).map { case SecretKeySpec(key, keyAlgorithm) => assertEquals(algorithm, keyAlgorithm) assert(key.size >= algorithm.minimumKeyLength) } } - tests[IO] - - List(SHA1, SHA256, SHA512).foreach(testGenerateKey[IO]) + List(SHA1, SHA256, SHA512).foreach(testGenerateKey) } diff --git a/core/shared/src/test/scala/bobcats/HotpSuite.scala b/core/shared/src/test/scala/bobcats/HotpSuite.scala index c424c2f..730a58b 100644 --- a/core/shared/src/test/scala/bobcats/HotpSuite.scala +++ b/core/shared/src/test/scala/bobcats/HotpSuite.scala @@ -16,15 +16,10 @@ package bobcats -import cats.Functor import cats.effect.IO -import cats.syntax.functor._ -import munit.CatsEffectSuite import scodec.bits._ -import scala.reflect.ClassTag - -class HotpSuite extends CatsEffectSuite { +class HotpSuite extends CryptoSuite { val key = hex"3132333435363738393031323334353637383930" @@ -32,16 +27,12 @@ class HotpSuite extends CatsEffectSuite { 755224, 287082, 359152, 969429, 338314, 254676, 287922, 162583, 399871, 520489 ) - def tests[F[_]: Hmac: Functor](implicit ct: ClassTag[F[Nothing]]) = { - expectedValues.zipWithIndex.foreach { - case (expected, counter) => - test(s"RFC4226 test case ${counter} for ${ct.runtimeClass.getSimpleName()}") { - Hotp - .generate[F](SecretKeySpec(key, HmacAlgorithm.SHA1), counter.toLong, digits = 6) - .map { obtained => assertEquals(obtained, expected) } - } - } + expectedValues.zipWithIndex.foreach { + case (expected, counter) => + test(s"RFC4226 test case ${counter}") { + Hotp + .generate[IO](SecretKeySpec(key, HmacAlgorithm.SHA1), counter.toLong, digits = 6) + .map { obtained => assertEquals(obtained, expected) } + } } - - tests[IO] } From 0c38396eb9c88a855391f77c7231f1cf80b393e7 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Thu, 10 Aug 2023 17:40:20 +0100 Subject: [PATCH 12/17] Add JS --- .../main/scala/bobcats/CryptoPlatform.scala | 22 ++- .../main/scala/bobcats/Hash1Platform.scala | 75 ++++++++++ .../src/main/scala/bobcats/HashPlatform.scala | 62 +++----- .../src/main/scala/bobcats/HmacPlatform.scala | 139 +++++++++--------- .../scala/bobcats/SecurityException.scala | 3 + .../scala/bobcats/facade/node/crypto.scala | 2 + .../main/scala/bobcats/Hash1Platform.scala | 9 +- .../src/main/scala/bobcats/Context.scala | 2 - .../main/scala/bobcats/Hash1Platform.scala | 14 +- .../src/test/scala/bobcats/HashSuite.scala | 2 +- 10 files changed, 203 insertions(+), 127 deletions(-) create mode 100644 core/js/src/main/scala/bobcats/Hash1Platform.scala diff --git a/core/js/src/main/scala/bobcats/CryptoPlatform.scala b/core/js/src/main/scala/bobcats/CryptoPlatform.scala index eb9634a..a834937 100644 --- a/core/js/src/main/scala/bobcats/CryptoPlatform.scala +++ b/core/js/src/main/scala/bobcats/CryptoPlatform.scala @@ -16,12 +16,22 @@ package bobcats -import cats.effect.kernel.Async +import cats.effect.kernel.{Async, Resource} private[bobcats] trait CryptoCompanionPlatform { - def forAsync[F[_]](implicit F: Async[F]): Crypto[F] = - new UnsealedCrypto[F] { - override def hash: Hash[F] = Hash[F] - override def hmac: Hmac[F] = Hmac[F] - } + def forAsync[F[_]](implicit F: Async[F]): Resource[F, Crypto[F]] = { + Resource.pure( + if (facade.isNodeJSRuntime) { + new UnsealedCrypto[F] { + override def hash: Hash[F] = Hash.forSyncNodeJS + override def hmac: Hmac[F] = Hmac.forAsyncNodeJS + } + } else { + new UnsealedCrypto[F] { + override def hash: Hash[F] = Hash.forAsyncSubtleCrypto + override def hmac: Hmac[F] = Hmac.forAsyncSubtleCrypto + } + } + ) + } } diff --git a/core/js/src/main/scala/bobcats/Hash1Platform.scala b/core/js/src/main/scala/bobcats/Hash1Platform.scala new file mode 100644 index 0000000..acdcb41 --- /dev/null +++ b/core/js/src/main/scala/bobcats/Hash1Platform.scala @@ -0,0 +1,75 @@ +/* + * Copyright 2021 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 bobcats + +import cats.syntax.all._ +import cats.effect.{Async, Resource, Sync} +import scodec.bits.ByteVector +import fs2.{Chunk, Pipe, Stream} + +private[bobcats] final class SubtleCryptoDigest[F[_]](algorithm: String)(implicit F: Async[F]) + extends UnsealedHash1[F] { + + import facade.browser._ + + override def digest(data: ByteVector): F[ByteVector] = + F.fromPromise(F.delay(crypto.subtle.digest(algorithm, data.toUint8Array.buffer))) + .map(ByteVector.view) + + override def pipe: Pipe[F, Byte, Byte] = throw new UnsupportedOperationException( + "Browsers do not support streaming") + +} + +private final class NodeCryptoDigest[F[_]](algorithm: String)(implicit F: Sync[F]) + extends UnsealedHash1[F] { + + override def digest(data: ByteVector): F[ByteVector] = + F.pure { + // Assume we've checked the algorithm already + val hash = facade.node.crypto.createHash(algorithm) + hash.update(data.toUint8Array) + ByteVector.view(hash.digest()) + } + + override val pipe: Pipe[F, Byte, Byte] = + in => + Stream.eval(F.delay(facade.node.crypto.createHash(algorithm))).flatMap { hash => + in.chunks + .fold(hash) { (h, d) => + h.update(d.toUint8Array) + h + } + .flatMap(h => Stream.chunk(Chunk.uint8Array(h.digest()))) + } +} + +private[bobcats] trait Hash1CompanionPlatform { + def fromJSCryptoName[F[_]](alg: String)(implicit F: Sync[F]): F[Hash1[F]] = + if (facade.node.crypto.getHashes().contains(alg)) { + F.pure(new NodeCryptoDigest(alg)(F)) + } else { + F.raiseError(new NoSuchAlgorithmException(s"${alg} MessageDigest not available")) + } + + def forAsync[F[_]: Async](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = + if (facade.isNodeJSRuntime) Resource.eval(fromJSCryptoName(algorithm.toStringNodeJS)) + else { + // SubtleCrypto does not have a way of checking the supported hashes + Resource.pure(new SubtleCryptoDigest(algorithm.toStringWebCrypto)) + } +} diff --git a/core/js/src/main/scala/bobcats/HashPlatform.scala b/core/js/src/main/scala/bobcats/HashPlatform.scala index 35aaeff..d0b15a9 100644 --- a/core/js/src/main/scala/bobcats/HashPlatform.scala +++ b/core/js/src/main/scala/bobcats/HashPlatform.scala @@ -19,48 +19,30 @@ package bobcats import cats.effect.kernel.{Async, Resource, Sync} import cats.syntax.all._ import scodec.bits.ByteVector - -private final class NodeCryptoDigest[F[_]](var hash: facade.node.Hash, algorithm: String)( - implicit F: Sync[F]) - extends UnsealedDigest[F] { - override def update(data: ByteVector): F[Unit] = F.delay(hash.update(data.toUint8Array)) - override val reset = F.delay { - hash = facade.node.crypto.createHash(algorithm) - } - override def get: F[ByteVector] = F.delay(ByteVector.view(hash.digest())) -} +import fs2.{Pipe, Stream} private[bobcats] trait HashCompanionPlatform { - implicit def forAsync[F[_]](implicit F: Async[F]): Hash[F] = - if (facade.isNodeJSRuntime) - new UnsealedHash[F] { - - override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = - Resource.make(F.delay { - val alg = algorithm.toStringNodeJS - val hash = facade.node.crypto.createHash(alg) - new NodeCryptoDigest(hash, alg) - })(_ => F.unit) - override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = - F.delay { - val hash = facade.node.crypto.createHash(algorithm.toStringNodeJS) - hash.update(data.toUint8Array) - ByteVector.view(hash.digest()) + private[bobcats] def forSyncNodeJS[F[_]: Sync]: Hash[F] = + new UnsealedHash[F] { + override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = + in => + Stream.eval(Hash1.fromJSCryptoName(algorithm.toStringNodeJS)).flatMap { hash => + in.through(hash.pipe) } - } - else - new UnsealedHash[F] { - import facade.browser._ - override def incremental(algorithm: HashAlgorithm): Resource[F, Digest[F]] = { - val err = F.raiseError[Digest[F]]( - new UnsupportedOperationException("WebCrypto does not support incremental hashing")) - Resource.make(err)(_ => err.void) - } - override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = - F.fromPromise( - F.delay( - crypto.subtle.digest(algorithm.toStringWebCrypto, data.toUint8Array.buffer))) - .map(ByteVector.view) - } + override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = + Hash1.fromJSCryptoName(algorithm.toStringNodeJS).flatMap(_.digest(data)) + } + + private[bobcats] def forAsyncSubtleCrypto[F[_]: Async]: Hash[F] = + new UnsealedHash[F] { + override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = + new SubtleCryptoDigest(algorithm.toStringWebCrypto).pipe + + override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = + new SubtleCryptoDigest(algorithm.toStringWebCrypto).digest(data) + } + + def forAsync[F[_]: Async]: Resource[F, Hash[F]] = + Resource.pure(if (facade.isNodeJSRuntime) forSyncNodeJS else forAsyncSubtleCrypto) } diff --git a/core/js/src/main/scala/bobcats/HmacPlatform.scala b/core/js/src/main/scala/bobcats/HmacPlatform.scala index 93c0f7b..eafcdc0 100644 --- a/core/js/src/main/scala/bobcats/HmacPlatform.scala +++ b/core/js/src/main/scala/bobcats/HmacPlatform.scala @@ -26,79 +26,78 @@ import java.lang private[bobcats] trait HmacPlatform[F[_]] private[bobcats] trait HmacCompanionPlatform { - implicit def forAsync[F[_]](implicit F: Async[F]): Hmac[F] = - if (facade.isNodeJSRuntime) - new UnsealedHmac[F] { - import facade.node._ - override def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] = - key match { - case SecretKeySpec(key, algorithm) => - F.catchNonFatal { - val hmac = crypto.createHmac(algorithm.toStringNodeJS, key.toUint8Array) - hmac.update(data.toUint8Array) - ByteVector.view(hmac.digest()) - } - case _ => F.raiseError(new InvalidKeyException) - } + private[bobcats] def forAsyncNodeJS[F[_]](implicit F: Async[F]): Hmac[F] = + new UnsealedHmac[F] { + import facade.node._ + override def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] = + key match { + case SecretKeySpec(key, algorithm) => + F.catchNonFatal { + val hmac = crypto.createHmac(algorithm.toStringNodeJS, key.toUint8Array) + hmac.update(data.toUint8Array) + ByteVector.view(hmac.digest()) + } + case _ => F.raiseError(new InvalidKeyException) + } - override def generateKey[A <: HmacAlgorithm](algorithm: A): F[SecretKey[A]] = - F.async_[SecretKey[A]] { cb => - crypto.generateKey( - "hmac", - GenerateKeyOptions(algorithm.minimumKeyLength * lang.Byte.SIZE), - (err, key) => - cb( - Option(err) - .map(js.JavaScriptException) - .toLeft(SecretKeySpec(ByteVector.view(key.`export`()), algorithm))) - ) - } + override def generateKey[A <: HmacAlgorithm](algorithm: A): F[SecretKey[A]] = + F.async_[SecretKey[A]] { cb => + crypto.generateKey( + "hmac", + GenerateKeyOptions(algorithm.minimumKeyLength * lang.Byte.SIZE), + (err, key) => + cb( + Option(err) + .map(js.JavaScriptException) + .toLeft(SecretKeySpec(ByteVector.view(key.`export`()), algorithm))) + ) + } - override def importKey[A <: HmacAlgorithm]( - key: ByteVector, - algorithm: A): F[SecretKey[A]] = - F.pure(SecretKeySpec(key, algorithm)) + override def importKey[A <: HmacAlgorithm]( + key: ByteVector, + algorithm: A): F[SecretKey[A]] = + F.pure(SecretKeySpec(key, algorithm)) + } - } - else - new UnsealedHmac[F] { - import bobcats.facade.browser._ - override def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] = - key match { - case SecretKeySpec(key, algorithm) => - for { - key <- F.fromPromise( - F.delay( - crypto - .subtle - .importKey( - "raw", - key.toUint8Array, - HmacImportParams(algorithm.toStringWebCrypto), - false, - js.Array("sign")))) - signature <- F.fromPromise( - F.delay(crypto.subtle.sign("HMAC", key, data.toUint8Array.buffer))) - } yield ByteVector.view(signature) - case _ => F.raiseError(new InvalidKeyException) - } + private[bobcats] def forAsyncSubtleCrypto[F[_]](implicit F: Async[F]): Hmac[F] = + new UnsealedHmac[F] { + import bobcats.facade.browser._ + override def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] = + key match { + case SecretKeySpec(key, algorithm) => + for { + key <- F.fromPromise( + F.delay( + crypto + .subtle + .importKey( + "raw", + key.toUint8Array, + HmacImportParams(algorithm.toStringWebCrypto), + false, + js.Array("sign")))) + signature <- F.fromPromise( + F.delay(crypto.subtle.sign("HMAC", key, data.toUint8Array.buffer))) + } yield ByteVector.view(signature) + case _ => F.raiseError(new InvalidKeyException) + } - override def generateKey[A <: HmacAlgorithm](algorithm: A): F[SecretKey[A]] = - for { - key <- F.fromPromise( - F.delay( - crypto - .subtle - .generateKey( - HmacKeyGenParams(algorithm.toStringWebCrypto), - true, - js.Array("sign")))) - exported <- F.fromPromise(F.delay(crypto.subtle.exportKey("raw", key))) - } yield SecretKeySpec(ByteVector.view(exported), algorithm) + override def generateKey[A <: HmacAlgorithm](algorithm: A): F[SecretKey[A]] = + for { + key <- F.fromPromise( + F.delay( + crypto + .subtle + .generateKey( + HmacKeyGenParams(algorithm.toStringWebCrypto), + true, + js.Array("sign")))) + exported <- F.fromPromise(F.delay(crypto.subtle.exportKey("raw", key))) + } yield SecretKeySpec(ByteVector.view(exported), algorithm) - override def importKey[A <: HmacAlgorithm]( - key: ByteVector, - algorithm: A): F[SecretKey[A]] = - F.pure(SecretKeySpec(key, algorithm)) - } + override def importKey[A <: HmacAlgorithm]( + key: ByteVector, + algorithm: A): F[SecretKey[A]] = + F.pure(SecretKeySpec(key, algorithm)) + } } diff --git a/core/js/src/main/scala/bobcats/SecurityException.scala b/core/js/src/main/scala/bobcats/SecurityException.scala index 0f7ba6d..578438c 100644 --- a/core/js/src/main/scala/bobcats/SecurityException.scala +++ b/core/js/src/main/scala/bobcats/SecurityException.scala @@ -19,6 +19,9 @@ package bobcats class GeneralSecurityException(message: String = null, cause: Throwable = null) extends Exception(message, cause) +class NoSuchAlgorithmException(message: String = null, cause: Throwable = null) + extends GeneralSecurityException(message, cause) + class KeyException(message: String = null, cause: Throwable = null) extends GeneralSecurityException(message, cause) diff --git a/core/js/src/main/scala/bobcats/facade/node/crypto.scala b/core/js/src/main/scala/bobcats/facade/node/crypto.scala index 4bcbeb6..8d53fed 100644 --- a/core/js/src/main/scala/bobcats/facade/node/crypto.scala +++ b/core/js/src/main/scala/bobcats/facade/node/crypto.scala @@ -24,6 +24,8 @@ import scala.scalajs.js @nowarn("msg=never used") private[bobcats] trait crypto extends js.Any { + def getHashes(): js.Array[String] = js.native + def createHash(algorithm: String): Hash = js.native def createHmac(algorithm: String, key: js.typedarray.Uint8Array): Hmac = js.native diff --git a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala index 83ea5c2..77cfa3d 100644 --- a/core/jvm/src/main/scala/bobcats/Hash1Platform.scala +++ b/core/jvm/src/main/scala/bobcats/Hash1Platform.scala @@ -18,7 +18,7 @@ package bobcats import java.security.{MessageDigest, Provider} import scodec.bits.ByteVector -import cats.effect.{Resource, Sync} +import cats.effect.{Async, Resource, Sync} import fs2.{Chunk, Pipe, Stream} private final class JavaSecurityDigest[F[_]](algorithm: String, provider: Provider)( @@ -51,7 +51,7 @@ private[bobcats] trait Hash1CompanionPlatform { /** * Get a hash for a specific name used by the Java security providers. */ - def fromJavaName[F[_]](name: String)(implicit F: Sync[F]): F[Hash1[F]] = + def fromJavaSecurityName[F[_]](name: String)(implicit F: Sync[F]): F[Hash1[F]] = F.delay { // `Security#getProviders` is a mutable array, so cache the `Provider` val p = Providers.get().messageDigest(name) match { @@ -62,6 +62,9 @@ private[bobcats] trait Hash1CompanionPlatform { } def forSync[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = - Resource.eval(fromJavaName[F](algorithm.toStringJava)) + Resource.eval(fromJavaSecurityName[F](algorithm.toStringJava)) + + def forAsync[F[_]: Async](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = forSync( + algorithm) } diff --git a/core/native/src/main/scala/bobcats/Context.scala b/core/native/src/main/scala/bobcats/Context.scala index 0bbab1e..0e34907 100644 --- a/core/native/src/main/scala/bobcats/Context.scala +++ b/core/native/src/main/scala/bobcats/Context.scala @@ -26,5 +26,3 @@ private[bobcats] object Context { def apply[F[_]](implicit F: Sync[F]): Resource[F, Ptr[OSSL_LIB_CTX]] = Resource.make(F.delay(OSSL_LIB_CTX_new()))(ctx => F.delay(OSSL_LIB_CTX_free(ctx))) } - - diff --git a/core/native/src/main/scala/bobcats/Hash1Platform.scala b/core/native/src/main/scala/bobcats/Hash1Platform.scala index f0b7579..4425fdf 100644 --- a/core/native/src/main/scala/bobcats/Hash1Platform.scala +++ b/core/native/src/main/scala/bobcats/Hash1Platform.scala @@ -16,7 +16,8 @@ package bobcats -import cats.effect.kernel.{Resource, Sync} +import scala.scalanative.annotation.alwaysinline +import cats.effect.kernel.{Async, Resource, Sync} import scodec.bits.ByteVector import scalanative.unsafe._ import scalanative.unsigned._ @@ -33,7 +34,7 @@ private[bobcats] final class NativeEvpDigest[F[_]](digest: Ptr[EVP_MD])(implicit try { init(ctx, digest) update(ctx, d) - F.pure(`final`(ctx)) + F.pure(`final`(ctx, (ptr, len) => ByteVector.fromPtr(ptr, len.toLong))) } catch { case e: Error => F.raiseError(e) } finally { @@ -53,13 +54,13 @@ private[bobcats] final class NativeEvpDigest[F[_]](digest: Ptr[EVP_MD])(implicit throw Error("EVP_DigestInit_ex", ERR_get_error()) } - private def `final`(ctx: Ptr[EVP_MD_CTX]): ByteVector = { + @alwaysinline private def `final`[A](ctx: Ptr[EVP_MD_CTX], f: (Ptr[Byte], Int) => A): A = { val md = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE) val s = stackalloc[CInt]() if (EVP_DigestFinal_ex(ctx, md, s) != 1) { throw Error("EVP_DigestFinal_ex", ERR_get_error()) } - ByteVector.fromPtr(md.asInstanceOf[Ptr[Byte]], s(0).toLong) + f(md.asInstanceOf[Ptr[Byte]], s(0)) } private val context: Stream[F, Ptr[EVP_MD_CTX]] = @@ -74,7 +75,7 @@ private[bobcats] final class NativeEvpDigest[F[_]](digest: Ptr[EVP_MD])(implicit // Most of the calls throw, so wrap in a `delay` in.chunks .evalMap { chunk => F.delay(update(ctx, chunk.toByteVector.toArrayUnsafe)) } - .drain ++ Stream.eval(F.delay(Chunk.byteVector(`final`(ctx)))) + .drain ++ Stream.eval(F.delay(`final`(ctx, Chunk.fromBytePtr))) }.unchunks } } @@ -114,4 +115,7 @@ private[bobcats] trait Hash1CompanionPlatform { def forSync[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = fromCryptoName(evpAlgorithm(algorithm)) + + def forAsync[F[_]: Async](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = forSync( + algorithm) } diff --git a/core/shared/src/test/scala/bobcats/HashSuite.scala b/core/shared/src/test/scala/bobcats/HashSuite.scala index 855c57b..4a4d27f 100644 --- a/core/shared/src/test/scala/bobcats/HashSuite.scala +++ b/core/shared/src/test/scala/bobcats/HashSuite.scala @@ -42,7 +42,7 @@ class HashSuite extends CryptoSuite { .compile .to(ByteVector) .assertEquals(expect) *> Hash1 - .forSync[IO](algorithm) + .forAsync[IO](algorithm) .use(_.digest(data)) .assertEquals(expect) } From 2fe052915891444e3c0d47c216782a9969cfe2f5 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Thu, 10 Aug 2023 18:09:48 +0100 Subject: [PATCH 13/17] Ugly hack to skip browser tests --- .../src/test/scala/bobcats/HashSuite.scala | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/core/shared/src/test/scala/bobcats/HashSuite.scala b/core/shared/src/test/scala/bobcats/HashSuite.scala index 4a4d27f..4637a10 100644 --- a/core/shared/src/test/scala/bobcats/HashSuite.scala +++ b/core/shared/src/test/scala/bobcats/HashSuite.scala @@ -35,16 +35,17 @@ class HashSuite extends CryptoSuite { test(s"$algorithm for $name vector") { val runtime = BuildInfo.runtime assume(!ignoredRuntimes.contains(runtime), s"${runtime} does not support ${algorithm}") - Hash[IO].digest(algorithm, data).assertEquals(expect) *> - Stream - .chunk(Chunk.byteVector(data)) - .through(Hash[IO].digestPipe(algorithm)) - .compile - .to(ByteVector) - .assertEquals(expect) *> Hash1 - .forAsync[IO](algorithm) - .use(_.digest(data)) - .assertEquals(expect) + val streamTest = Stream + .chunk(Chunk.byteVector(data)) + .through(Hash[IO].digestPipe(algorithm)) + .compile + .to(ByteVector) + .assertEquals(expect) + Hash[IO].digest(algorithm, data).assertEquals(expect) *> Hash1 + .forAsync[IO](algorithm) + .use(_.digest(data)) + .assertEquals(expect) *> (if (Set("Firefox", "Chrome").contains(runtime)) IO.unit + else streamTest) } def testVector( From a0f1eded4ba7ee8f480733c84580c7fba1fce4c7 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Thu, 10 Aug 2023 18:14:52 +0100 Subject: [PATCH 14/17] log the info --- core/shared/src/test/scala/bobcats/HashSuite.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/shared/src/test/scala/bobcats/HashSuite.scala b/core/shared/src/test/scala/bobcats/HashSuite.scala index 4637a10..707ca75 100644 --- a/core/shared/src/test/scala/bobcats/HashSuite.scala +++ b/core/shared/src/test/scala/bobcats/HashSuite.scala @@ -31,9 +31,9 @@ class HashSuite extends CryptoSuite { name: String, data: ByteVector, expect: ByteVector, - ignoredRuntimes: Set[String]) = - test(s"$algorithm for $name vector") { - val runtime = BuildInfo.runtime + ignoredRuntimes: Set[String]) = { + val runtime = BuildInfo.runtime + test(s"$algorithm for $name vector on ${runtime}") { assume(!ignoredRuntimes.contains(runtime), s"${runtime} does not support ${algorithm}") val streamTest = Stream .chunk(Chunk.byteVector(data)) @@ -47,6 +47,7 @@ class HashSuite extends CryptoSuite { .assertEquals(expect) *> (if (Set("Firefox", "Chrome").contains(runtime)) IO.unit else streamTest) } + } def testVector( algorithm: HashAlgorithm, From 5796fba537838077572c440f65fc857fe0e243f3 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Thu, 10 Aug 2023 18:31:11 +0100 Subject: [PATCH 15/17] Ignore for `MD5`. --- core/shared/src/test/scala/bobcats/HashSuite.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/test/scala/bobcats/HashSuite.scala b/core/shared/src/test/scala/bobcats/HashSuite.scala index 707ca75..d522afb 100644 --- a/core/shared/src/test/scala/bobcats/HashSuite.scala +++ b/core/shared/src/test/scala/bobcats/HashSuite.scala @@ -72,6 +72,6 @@ class HashSuite extends CryptoSuite { SHA512, hex"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") - testVector(MD5, hex"9e107d9d372bb6826bd81d3542a419d6", ignoredRuntimes = Set("Browser")) + testVector(MD5, hex"9e107d9d372bb6826bd81d3542a419d6", ignoredRuntimes = Set("Firefox", "Chrome")) } From 6db3a562af528d3738ab17adde7f5d38e89642f6 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Thu, 10 Aug 2023 18:32:32 +0100 Subject: [PATCH 16/17] Stop being such an idiot. --- .../src/test/scala/bobcats/HashSuite.scala | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/core/shared/src/test/scala/bobcats/HashSuite.scala b/core/shared/src/test/scala/bobcats/HashSuite.scala index d522afb..2a58913 100644 --- a/core/shared/src/test/scala/bobcats/HashSuite.scala +++ b/core/shared/src/test/scala/bobcats/HashSuite.scala @@ -35,17 +35,19 @@ class HashSuite extends CryptoSuite { val runtime = BuildInfo.runtime test(s"$algorithm for $name vector on ${runtime}") { assume(!ignoredRuntimes.contains(runtime), s"${runtime} does not support ${algorithm}") - val streamTest = Stream - .chunk(Chunk.byteVector(data)) - .through(Hash[IO].digestPipe(algorithm)) - .compile - .to(ByteVector) - .assertEquals(expect) Hash[IO].digest(algorithm, data).assertEquals(expect) *> Hash1 .forAsync[IO](algorithm) .use(_.digest(data)) .assertEquals(expect) *> (if (Set("Firefox", "Chrome").contains(runtime)) IO.unit - else streamTest) + else { + Stream + .chunk(Chunk.byteVector(data)) + .through(Hash[IO].digestPipe(algorithm)) + .compile + .to(ByteVector) + .assertEquals(expect) + + }) } } @@ -72,6 +74,9 @@ class HashSuite extends CryptoSuite { SHA512, hex"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") - testVector(MD5, hex"9e107d9d372bb6826bd81d3542a419d6", ignoredRuntimes = Set("Firefox", "Chrome")) + testVector( + MD5, + hex"9e107d9d372bb6826bd81d3542a419d6", + ignoredRuntimes = Set("Firefox", "Chrome")) } From bf68912dc4819b8a9aba94b81a49d3913669fa58 Mon Sep 17 00:00:00 2001 From: Yilin Wei Date: Thu, 10 Aug 2023 22:11:38 +0100 Subject: [PATCH 17/17] get rid of legacy functions for evp --- .../src/main/scala/bobcats/openssl/evp.scala | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/core/native/src/main/scala/bobcats/openssl/evp.scala b/core/native/src/main/scala/bobcats/openssl/evp.scala index 7a981d6..1ee20ef 100644 --- a/core/native/src/main/scala/bobcats/openssl/evp.scala +++ b/core/native/src/main/scala/bobcats/openssl/evp.scala @@ -69,26 +69,6 @@ private[bobcats] object evp { */ def EVP_MD_get0_name(md: Ptr[EVP_MD]): CString = extern - /** - * See [[https://www.openssl.org/docs/man3.1/man3/EVP_md5.html]] - */ - def EVP_md5(): Ptr[EVP_MD] = extern - - /** - * See [[https://www.openssl.org/docs/man3.1/man3/EVP_sha1.html]] - */ - def EVP_sha1(): Ptr[EVP_MD] = extern - - /** - * See [[https://www.openssl.org/docs/man3.1/man3/EVP_sha256.html]] - */ - def EVP_sha256(): Ptr[EVP_MD] = extern - - /** - * See [[https://www.openssl.org/docs/man3.1/man3/EVP_sha512.html]] - */ - def EVP_sha512(): Ptr[EVP_MD] = extern - /** * See [[https://www.openssl.org/docs/man3.1/man3/EVP_DigestInit_ex.html]] */