Skip to content

Commit

Permalink
Merge pull request #3465 from mpilquist/topic/digest
Browse files Browse the repository at this point in the history
Update Hashing to support HMACs, update names in fs2.hashing to be clearer, introduce Digest type
  • Loading branch information
mpilquist authored Aug 26, 2024
2 parents e2925a2 + 4ff8b50 commit 333b753
Show file tree
Hide file tree
Showing 16 changed files with 568 additions and 228 deletions.
8 changes: 4 additions & 4 deletions core/js/src/main/scala/fs2/hash.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
package fs2

import cats.effect.SyncIO
import fs2.hashing.{Hash, HashAlgorithm}
import fs2.hashing.{Hasher, HashAlgorithm}

/** Provides various cryptographic hashes as pipes. Supported only on Node.js. */
@deprecated("Use fs2.hashing.Hashing[F] instead", "3.11.0")
Expand All @@ -49,12 +49,12 @@ object hash {
private[this] def digest[F[_]](algorithm: HashAlgorithm): Pipe[F, Byte, Byte] =
source =>
Stream.suspend {
val h = Hash.unsafe[SyncIO](algorithm)
val h = Hasher.unsafe[SyncIO](algorithm)
source.chunks
.fold(h) { (h, c) =>
h.addChunk(c).unsafeRunSync()
h.update(c).unsafeRunSync()
h
}
.flatMap(h => Stream.chunk(h.computeAndReset.unsafeRunSync()))
.flatMap(h => Stream.chunk(h.hash.unsafeRunSync().bytes))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,40 +30,63 @@ import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import scala.scalajs.js.typedarray.Uint8Array

trait HashCompanionPlatform {
trait HasherCompanionPlatform {

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

private[fs2] def unsafe[F[_]: Sync](algorithm: HashAlgorithm): Hash[F] =
new Hash[F] {
private[hashing] def hmac[F[_]: Sync](
algorithm: HashAlgorithm,
key: Chunk[Byte]
): Resource[F, Hasher[F]] =
Resource.eval(Sync[F].delay(unsafeHmac(algorithm, key)))

private[fs2] def unsafe[F[_]: Sync](algorithm: HashAlgorithm): Hasher[F] =
new SyncHasher[F] {
private def newHash() = JsHash.createHash(toAlgorithmString(algorithm))
private var h = newHash()

def addChunk(bytes: Chunk[Byte]): F[Unit] =
Sync[F].delay(unsafeAddChunk(bytes))

def computeAndReset: F[Chunk[Byte]] =
Sync[F].delay(unsafeComputeAndReset())

def unsafeAddChunk(chunk: Chunk[Byte]): Unit =
def unsafeUpdate(chunk: Chunk[Byte]): Unit =
h.update(chunk.toUint8Array)

def unsafeComputeAndReset(): Chunk[Byte] = {
val result = Chunk.uint8Array(h.digest())
def unsafeHash(): Hash = {
val result = Hash(Chunk.uint8Array(h.digest()))
h = newHash()
result
}
}

private def toAlgorithmString(algorithm: HashAlgorithm): String =
private[hashing] def toAlgorithmString(algorithm: HashAlgorithm): String =
algorithm match {
case HashAlgorithm.MD5 => "MD5"
case HashAlgorithm.SHA1 => "SHA1"
case HashAlgorithm.SHA224 => "SHA224"
case HashAlgorithm.SHA256 => "SHA256"
case HashAlgorithm.SHA384 => "SHA384"
case HashAlgorithm.SHA512 => "SHA512"
case HashAlgorithm.SHA512_224 => "SHA512-224"
case HashAlgorithm.SHA512_256 => "SHA512-256"
case HashAlgorithm.SHA3_224 => "SHA3-224"
case HashAlgorithm.SHA3_256 => "SHA3-256"
case HashAlgorithm.SHA3_384 => "SHA3-384"
case HashAlgorithm.SHA3_512 => "SHA3-512"
case HashAlgorithm.Named(name) => name
case other => sys.error(s"unsupported algorithm $other")
}

private[fs2] def unsafeHmac[F[_]: Sync](algorithm: HashAlgorithm, key: Chunk[Byte]): Hasher[F] =
new SyncHasher[F] {
private def newHash() = JsHash.createHmac(toAlgorithmString(algorithm), key.toUint8Array)
private var h = newHash()

def unsafeUpdate(chunk: Chunk[Byte]): Unit =
h.update(chunk.toUint8Array)

def unsafeHash(): Hash = {
val result = Hash(Chunk.uint8Array(h.digest()))
h = newHash()
result
}
}
}

Expand All @@ -74,6 +97,11 @@ private[hashing] object JsHash {
@nowarn212("cat=unused")
private[fs2] def createHash(algorithm: String): Hash = js.native

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

@js.native
@nowarn212("cat=unused")
private[fs2] trait Hash extends js.Object {
Expand Down
12 changes: 9 additions & 3 deletions core/js/src/test/scala/fs2/hashing/HashingSuitePlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@ package hashing
import scodec.bits.ByteVector

trait HashingSuitePlatform {
def digest(algo: String, str: String): Chunk[Byte] = {
val hash = JsHash.createHash(algo.replace("-", ""))
def digest(algo: HashAlgorithm, str: String): Hash = {
val hash = JsHash.createHash(Hasher.toAlgorithmString(algo))
hash.update(ByteVector.view(str.getBytes).toUint8Array)
Chunk.uint8Array(hash.digest())
Hash(Chunk.uint8Array(hash.digest()))
}

def hmac(algo: HashAlgorithm, key: Chunk[Byte], str: String): Hash = {
val hash = JsHash.createHmac(Hasher.toAlgorithmString(algo), key.toUint8Array)
hash.update(ByteVector.view(str.getBytes).toUint8Array)
Hash(Chunk.uint8Array(hash.digest()))
}
}
63 changes: 0 additions & 63 deletions core/jvm/src/main/scala/fs2/hashing/HashCompanionPlatform.scala

This file was deleted.

112 changes: 112 additions & 0 deletions core/jvm/src/main/scala/fs2/hashing/HasherCompanionPlatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2013 Functional Streams for Scala
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package fs2
package hashing

import cats.effect.{Resource, Sync}

import java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

private[hashing] trait HasherCompanionPlatform {

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

private[hashing] def hmac[F[_]: Sync](
algorithm: HashAlgorithm,
key: Chunk[Byte]
): Resource[F, Hasher[F]] =
Resource.eval(Sync[F].delay(unsafeHmac(algorithm, key)))

private[hashing] def unsafe[F[_]: Sync](algorithm: HashAlgorithm): Hasher[F] =
unsafeFromMessageDigest(MessageDigest.getInstance(toAlgorithmString(algorithm)))

private[hashing] def toAlgorithmString(algorithm: HashAlgorithm): String =
algorithm match {
case HashAlgorithm.MD5 => "MD5"
case HashAlgorithm.SHA1 => "SHA-1"
case HashAlgorithm.SHA224 => "SHA-224"
case HashAlgorithm.SHA256 => "SHA-256"
case HashAlgorithm.SHA384 => "SHA-384"
case HashAlgorithm.SHA512 => "SHA-512"
case HashAlgorithm.SHA512_224 => "SHA-512/224"
case HashAlgorithm.SHA512_256 => "SHA-512/256"
case HashAlgorithm.SHA3_224 => "SHA3-224"
case HashAlgorithm.SHA3_256 => "SHA3-256"
case HashAlgorithm.SHA3_384 => "SHA3-384"
case HashAlgorithm.SHA3_512 => "SHA3-512"
case HashAlgorithm.Named(name) => name
case other => sys.error(s"unsupported algorithm $other")
}

private[hashing] def unsafeHmac[F[_]: Sync](
algorithm: HashAlgorithm,
key: Chunk[Byte]
): Hasher[F] = {
val name = toMacAlgorithmString(algorithm)
val mac = Mac.getInstance(name)
mac.init(new SecretKeySpec(key.toArray, name))
unsafeFromMac(mac)
}

private[hashing] def toMacAlgorithmString(algorithm: HashAlgorithm): String =
algorithm match {
case HashAlgorithm.MD5 => "HmacMD5"
case HashAlgorithm.SHA1 => "HmacSHA1"
case HashAlgorithm.SHA224 => "HmacSHA224"
case HashAlgorithm.SHA256 => "HmacSHA256"
case HashAlgorithm.SHA384 => "HmacSHA384"
case HashAlgorithm.SHA512 => "HmacSHA512"
case HashAlgorithm.SHA512_224 => "HmacSHA512/224"
case HashAlgorithm.SHA512_256 => "HmacSHA512/256"
case HashAlgorithm.SHA3_224 => "HmacSHA3-224"
case HashAlgorithm.SHA3_256 => "HmacSHA3-256"
case HashAlgorithm.SHA3_384 => "HmacSHA3-384"
case HashAlgorithm.SHA3_512 => "HmacSHA3-512"
case HashAlgorithm.Named(name) => name
case other => sys.error(s"unsupported algorithm $other")
}

def unsafeFromMessageDigest[F[_]: Sync](d: MessageDigest): Hasher[F] =
new SyncHasher[F] {
def unsafeUpdate(chunk: Chunk[Byte]): Unit = {
val slice = chunk.toArraySlice
d.update(slice.values, slice.offset, slice.size)
}

def unsafeHash(): Hash =
Hash(Chunk.array(d.digest()))
}

def unsafeFromMac[F[_]: Sync](d: Mac): Hasher[F] =
new SyncHasher[F] {
def unsafeUpdate(chunk: Chunk[Byte]): Unit = {
val slice = chunk.toArraySlice
d.update(slice.values, slice.offset, slice.size)
}

def unsafeHash(): Hash =
Hash(Chunk.array(d.doFinal()))
}
}
16 changes: 14 additions & 2 deletions core/jvm/src/test/scala/fs2/hashing/HashingSuitePlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,22 @@
*/

package fs2
package hashing

import java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

trait HashingSuitePlatform {
def digest(algo: String, str: String): Chunk[Byte] =
Chunk.array(MessageDigest.getInstance(algo).digest(str.getBytes))
def digest(algo: HashAlgorithm, str: String): Hash =
Hash(
Chunk.array(MessageDigest.getInstance(Hasher.toAlgorithmString(algo)).digest(str.getBytes))
)

def hmac(algo: HashAlgorithm, key: Chunk[Byte], str: String): Hash = {
val name = Hasher.toMacAlgorithmString(algo)
val m = Mac.getInstance(name)
m.init(new SecretKeySpec(key.toArray, name))
Hash(Chunk.array(m.doFinal(str.getBytes)))
}
}
2 changes: 1 addition & 1 deletion core/native/src/main/scala/fs2/hash.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@ object hash {
algorithm: HashAlgorithm
)(implicit F: Sync[F]): Pipe[F, Byte, Byte] = {
val h = Hashing.forSync[F]
h.hashWith(h.create(algorithm))
s => h.hashWith(h.hasher(algorithm))(s).map(_.bytes).unchunks
}
}
Loading

0 comments on commit 333b753

Please sign in to comment.