Skip to content

Commit

Permalink
Write fs2.hash in terms of fs2.hashing (except JVM due to exposed dep…
Browse files Browse the repository at this point in the history
…endency on MessageDigest)
  • Loading branch information
mpilquist committed Jul 7, 2024
1 parent 6480b7e commit 8993665
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 126 deletions.
54 changes: 16 additions & 38 deletions core/js/src/main/scala/fs2/hash.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,62 +21,40 @@

package fs2

import org.typelevel.scalaccompat.annotation._

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import scala.scalajs.js.typedarray.Uint8Array
import cats.effect.SyncIO
import fs2.hashing.{Hash, HashAlgorithm}

/** Provides various cryptographic hashes as pipes. Supported only on Node.js. */
@deprecated("Use fs2.hashing.Hashing[F] instead", "3.11.0")
object hash {

/** Computes an MD2 digest. */
def md2[F[_]]: Pipe[F, Byte, Byte] = digest(createHash("md2"))
def md2[F[_]]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.Named("MD2"))

/** Computes an MD5 digest. */
def md5[F[_]]: Pipe[F, Byte, Byte] = digest(createHash("md5"))
def md5[F[_]]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.MD5)

/** Computes a SHA-1 digest. */
def sha1[F[_]]: Pipe[F, Byte, Byte] =
digest(createHash("sha1"))
def sha1[F[_]]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.SHA1)

/** Computes a SHA-256 digest. */
def sha256[F[_]]: Pipe[F, Byte, Byte] =
digest(createHash("sha256"))
def sha256[F[_]]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.SHA256)

/** Computes a SHA-384 digest. */
def sha384[F[_]]: Pipe[F, Byte, Byte] =
digest(createHash("sha384"))
def sha384[F[_]]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.SHA384)

/** Computes a SHA-512 digest. */
def sha512[F[_]]: Pipe[F, Byte, Byte] =
digest(createHash("sha512"))
def sha512[F[_]]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.SHA512)

/** Computes the digest of the source stream, emitting the digest as a chunk
* after completion of the source stream.
*/
private[this] def digest[F[_]](hash: => Hash): Pipe[F, Byte, Byte] =
in =>
private[this] def digest[F[_]](algorithm: HashAlgorithm): Pipe[F, Byte, Byte] =
source =>
Stream.suspend {
in.chunks
.fold(hash) { (d, c) =>
val bytes = c.toUint8Array
d.update(bytes)
d
val h = Hash.unsafe[SyncIO](algorithm)
source.chunks
.fold(h) { (h, c) =>
h.addChunk(c).unsafeRunSync()
h
}
.flatMap(d => Stream.chunk(Chunk.uint8Array(d.digest())))
.flatMap(h => Stream.chunk(h.computeAndReset.unsafeRunSync()))
}

@js.native
@JSImport("crypto", "createHash")
@nowarn212("cat=unused")
private[fs2] def createHash(algorithm: String): Hash = js.native

@js.native
@nowarn212("cat=unused")
private[fs2] trait Hash extends js.Object {
def update(data: Uint8Array): Unit = js.native
def digest(): Uint8Array = js.native
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ import scala.scalajs.js.typedarray.Uint8Array

trait HashCompanionPlatform {

private[hashing] def apply[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash[F]] =
private[fs2] def apply[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash[F]] =
Resource.eval(Sync[F].delay(unsafe(algorithm)))

private[hashing] def unsafe[F[_]: Sync](algorithm: HashAlgorithm): Hash[F] =
private[fs2] def unsafe[F[_]: Sync](algorithm: HashAlgorithm): Hash[F] =
new Hash[F] {
private def newHash() = JsHash.createHash(toAlgorithmString(algorithm))
private var h = newHash()
Expand Down
98 changes: 12 additions & 86 deletions core/native/src/main/scala/fs2/hash.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,108 +22,34 @@
package fs2

import cats.effect.kernel.Sync
import org.typelevel.scalaccompat.annotation._

import scala.scalanative.unsafe._
import scala.scalanative.unsigned._
import fs2.hashing.{Hashing, HashAlgorithm}

/** Provides various cryptographic hashes as pipes. Requires OpenSSL. */
@deprecated("Use fs2.hashing.Hashing[F] instead", "3.11.0")
object hash {
import openssl._

/** Computes an MD2 digest. */
def md2[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(c"MD2")
def md2[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.Named("MD2"))

/** Computes an MD5 digest. */
def md5[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(c"MD5")
def md5[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.MD5)

/** Computes a SHA-1 digest. */
def sha1[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(c"SHA1")
def sha1[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.SHA1)

/** Computes a SHA-256 digest. */
def sha256[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(c"SHA256")
def sha256[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.SHA256)

/** Computes a SHA-384 digest. */
def sha384[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(c"SHA384")
def sha384[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.SHA384)

/** Computes a SHA-512 digest. */
def sha512[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(c"SHA512")

/** Computes the digest of the source stream, emitting the digest as a chunk
* after completion of the source stream.
*/
private[this] def digest[F[_]](digest: CString)(implicit F: Sync[F]): Pipe[F, Byte, Byte] =
in =>
Stream
.bracket(F.delay {
val ctx = EVP_MD_CTX_new()
if (ctx == null)
throw new RuntimeException(s"EVP_MD_CTX_new: ${getError()}")
ctx
})(ctx => F.delay(EVP_MD_CTX_free(ctx)))
.evalTap { ctx =>
F.delay {
val `type` = EVP_get_digestbyname(digest)
if (`type` == null)
throw new RuntimeException(s"EVP_get_digestbyname: ${getError()}")
if (EVP_DigestInit_ex(ctx, `type`, null) != 1)
throw new RuntimeException(s"EVP_DigestInit_ex: ${getError()}")
}
}
.flatMap { ctx =>
in.chunks
.foreach { chunk =>
F.delay {
val Chunk.ArraySlice(values, offset, size) = chunk.toArraySlice
if (EVP_DigestUpdate(ctx, values.atUnsafe(offset), size.toULong) != 1)
throw new RuntimeException(s"EVP_DigestUpdate: ${getError()}")
}
} ++ Stream
.evalUnChunk {
F.delay[Chunk[Byte]] {
val md = new Array[Byte](EVP_MAX_MD_SIZE)
val size = stackalloc[CUnsignedInt]()
if (EVP_DigestFinal_ex(ctx, md.atUnsafe(0), size) != 1)
throw new RuntimeException(s"EVP_DigestFinal_ex: ${getError()}")
Chunk.ArraySlice(md, 0, (!size).toInt)
}
}
}

private[this] def getError(): String =
fromCString(ERR_reason_error_string(ERR_get_error()))

@link("crypto")
@extern
@nowarn212("cat=unused")
private[fs2] object openssl {

final val EVP_MAX_MD_SIZE = 64

type EVP_MD
type EVP_MD_CTX
type ENGINE

def ERR_get_error(): ULong = extern
def ERR_reason_error_string(e: ULong): CString = extern

def EVP_get_digestbyname(name: Ptr[CChar]): Ptr[EVP_MD] = extern

def EVP_MD_CTX_new(): Ptr[EVP_MD_CTX] = extern
def EVP_MD_CTX_free(ctx: Ptr[EVP_MD_CTX]): Unit = extern
def sha512[F[_]: Sync]: Pipe[F, Byte, Byte] = digest(HashAlgorithm.SHA512)

def EVP_DigestInit_ex(ctx: Ptr[EVP_MD_CTX], `type`: Ptr[EVP_MD], impl: Ptr[ENGINE]): CInt =
extern
def EVP_DigestUpdate(ctx: Ptr[EVP_MD_CTX], d: Ptr[Byte], cnt: CSize): CInt = extern
def EVP_DigestFinal_ex(ctx: Ptr[EVP_MD_CTX], md: Ptr[Byte], s: Ptr[CUnsignedInt]): CInt = extern
def EVP_Digest(
data: Ptr[Byte],
count: CSize,
md: Ptr[Byte],
size: Ptr[CUnsignedInt],
`type`: Ptr[EVP_MD],
impl: Ptr[ENGINE]
): CInt = extern
private[this] def digest[F[_]](
algorithm: HashAlgorithm
)(implicit F: Sync[F]): Pipe[F, Byte, Byte] = {
val h = Hashing.forSync[F]
h.hashWith(h.create(algorithm))
}
}

0 comments on commit 8993665

Please sign in to comment.