Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Introduces incremental hash. #188

Merged
merged 17 commits into from
Aug 10, 2023
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion core/js/src/main/scala/bobcats/CryptoPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
27 changes: 25 additions & 2 deletions core/js/src/main/scala/bobcats/HashPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,33 @@

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.delay {
val alg = algorithm.toStringNodeJS
val hash = facade.node.crypto.createHash(alg)
yilinwei marked this conversation as resolved.
Show resolved Hide resolved
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())
Expand All @@ -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(
Expand Down
18 changes: 14 additions & 4 deletions core/jvm/src/main/scala/bobcats/CryptoPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,22 @@

package bobcats

import cats.effect.kernel.Async
import cats.effect.kernel.{Async, Resource, Sync}

private[bobcats] trait CryptoCompanionPlatform {
implicit def forAsync[F[_]: Async]: Crypto[F] =

def forSync[F[_]](implicit F: Sync[F]): F[Crypto[F]] = F.delay {
yilinwei marked this conversation as resolved.
Show resolved Hide resolved
val providers = Providers.get()
new UnsealedCrypto[F] {
override def hash: Hash[F] = Hash[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

def forSyncResource[F[_]: Sync]: Resource[F, Crypto[F]] = Resource.eval(forSync[F])

def forAsyncResource[F[_]: Async]: Resource[F, Crypto[F]] = Resource.eval(forSync[F])

}
68 changes: 68 additions & 0 deletions core/jvm/src/main/scala/bobcats/Hash1Platform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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, 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])
extends UnsealedHash1[F] {

override def digest(data: ByteVector): F[ByteVector] = F.pure {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think by convention this shouldn't throw if the MessageDigest is well behaved but we can err on the side of caution and just catchNonFatal.

val h = MessageDigest.getInstance(algorithm, provider)
h.update(data.toByteBuffer)
ByteVector.view(h.digest())
}

override val pipe: Pipe[F, Byte, Byte] =
in =>
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 = s"JavaSecurityDigest(${algorithm}, ${provider.getName})"
}

private[bobcats] trait Hash1CompanionPlatform {

/**
* 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 forSync[F[_]](algorithm: HashAlgorithm)(implicit F: Sync[F]): F[Hash1[F]] = fromName(
algorithm.toStringJava)

def forSyncResource[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] =
yilinwei marked this conversation as resolved.
Show resolved Hide resolved
Resource.eval(forSync[F](algorithm))

}
34 changes: 25 additions & 9 deletions core/jvm/src/main/scala/bobcats/HashPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,35 @@

package bobcats

import cats.effect.kernel.Async
import fs2.{Pipe, Stream}
import cats.effect.kernel.{Async, Sync}
import scodec.bits.ByteVector

import java.security.MessageDigest
import cats.ApplicativeThrow

private[bobcats] trait HashCompanionPlatform {
implicit def forAsync[F[_]](implicit F: Async[F]): Hash[F] =

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] =
F.catchNonFatal {
val hash = MessageDigest.getInstance(algorithm.toStringJava)
hash.update(data.toByteBuffer)
ByteVector.view(hash.digest())
override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = {
val name = algorithm.toStringJava
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
providers.messageDigest(name) match {
case Left(e) =>
_ => Stream.eval(F.raiseError(e))
case Right(p) => new JavaSecurityDigest(name, p).pipe
}
}
}
}

def forSync[F[_]](implicit F: Sync[F]): F[Hash[F]] = F.delay(forProviders(Providers.get())(F))
def forAsync[F[_]: Async]: F[Hash[F]] = forSync
}
34 changes: 34 additions & 0 deletions core/jvm/src/main/scala/bobcats/Providers.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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, Provider, Security}

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"))

}

private[bobcats] object Providers {
def get(): Providers = new Providers(Security.getProviders().clone())
}
17 changes: 12 additions & 5 deletions core/native/src/main/scala/bobcats/CryptoPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
111 changes: 111 additions & 0 deletions core/native/src/main/scala/bobcats/Hash1Platform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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.{Resource, Sync}
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 {
yilinwei marked this conversation as resolved.
Show resolved Hide resolved
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))
}
Loading