Hmac sha256

use "path:/usr/local/opt/libressl/lib" if osx and x86
use "path:/opt/homebrew/opt/libressl/lib" if osx and arm
use "lib:crypto"

use @HMAC[Pointer[U8]](
  evp_md: Pointer[_EVPMD],
  key: Pointer[U8] tag, key_len: I32,
  data: Pointer[U8] tag, data_len: USize,
  md: Pointer[U8] tag, md_len: Pointer[U32])

primitive HmacSha256
  """
  Compute HMAC using SHA-256 as the hash function, as defined in RFC 2104.

  Returns a 32-byte message authentication code.

  ```pony
  let mac = HmacSha256("secret-key", "Hello, World!")?
  ```

  Raises an error when the key is longer than OpenSSL's `int` can hold, and when
  OpenSSL could not compute the code.

  When it raises, reject. Do not compare a message against a code of your own
  making: a code you invent is one an attacker can send you.
  """
  fun apply(key: ByteSeq, data: ByteSeq): Array[U8] val ? =>
    if key.size() > I32.max_value().usize() then error end

    recover
      let size: USize = 32
      let arr = Array[U8].init(0, size)

      // `HMAC` returns NULL when the message pointer is null, even when the
      // message is empty, and an empty `Array[U8]` has a null pointer. A code
      // over an empty message is well defined, so hand it a pointer to bytes it
      // will not read.
      let message =
        if data.size() == 0 then arr.cpointer() else data.cpointer() end

      let md =
        @HMAC(
          @EVP_sha256(),
          key.cpointer(),
          key.size().i32(),
          message,
          data.size(),
          arr.cpointer(),
          Pointer[U32])

      // `HMAC` writes nothing when it fails, and `arr` is 32 zero bytes, which
      // is a code an attacker can send. Raise rather than return it.
      if md.is_null() then error end
      arr
    end