packages feed

sha256-0.1.0.0: lib/Crypto/Sha256/Hmac.hs

{-# LANGUAGE ViewPatterns, LambdaCase, BangPatterns #-}

-------------------------------------------------------------------------------
-- |
-- Module:      Crypto.Sha256.Hmac
-- Copyright:   (c) 2024 Auth Global
-- License:     Apache2
--
-- An implementation of HMAC-SHA256 that supports precomputed keys, streaming,
-- backtracking, bitstring inputs, and (de)serialization of intermediate states.
--
-------------------------------------------------------------------------------

module Crypto.Sha256.Hmac
  ( hmac
  , hmac'
  , HmacKeyPlain
  -- , hmacKeyPlain_eq
  , HmacKey()
  , hmacKey
  , hmacKey_hashed
  , hmacKey_toPlain
  , hmacKey_toHashed
  , hmacKey_forgetPlain
  , hmacKey_run
  , HmacKeyLike()
  , hmacKeyLike
  , hmacKeyLike_init
  , hmacKeyLike_initHashed
  , hmacKeyLike_initPrefixed
  , hmacKeyLike_toKey
  , hmacKeyLike_toPlain
  , hmacKeyLike_toHashed
  , hmacKeyLike_toPrefixed
  , hmacKeyLike_run
  , hmacKeyLike_byteCount
  , hmacKeyLike_blockCount
  , hmacKeyLike_bufferLength
  -- , hmacKeyLike_feeds
  -- , hmacKeyLike_feedsWith
  , HmacKeyHashed()
  , hmacKeyHashed
  , hmacKeyHashed_toKey
  , hmacKeyHashed_run
  , hmacKeyHashed_runWith
  , HmacKeyPrefixed()
  , hmacKeyPrefixed
  , hmacKeyPrefixed_init
  , hmacKeyPrefixed_initHashed
  , hmacKeyPrefixed_initLike
  , hmacKeyPrefixed_toHashed
  -- , hmacKeyPrefixed_eqHashed
  , hmacKeyPrefixed_feed
  , hmacKeyPrefixed_feeds
  , hmacKeyPrefixed_feedsWith
  , hmacKeyPrefixed_run
  , hmacKeyPrefixed_byteCount
  , hmacKeyPrefixed_blockCount
  , hmacKeyPrefixed_bufferLength
  , HmacCtx()
  , hmacCtx
  , hmacCtx_init
  , hmacCtx_initWith
  , hmacCtx_update,  hmacCtx_feed
  , hmacCtx_updates, hmacCtx_feeds
  , hmacCtx_finalize     , hmacCtx_finalize_toByteString
  , hmacCtx_finalizeBits , hmacCtx_finalizeBits_toByteString
  , hmacCtx_finalizeBytes, hmacCtx_finalizeBytes_toByteString
  , hmacCtx_byteCount
  , hmacCtx_blockCount
  , hmacCtx_bufferLength
--  , hmacCtx_toHmacKeyPrefixed
  ) where

import           Data.Bits(xor)
import           Data.ByteString (ByteString)
import qualified Data.ByteString as B
import           Data.List(scanl')
import           Data.Function((&))
import           Data.Foldable(Foldable, toList)
import           Data.Int
import           Data.Word

import           Crypto.HashString
import           Crypto.Sha256 as Sha256
import           Crypto.Sha256.Subtle
import           Crypto.Sha256.Hmac.Implementation

-- Should these be made publicly available?  Are these available anywhere else?
dropBs :: Int64 -> [ ByteString ] -> [ ByteString ]
dropBs = go
  where
    len = fromIntegral . B.length
    go _ [] = []
    go 0 bs = bs
    go n (b:bs)
      | n >= len b = go (n - len b) bs
      | otherwise = B.drop (fromIntegral n) b : bs

takeBs :: Int64 -> [ ByteString ] -> [ ByteString ]
takeBs = go
  where
    len = fromIntegral . B.length
    go _ [] = []
    go n (b:bs)
      | n <= 0 = []
      | len b < n = b : go (n - len b) bs
      | otherwise = [B.take (fromIntegral n) b]

takeBs' :: Int64 -> [ ByteString ] -> [ ByteString ]
takeBs' n bs = if haveEnough then takeBs n bs else []
  where
    len = fromIntegral . B.length
    haveEnough = any (>= n) (scanl' (+) 0 (map len bs))


-- Initialize a precomputed hmac key from a plaintext bytestring, which
-- can then be turned into an hmac context using 'hmacKey_run'
--
-- Note this structure retains the plaintext key, which isn't strictly necessary
-- for actually computing the resulting hmac function.  The plaintext key can
-- be forgotten using 'hmacKey_forgetPlain'.
hmacKey :: HmacKeyPlain -> HmacKey
hmacKey key = HmacKey_Plain key (hmacKeyHashed key)

-- | If the plaintext hmac key has been remembered by the precomputed key, return it.
--   Otherwise return 'Nothing'.  Keys precomputed by 'hmacKey' retain the plaintext,
--   which can subsequently be forgotten by 'hmacKey_forgetPlain'.  Alternatively,
--   keys precomputed by 'hmacKey_hashed' never retains the plaintext key in the
--   first place.
hmacKey_toPlain :: HmacKey -> Maybe HmacKeyPlain
hmacKey_toPlain = \case
  HmacKey_Plain a _ -> Just a
  HmacKey_Hashed _  -> Nothing

-- | Forget any plaintext hmac keys being retained by a given precomputed key, meaning
--   that for all x, @hmacKey_toPlain (hmacKey_forgetPlain x) == Nothing@.
--
--   This is potentially useful when implementing PBKDF2, as the plaintext password can
--   immediately be replaced with a precomputed hmac key, even before key-stretching
--   is complete. Note that the precomputed hmac key does provide a fast brute-force
--   attack on the plaintext key, typically as little as 1 SHA256 block, so this cannot be
--   relied upon for secrecy if the hmac key is potentially guessable, such as a weak
--   password or a non-secret salt.

hmacKey_forgetPlain :: HmacKey -> HmacKey
hmacKey_forgetPlain = \case
  HmacKey_Plain _ b -> HmacKey_Hashed b
  x@(HmacKey_Hashed _) -> x

hmacKeyLike :: HmacKeyPlain -> HmacKeyLike
hmacKeyLike key = HmacKeyLike_Plain key (hmacKeyHashed key)

hmacKeyLike_init :: HmacKey -> HmacKeyLike
hmacKeyLike_init = \case
  HmacKey_Plain a b -> HmacKeyLike_Plain a b
  HmacKey_Hashed b -> HmacKeyLike_Hashed b

hmacKeyLike_initHashed :: HmacKeyHashed -> HmacKeyLike
hmacKeyLike_initHashed = HmacKeyLike_Hashed

hmacKeyLike_initPrefixed :: HmacKeyPrefixed -> HmacKeyLike
hmacKeyLike_initPrefixed = HmacKeyLike_Prefixed

hmacKeyLike_toPlain :: HmacKeyLike -> Maybe HmacKeyPlain
hmacKeyLike_toPlain = \case
  HmacKeyLike_Plain a _ -> Just a
  HmacKeyLike_Hashed _ -> Nothing
  HmacKeyLike_Prefixed _ -> Nothing

hmacKeyLike_toHashed :: HmacKeyLike -> Maybe HmacKeyHashed
hmacKeyLike_toHashed = \case
  HmacKeyLike_Plain _ a -> Just a
  HmacKeyLike_Hashed a -> Just a
  HmacKeyLike_Prefixed a -> hmacKeyPrefixed_toHashed a

hmacKeyLike_toKey :: HmacKeyLike -> Maybe HmacKey
hmacKeyLike_toKey = \case
  HmacKeyLike_Plain a b -> Just $ HmacKey_Plain a b
  HmacKeyLike_Hashed b -> Just $ HmacKey_Hashed b
  HmacKeyLike_Prefixed c -> HmacKey_Hashed <$> hmacKeyPrefixed_toHashed c

hmacKeyLike_run :: HmacKeyLike -> HmacCtx
hmacKeyLike_run = \case
  HmacKeyLike_Plain _ a -> hmacKeyHashed_run a
  HmacKeyLike_Hashed a -> hmacKeyHashed_run a
  HmacKeyLike_Prefixed a -> hmacKeyPrefixed_run a

-- | how many bytes have been fed into the SHA256 state machine?  This is always 64
--   more bytes than hmac's "message" input.
--   If @hmacKeyLike_toKey x == Just ...@, then @hmacKeyLike_byteCount x == 64@.
--   If @hmacKeyLike_toKey x == Nothing@, then this returns a multiple of 64 that is
--   greater or equal to 128.
--   

hmacKeyLike_byteCount :: HmacKeyLike -> Word64
hmacKeyLike_byteCount = \case
  HmacKeyLike_Plain _ _ -> 64
  HmacKeyLike_Hashed _ -> 64
  HmacKeyLike_Prefixed b -> hmacKeyPrefixed_byteCount b

hmacKeyLike_blockCount :: HmacKeyLike -> Word64
hmacKeyLike_blockCount = \case
  HmacKeyLike_Plain _ _ -> 1
  HmacKeyLike_Hashed _ -> 1
  HmacKeyLike_Prefixed b -> hmacKeyPrefixed_blockCount b

hmacKeyLike_bufferLength :: HmacKeyLike -> Word8
hmacKeyLike_bufferLength = const 0

-- | Precompute an HmacKey without retaining the plaintext input, equivalent to
--   'hmacKey_forgetInput . hmacKey'
hmacKey_hashed :: HmacKeyPlain -> HmacKey
hmacKey_hashed = HmacKey_Hashed . hmacKeyHashed

hmacKey_run :: HmacKey -> HmacCtx
hmacKey_run = hmacCtx_init

hmacKeyHashed :: HmacKeyPlain -> HmacKeyHashed
hmacKeyHashed key = HmacKeyHashed opad ipad
  where
    ipad = tweak 0x36
    opad = tweak 0x5c
    k1 = if B.length key > 64 then Sha256.hash key else key
    k2 = B.append k1 (B.replicate (64 - B.length k1) 0)
    tweak c = sha256state_init & sha256state_feed (B.map (xor c) k2)

hmacKeyHashed_toKey :: HmacKeyHashed -> HmacKey
hmacKeyHashed_toKey = HmacKey_Hashed

hmacKeyHashed_run :: HmacKeyHashed -> HmacCtx
hmacKeyHashed_run key = HmacCtx
    { hmacCtx_ipadCtx = hmacKeyHashed_ipadCtx key
    , hmacCtx_opad = hmacKeyHashed_opad key
    }

hmacKeyHashed_runWith :: HmacKeyHashed -> ByteString -> HmacCtx
hmacKeyHashed_runWith key str = HmacCtx
    { hmacCtx_ipadCtx = hmacKeyHashed_runIpadCtx key str
    , hmacCtx_opad = hmacKeyHashed_opad key
    }

hmacKeyPrefixed :: HmacKeyPlain -> HmacKeyPrefixed
hmacKeyPrefixed = hmacKeyPrefixed_initHashed . hmacKeyHashed

hmacKeyPrefixed_init :: HmacKey -> HmacKeyPrefixed
hmacKeyPrefixed_init = hmacKeyPrefixed_initHashed . hmacKey_toHashed

hmacKeyPrefixed_initLike :: HmacKeyLike -> HmacKeyPrefixed
hmacKeyPrefixed_initLike = hmacKeyLike_toPrefixed

hmacKeyPrefixed_toHashed :: HmacKeyPrefixed -> Maybe HmacKeyHashed
hmacKeyPrefixed_toHashed x =
  if hmacKeyPrefixed_blockCount x == 1
  then Just $ HmacKeyHashed
    { hmacKeyHashed_ipad = sha256state_fromCtxInplace (hmacKeyPrefixed_ipadCtx x)
    , hmacKeyHashed_opad = hmacKeyPrefixed_opad x
    }
  else Nothing

hmacKeyPrefixed_feed :: ByteString -> HmacKeyPrefixed -> (ByteString, HmacKeyPrefixed)
hmacKeyPrefixed_feed x = hmacKeyPrefixed_feeds [x]

hmacKeyPrefixed_feeds :: Foldable f => f ByteString -> HmacKeyPrefixed -> (ByteString, HmacKeyPrefixed)
hmacKeyPrefixed_feeds = hmacKeyPrefixed_feedsWith id

hmacKeyPrefixed_feedsWith :: Foldable f => (a -> ByteString) -> f a -> HmacKeyPrefixed -> (ByteString, HmacKeyPrefixed)
hmacKeyPrefixed_feedsWith f = go . map f . toList
  where
    go bss !st =
      case takeBs' 64 bss of
        [] -> (B.concat bss, st)
        x  -> go (dropBs 64 bss) st'
                where
                  st' = HmacKeyPrefixed
                    { hmacKeyPrefixed_ipadCtx = sha256_updates (hmacKeyPrefixed_ipadCtx st) x
                    , hmacKeyPrefixed_opad = hmacKeyPrefixed_opad st
                    }

hmacKeyPrefixed_run :: HmacKeyPrefixed -> HmacCtx
hmacKeyPrefixed_run key = HmacCtx
    { hmacCtx_ipadCtx = hmacKeyPrefixed_ipadCtx key
    , hmacCtx_opad    = hmacKeyPrefixed_opad key
    }

hmacKeyPrefixed_byteCount :: HmacKeyPrefixed -> Word64
hmacKeyPrefixed_byteCount = sha256_byteCount . hmacKeyPrefixed_ipadCtx

hmacKeyPrefixed_bufferLength :: HmacKeyPrefixed -> Word8
hmacKeyPrefixed_bufferLength = const 0

-- | A simple interface to HMAC-SHA256. Note that this function was written
--   to make partial application an efficient way to compute the hmac of
--   multiple messages with exactly the same key:
--
--   @
--     let myHash = hmac "my-key"
--      in (myHash "message 1", myHash "message 2", myHash "message 3")
--   @
--
--   This typically saves two SHA-256 blocks per reused function application.
--   Thus this example saves four block computations from the two reused
--   calls to @myHash@ in this example.
--
--   Initializing the @myHash@ closure requires computing two SHA-256 blocks.
--   Applying the closure requires two further SHA-256 blocks per message,
--   as every message is less than 56 bytes long. Thus the total computation
--   requires 8 SHA-256 blocks with reuse, or 12 SHA-256 blocks without reuse.
--
--   Key reuse can save four or more block computations per application if
--   the reused key is longer than 64 bytes. I don't recommend using HMAC
--   keys that are longer than 64 bytes, as all such keys can be trivially
--   replaced with the SHA256 hash of the key, which is only 32 bytes long.
--
--   This high-level interface is implemented using 'hmacCtx_finalize',
--   'hmacKeyHashed_run', and 'hmacKeyHashed' composed in a point-free style
--   in order to help ensure key reuse works as expected.

hmac :: HmacKeyPlain -> ByteString -> ByteString
hmac = fmap hmacCtx_finalize_toByteString . hmacCtx_initWith . hmacKey_hashed

hmac' :: HmacKeyPlain -> ByteString -> HashString
hmac' = fmap hmacCtx_finalize . hmacCtx_initWith . hmacKey_hashed

hmacCtx :: HmacKeyPlain -> HmacCtx
hmacCtx = hmacCtx_init . hmacKey_hashed

-- | Initialize a new empty HMAC context from a precomputed HMAC key.

hmacCtx_init :: HmacKey -> HmacCtx
hmacCtx_init = hmacKeyHashed_run . hmacKey_toHashed

hmacCtx_initWith :: HmacKey -> ByteString -> HmacCtx
hmacCtx_initWith = hmacKeyHashed_runWith . hmacKey_toHashed

-- | Append a bytestring onto the end of the message argument to HMAC.

hmacCtx_update ::  HmacCtx -> ByteString -> HmacCtx
hmacCtx_update = flip hmacCtx_feed

hmacCtx_feed :: ByteString -> HmacCtx -> HmacCtx
hmacCtx_feed b (HmacCtx oc ic) = HmacCtx oc (sha256_update ic b)

-- | Append zero or more bytestrings onto the end of the message argument to
--   HMAC.

hmacCtx_updates :: Foldable f => HmacCtx -> f ByteString -> HmacCtx
hmacCtx_updates = flip hmacCtx_feeds

hmacCtx_feeds :: Foldable f => f ByteString -> HmacCtx -> HmacCtx
hmacCtx_feeds bs (HmacCtx oc ic) = HmacCtx oc (sha256_updates ic (toList bs))

-- | Finish computing the final 32-byte hash for an HMAC context.

hmacCtx_finalize :: HmacCtx -> HashString
hmacCtx_finalize = hmacCtx_finalizeBits B.empty 0

-- | Append any arbitrary bitstring onto the end of an HMAC context, and
--   finish computing the final 32-byte hash.

hmacCtx_finalizeBits :: ByteString -> Word64 -> HmacCtx -> HashString
hmacCtx_finalizeBits bits bitlen (HmacCtx oc ic) = outer
  where
    inner = sha256_finalizeBits_toByteString bits bitlen ic
    outer = sha256_finalize (sha256state_runWith 1 inner oc)

hmacCtx_finalize_toByteString :: HmacCtx -> ByteString
hmacCtx_finalize_toByteString = hmacCtx_finalizeBits_toByteString B.empty 0

-- | Append any arbitrary bitstring onto the end of an HMAC context, and
--   finish computing the final 32-byte hash.

hmacCtx_finalizeBits_toByteString :: ByteString -> Word64 -> HmacCtx -> ByteString
hmacCtx_finalizeBits_toByteString bits bitlen (HmacCtx oc ic) = outer
  where
    inner = sha256_finalizeBits_toByteString bits bitlen ic
    outer = sha256_finalize_toByteString (sha256state_runWith 1 inner oc)

hmacCtx_finalizeBytes :: ByteString -> HmacCtx -> HashString
hmacCtx_finalizeBytes = flip hmacCtx_finalizeBits maxBound

hmacCtx_finalizeBytes_toByteString :: ByteString -> HmacCtx -> ByteString
hmacCtx_finalizeBytes_toByteString = flip hmacCtx_finalizeBits_toByteString maxBound

hmacCtx_byteCount :: HmacCtx -> Word64
hmacCtx_byteCount = sha256_byteCount . hmacCtx_ipadCtx

hmacCtx_blockCount :: HmacCtx -> Word64
hmacCtx_blockCount = sha256_blockCount . hmacCtx_ipadCtx

hmacCtx_bufferLength :: HmacCtx -> Word8
hmacCtx_bufferLength = sha256_bufferLength . hmacCtx_ipadCtx

-- Ugh, I don't have convenient access to cryptohash's internal counter. I
-- should fix that. I also need to fix the fact that cryptohash-sha256 exposes
-- endianess issues in a publicly-facing bytestrings, thus potentially creating
-- less-than-immediately-obvious problems when serializing/deserializing SHA256
-- states. Thus part of the reason why I started on newer SHA256 bindings for
-- GHC 9.4.

-- hmacCtx_toHmacKeyPrefixed :: HmacCtx -> (ByteString, HmacKeyPrefixed)