packages feed

hstratus-auth-0.1.0.0: src-internal/Network/HStratus/Internal/PBKDF2.hs

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

{- |
Module      : Network.HStratus.PBKDF2
Copyright   : (c) 2025 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD3

Copied then modified from an implementation in the package
[ppad-pbkdf](https://git.ppad.tech/pbkdf/file/lib/Crypto/KDF/PBKDF.hs.html)

Re-implemented here rather than making it direct dependency, because:
    - 1 fewer dependency => less future dependency-related maintenance
    - faster route for this package to stackage
        - as of (2025/04/01, ppad-ppbkdf was not on stackage)
-}
module Network.HStratus.Internal.PBKDF2
  ( -- * specify a pseudorandom function and derived key length
    FancyPseudoRandomF
  , wrap
  , wrapIO
  , PseudoRandomF
  , BadKeyLength (..)

    -- * perform PBKDF2 derivation
  , deriveKey

    -- * re-export
  , ByteString
  )
where

import Control.Exception (Exception, throwIO)
import Data.Bits (shiftR, xor, (.&.))
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.ByteString.Builder (byteString, toLazyByteString)
import Data.ByteString.Builder.Extra
  ( safeStrategy
  , smallChunkSize
  , toLazyByteStringWith
  )
import Data.Word (Word32, Word64)


{- | A pseudorandom function for use in PBKDF2

See
[PBKDF-RFC/section5.2](https://datatracker.ietf.org/doc/html/rfc2898#section-5.2)
-}
type PseudoRandomF = ByteString -> ByteString -> ByteString


-- | Indicates the derived key length is too long
data BadKeyLength = TooLong
  deriving (Eq, Show)


instance Exception BadKeyLength


{- | A 'PseudoRandomF' wrapped up with @dkLen@ and @hLen@'

where @dkLen@ the required length in octets of the derived key
and @hLen@ is the length of the output of the 'PseudoRandomF'

As per
[PBKDF-RFC/section5.2](https://datatracker.ietf.org/doc/html/rfc2898#section-5.2)

@dkLen@ must be at most 2^32 - 1 * @hLen@

The constructor `wrap` enforces this constraint
-}
newtype FancyPseudoRandomF = Fancy (PseudoRandomF, Word32, Word32)


-- | Construct a 'FancyPseudoRandomF'
wrap :: PseudoRandomF -> Word32 -> Either BadKeyLength FancyPseudoRandomF
wrap f dkLen =
  let !hLen = toNum $ BS.length $ f mempty mempty
   in if dkLen > 0xffffffff * hLen
        then Left TooLong
        else Right $ Fancy (f, dkLen, hLen)


-- | Like 'wrap', but fails by throwing 'BadKeyLength' in IO
wrapIO :: PseudoRandomF -> Word32 -> IO FancyPseudoRandomF
wrapIO f = either throwIO pure . wrap f


blockInfoOf :: FancyPseudoRandomF -> (Word32, Int)
blockInfoOf (Fancy (_f, !dkLen, hLen)) =
  let numBlocks = ceiling (toNum dkLen / toNum hLen :: Double)
      lastBlockSize = toNum $ dkLen - (numBlocks - 1) * hLen
   in (numBlocks, lastBlockSize)


{- | Derive a key from a secret using PBKDF2

Implements the key derivation algorithm described in
[PBKDF-RFC](https://datatracker.ietf.org/doc/html/rfc2898)

Usage - this example uses the SHA256 hmac function as the pseudorandom function

  >>> :set -XOverloadedStrings
  >>> import qualified Crypto.Hash.SHA256 as SHA256
  >>> pseudoF <- wrapIO SHA256.hmac 64
  >>> deriveKey pseudoF "passwd" "salt" 1000
-}
deriveKey
  :: FancyPseudoRandomF
  -- ^ a 'FancyPseudoRandomF'
  -> ByteString
  -- ^ the password from which to derive a key
  -> ByteString
  -- ^ the salt used in key derivation
  -> Word64
  -- ^ the iteration count
  -> ByteString
deriveKey fancy password salt count =
  let Fancy (!pseudoRandomF, !dkLen, !_notUsed) = fancy
      (!numBlocks, !lastBlockSize) = blockInfoOf fancy
      xorSum i =
        let initial = pseudoRandomF password $ salt <> asBytes i
            go j !current !_ignored | j == count = current
            go j !current !previous =
              let latest = pseudoRandomF password previous
               in go (j + 1) (current `xorBytes` latest) latest
         in go 1 initial initial
      {-# INLINE xorSum #-}

      smaller = safeStrategy 128 smallChunkSize
      strictBS =
        if dkLen <= 128
          then BS.toStrict . toLazyByteStringWith smaller mempty
          else BS.toStrict . toLazyByteString
      {-# INLINE strictBS #-}

      genBlocks i acc =
        if i < numBlocks
          then genBlocks (i + 1) (acc <> byteString (xorSum i))
          else strictBS $ acc <> byteString (BS.take lastBlockSize $ xorSum i)
   in genBlocks 1 mempty


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


asBytes :: Word32 -> ByteString
asBytes x =
  let !mask = 0b00000000000000000000000011111111
      !word0 = toNum (x `shiftR` 24) .&. mask
      !word1 = toNum (x `shiftR` 16) .&. mask
      !word2 = toNum (x `shiftR` 08) .&. mask
      !word3 = toNum x .&. mask
   in BS.cons word0 $ BS.cons word1 $ BS.cons word2 $ BS.singleton word3
{-# INLINE asBytes #-}


xorBytes :: ByteString -> ByteString -> ByteString
xorBytes = BS.packZipWith xor
{-# INLINE xorBytes #-}