Skip to content

Commit 333b753

Browse files
authored
Merge pull request #3465 from mpilquist/topic/digest
Update Hashing to support HMACs, update names in fs2.hashing to be clearer, introduce Digest type
2 parents e2925a2 + 4ff8b50 commit 333b753

File tree

16 files changed

+568
-228
lines changed

16 files changed

+568
-228
lines changed

core/js/src/main/scala/fs2/hash.scala

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
package fs2
2323

2424
import cats.effect.SyncIO
25-
import fs2.hashing.{Hash, HashAlgorithm}
25+
import fs2.hashing.{Hasher, HashAlgorithm}
2626

2727
/** Provides various cryptographic hashes as pipes. Supported only on Node.js. */
2828
@deprecated("Use fs2.hashing.Hashing[F] instead", "3.11.0")
@@ -49,12 +49,12 @@ object hash {
4949
private[this] def digest[F[_]](algorithm: HashAlgorithm): Pipe[F, Byte, Byte] =
5050
source =>
5151
Stream.suspend {
52-
val h = Hash.unsafe[SyncIO](algorithm)
52+
val h = Hasher.unsafe[SyncIO](algorithm)
5353
source.chunks
5454
.fold(h) { (h, c) =>
55-
h.addChunk(c).unsafeRunSync()
55+
h.update(c).unsafeRunSync()
5656
h
5757
}
58-
.flatMap(h => Stream.chunk(h.computeAndReset.unsafeRunSync()))
58+
.flatMap(h => Stream.chunk(h.hash.unsafeRunSync().bytes))
5959
}
6060
}

core/js/src/main/scala/fs2/hashing/HashCompanionPlatform.scala renamed to core/js/src/main/scala/fs2/hashing/HasherCompanionPlatform.scala

+42-14
Original file line numberDiff line numberDiff line change
@@ -30,40 +30,63 @@ import scala.scalajs.js
3030
import scala.scalajs.js.annotation.JSImport
3131
import scala.scalajs.js.typedarray.Uint8Array
3232

33-
trait HashCompanionPlatform {
33+
trait HasherCompanionPlatform {
3434

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

38-
private[fs2] def unsafe[F[_]: Sync](algorithm: HashAlgorithm): Hash[F] =
39-
new Hash[F] {
38+
private[hashing] def hmac[F[_]: Sync](
39+
algorithm: HashAlgorithm,
40+
key: Chunk[Byte]
41+
): Resource[F, Hasher[F]] =
42+
Resource.eval(Sync[F].delay(unsafeHmac(algorithm, key)))
43+
44+
private[fs2] def unsafe[F[_]: Sync](algorithm: HashAlgorithm): Hasher[F] =
45+
new SyncHasher[F] {
4046
private def newHash() = JsHash.createHash(toAlgorithmString(algorithm))
4147
private var h = newHash()
4248

43-
def addChunk(bytes: Chunk[Byte]): F[Unit] =
44-
Sync[F].delay(unsafeAddChunk(bytes))
45-
46-
def computeAndReset: F[Chunk[Byte]] =
47-
Sync[F].delay(unsafeComputeAndReset())
48-
49-
def unsafeAddChunk(chunk: Chunk[Byte]): Unit =
49+
def unsafeUpdate(chunk: Chunk[Byte]): Unit =
5050
h.update(chunk.toUint8Array)
5151

52-
def unsafeComputeAndReset(): Chunk[Byte] = {
53-
val result = Chunk.uint8Array(h.digest())
52+
def unsafeHash(): Hash = {
53+
val result = Hash(Chunk.uint8Array(h.digest()))
5454
h = newHash()
5555
result
5656
}
5757
}
5858

59-
private def toAlgorithmString(algorithm: HashAlgorithm): String =
59+
private[hashing] def toAlgorithmString(algorithm: HashAlgorithm): String =
6060
algorithm match {
6161
case HashAlgorithm.MD5 => "MD5"
6262
case HashAlgorithm.SHA1 => "SHA1"
63+
case HashAlgorithm.SHA224 => "SHA224"
6364
case HashAlgorithm.SHA256 => "SHA256"
6465
case HashAlgorithm.SHA384 => "SHA384"
6566
case HashAlgorithm.SHA512 => "SHA512"
67+
case HashAlgorithm.SHA512_224 => "SHA512-224"
68+
case HashAlgorithm.SHA512_256 => "SHA512-256"
69+
case HashAlgorithm.SHA3_224 => "SHA3-224"
70+
case HashAlgorithm.SHA3_256 => "SHA3-256"
71+
case HashAlgorithm.SHA3_384 => "SHA3-384"
72+
case HashAlgorithm.SHA3_512 => "SHA3-512"
6673
case HashAlgorithm.Named(name) => name
74+
case other => sys.error(s"unsupported algorithm $other")
75+
}
76+
77+
private[fs2] def unsafeHmac[F[_]: Sync](algorithm: HashAlgorithm, key: Chunk[Byte]): Hasher[F] =
78+
new SyncHasher[F] {
79+
private def newHash() = JsHash.createHmac(toAlgorithmString(algorithm), key.toUint8Array)
80+
private var h = newHash()
81+
82+
def unsafeUpdate(chunk: Chunk[Byte]): Unit =
83+
h.update(chunk.toUint8Array)
84+
85+
def unsafeHash(): Hash = {
86+
val result = Hash(Chunk.uint8Array(h.digest()))
87+
h = newHash()
88+
result
89+
}
6790
}
6891
}
6992

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

100+
@js.native
101+
@JSImport("crypto", "createHmac")
102+
@nowarn212("cat=unused")
103+
private[fs2] def createHmac(algorithm: String, key: Uint8Array): Hash = js.native
104+
77105
@js.native
78106
@nowarn212("cat=unused")
79107
private[fs2] trait Hash extends js.Object {

core/js/src/test/scala/fs2/hashing/HashingSuitePlatform.scala

+9-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@ package hashing
2525
import scodec.bits.ByteVector
2626

2727
trait HashingSuitePlatform {
28-
def digest(algo: String, str: String): Chunk[Byte] = {
29-
val hash = JsHash.createHash(algo.replace("-", ""))
28+
def digest(algo: HashAlgorithm, str: String): Hash = {
29+
val hash = JsHash.createHash(Hasher.toAlgorithmString(algo))
3030
hash.update(ByteVector.view(str.getBytes).toUint8Array)
31-
Chunk.uint8Array(hash.digest())
31+
Hash(Chunk.uint8Array(hash.digest()))
32+
}
33+
34+
def hmac(algo: HashAlgorithm, key: Chunk[Byte], str: String): Hash = {
35+
val hash = JsHash.createHmac(Hasher.toAlgorithmString(algo), key.toUint8Array)
36+
hash.update(ByteVector.view(str.getBytes).toUint8Array)
37+
Hash(Chunk.uint8Array(hash.digest()))
3238
}
3339
}

core/jvm/src/main/scala/fs2/hashing/HashCompanionPlatform.scala

-63
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) 2013 Functional Streams for Scala
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
5+
* this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to
7+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8+
* the Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16+
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18+
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package fs2
23+
package hashing
24+
25+
import cats.effect.{Resource, Sync}
26+
27+
import java.security.MessageDigest
28+
import javax.crypto.Mac
29+
import javax.crypto.spec.SecretKeySpec
30+
31+
private[hashing] trait HasherCompanionPlatform {
32+
33+
private[hashing] def apply[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hasher[F]] =
34+
Resource.eval(Sync[F].delay(unsafe(algorithm)))
35+
36+
private[hashing] def hmac[F[_]: Sync](
37+
algorithm: HashAlgorithm,
38+
key: Chunk[Byte]
39+
): Resource[F, Hasher[F]] =
40+
Resource.eval(Sync[F].delay(unsafeHmac(algorithm, key)))
41+
42+
private[hashing] def unsafe[F[_]: Sync](algorithm: HashAlgorithm): Hasher[F] =
43+
unsafeFromMessageDigest(MessageDigest.getInstance(toAlgorithmString(algorithm)))
44+
45+
private[hashing] def toAlgorithmString(algorithm: HashAlgorithm): String =
46+
algorithm match {
47+
case HashAlgorithm.MD5 => "MD5"
48+
case HashAlgorithm.SHA1 => "SHA-1"
49+
case HashAlgorithm.SHA224 => "SHA-224"
50+
case HashAlgorithm.SHA256 => "SHA-256"
51+
case HashAlgorithm.SHA384 => "SHA-384"
52+
case HashAlgorithm.SHA512 => "SHA-512"
53+
case HashAlgorithm.SHA512_224 => "SHA-512/224"
54+
case HashAlgorithm.SHA512_256 => "SHA-512/256"
55+
case HashAlgorithm.SHA3_224 => "SHA3-224"
56+
case HashAlgorithm.SHA3_256 => "SHA3-256"
57+
case HashAlgorithm.SHA3_384 => "SHA3-384"
58+
case HashAlgorithm.SHA3_512 => "SHA3-512"
59+
case HashAlgorithm.Named(name) => name
60+
case other => sys.error(s"unsupported algorithm $other")
61+
}
62+
63+
private[hashing] def unsafeHmac[F[_]: Sync](
64+
algorithm: HashAlgorithm,
65+
key: Chunk[Byte]
66+
): Hasher[F] = {
67+
val name = toMacAlgorithmString(algorithm)
68+
val mac = Mac.getInstance(name)
69+
mac.init(new SecretKeySpec(key.toArray, name))
70+
unsafeFromMac(mac)
71+
}
72+
73+
private[hashing] def toMacAlgorithmString(algorithm: HashAlgorithm): String =
74+
algorithm match {
75+
case HashAlgorithm.MD5 => "HmacMD5"
76+
case HashAlgorithm.SHA1 => "HmacSHA1"
77+
case HashAlgorithm.SHA224 => "HmacSHA224"
78+
case HashAlgorithm.SHA256 => "HmacSHA256"
79+
case HashAlgorithm.SHA384 => "HmacSHA384"
80+
case HashAlgorithm.SHA512 => "HmacSHA512"
81+
case HashAlgorithm.SHA512_224 => "HmacSHA512/224"
82+
case HashAlgorithm.SHA512_256 => "HmacSHA512/256"
83+
case HashAlgorithm.SHA3_224 => "HmacSHA3-224"
84+
case HashAlgorithm.SHA3_256 => "HmacSHA3-256"
85+
case HashAlgorithm.SHA3_384 => "HmacSHA3-384"
86+
case HashAlgorithm.SHA3_512 => "HmacSHA3-512"
87+
case HashAlgorithm.Named(name) => name
88+
case other => sys.error(s"unsupported algorithm $other")
89+
}
90+
91+
def unsafeFromMessageDigest[F[_]: Sync](d: MessageDigest): Hasher[F] =
92+
new SyncHasher[F] {
93+
def unsafeUpdate(chunk: Chunk[Byte]): Unit = {
94+
val slice = chunk.toArraySlice
95+
d.update(slice.values, slice.offset, slice.size)
96+
}
97+
98+
def unsafeHash(): Hash =
99+
Hash(Chunk.array(d.digest()))
100+
}
101+
102+
def unsafeFromMac[F[_]: Sync](d: Mac): Hasher[F] =
103+
new SyncHasher[F] {
104+
def unsafeUpdate(chunk: Chunk[Byte]): Unit = {
105+
val slice = chunk.toArraySlice
106+
d.update(slice.values, slice.offset, slice.size)
107+
}
108+
109+
def unsafeHash(): Hash =
110+
Hash(Chunk.array(d.doFinal()))
111+
}
112+
}

core/jvm/src/test/scala/fs2/hashing/HashingSuitePlatform.scala

+14-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,22 @@
2020
*/
2121

2222
package fs2
23+
package hashing
2324

2425
import java.security.MessageDigest
26+
import javax.crypto.Mac
27+
import javax.crypto.spec.SecretKeySpec
2528

2629
trait HashingSuitePlatform {
27-
def digest(algo: String, str: String): Chunk[Byte] =
28-
Chunk.array(MessageDigest.getInstance(algo).digest(str.getBytes))
30+
def digest(algo: HashAlgorithm, str: String): Hash =
31+
Hash(
32+
Chunk.array(MessageDigest.getInstance(Hasher.toAlgorithmString(algo)).digest(str.getBytes))
33+
)
34+
35+
def hmac(algo: HashAlgorithm, key: Chunk[Byte], str: String): Hash = {
36+
val name = Hasher.toMacAlgorithmString(algo)
37+
val m = Mac.getInstance(name)
38+
m.init(new SecretKeySpec(key.toArray, name))
39+
Hash(Chunk.array(m.doFinal(str.getBytes)))
40+
}
2941
}

core/native/src/main/scala/fs2/hash.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,6 @@ object hash {
5050
algorithm: HashAlgorithm
5151
)(implicit F: Sync[F]): Pipe[F, Byte, Byte] = {
5252
val h = Hashing.forSync[F]
53-
h.hashWith(h.create(algorithm))
53+
s => h.hashWith(h.hasher(algorithm))(s).map(_.bytes).unchunks
5454
}
5555
}

0 commit comments

Comments
 (0)