packages feed

ppad-hkdf-0.3.0: lib/Crypto/KDF/HMAC.hs

{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ViewPatterns #-}

-- |
-- Module: Crypto.KDF.HMAC
-- Copyright: (c) 2024 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- A pure HKDF implementation, as specified by
-- [RFC5869](https://datatracker.ietf.org/doc/html/rfc5869).

module Crypto.KDF.HMAC (
    -- * HMAC synonym
    HMAC

    -- * HMAC-based KDF
  , derive

    -- internals
  , extract
  , expand
  , HMACEnv
  ) where

import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Internal as BI
import Data.Word (Word64)

fi :: (Integral a, Num b) => a -> b
fi = fromIntegral
{-# INLINE fi #-}

-- NB following synonym really only exists to make haddocks more
--    readable

-- | A HMAC function, taking a key as the first argument and the input
--   value as the second, producing a MAC digest.
--
--   >>> import qualified Crypto.Hash.SHA256 as SHA256
--   >>> :t SHA256.hmac
--   SHA256.hmac :: BS.ByteString -> BS.ByteString -> BS.ByteString
--   >>> SHA256.hmac "my HMAC key" "my HMAC input"
--   <256-bit MAC>
type HMAC = BS.ByteString -> BS.ByteString -> BS.ByteString

-- HMAC function and its associated outlength
data HMACEnv = HMACEnv
                 !HMAC
  {-# UNPACK #-} !Int

extract
  :: HMACEnv
  -> BS.ByteString  -- ^ salt
  -> BS.ByteString  -- ^ input keying material
  -> BS.ByteString  -- ^ pseudorandom key
extract (HMACEnv hmac hashlen) salt@(BI.PS _ _ l) ikm
  | l == 0    = hmac (BS.replicate hashlen 0x00) ikm
  | otherwise = hmac salt ikm
{-# INLINE extract #-}

expand
  :: HMACEnv
  -> BS.ByteString  -- ^ optional context and application-specific info
  -> Word64         -- ^ bytelength of output keying material
  -> BS.ByteString  -- ^ pseudorandom key
  -> Maybe BS.ByteString  -- ^ output keying material
expand (HMACEnv hmac hashlen) info (fi -> len) prk
    | len > 255 * hashlen = Nothing
    | otherwise = pure (BS.take len (go (1 :: Int) mempty mempty))
  where
    n = ceiling ((fi len :: Double) / (fi hashlen :: Double)) :: Int
    go !j t !tl
      | j > fi n = BS.toStrict (BSB.toLazyByteString t)
      | otherwise =
          let nt = hmac prk (tl <> info <> BS.singleton (fi j))
          in  go (succ j) (t <> BSB.byteString nt) nt
{-# INLINE expand #-}

-- | Derive a key from a secret, via a HMAC-based key derivation
--   function.
--
--   The /salt/ and /info/ arguments are optional to the KDF, and may
--   be simply passed as 'mempty'. An empty salt will be replaced by
--   /hashlen/ zero bytes.
--
--   >>> import qualified Crypto.Hash.SHA256 as SHA256
--   >>> derive SHA256.hmac "my public salt" mempty 64 "my secret input"
--   <64-byte output keying material>
derive
  :: HMAC          -- ^ HMAC function
  -> BS.ByteString -- ^ salt
  -> BS.ByteString -- ^ optional context and application-specific info
  -> Word64        -- ^ bytelength of output keying material (<= 255 * hashlen)
  -> BS.ByteString -- ^ input keying material
  -> Maybe BS.ByteString -- ^ output keying material
derive hmac salt info len = expand env info len . extract env salt where
  env = HMACEnv hmac (fi (BS.length (hmac mempty mempty)))