packages feed

ppad-pbkdf-0.2.0: lib/Crypto/KDF/PBKDF.hs

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

-- |
-- Module: Crypto.KDF.PBKDF
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- A pure PBKDF2 (password-based key derivation
-- function) implementation, as specified by
-- [RFC2898](https://datatracker.ietf.org/doc/html/rfc2898).

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

    -- * PBKDF2
  , derive
  )where

import Control.Monad (guard)
import Data.Bits ((.>>.), (.&.))
import qualified Data.Bits as B
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Builder.Extra as BE
import Data.Word (Word32, Word64)

-- 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.
--
--   (RFC2898 specifically requires a "pseudorandom function" of two
--   arguments, but in practice this will usually be a HMAC function.)
--
--   >>> 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

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

-- serialize a 32-bit word, MSB first
ser32 :: Word32 -> BS.ByteString
ser32 w =
  let !mask = 0b00000000_00000000_00000000_11111111
      !w0 = fi (w .>>. 24) .&. mask
      !w1 = fi (w .>>. 16) .&. mask
      !w2 = fi (w .>>. 08) .&. mask
      !w3 = fi w .&. mask
  in  BS.cons w0 (BS.cons w1 (BS.cons w2 (BS.singleton w3)))
{-# INLINE ser32 #-}

-- bytewise xor on bytestrings
xor :: BS.ByteString -> BS.ByteString -> BS.ByteString
xor = BS.packZipWith B.xor
{-# INLINE xor #-}

-- | Derive a key from a secret via the PBKDF2 key derivation function.
--
--   >>> :set -XOverloadedStrings
--   >>> import qualified Crypto.Hash.SHA256 as SHA256
--   >>> import qualified Data.ByteString as BS
--   >>> import qualified Data.ByteString.Base16 as B16
--   >>> let Just key = derive SHA256.hmac "passwd" "salt" 1 64
--   >>> BS.take 16 (B16.encode key)
--   "55ac046e56e3089f"
derive
  :: HMAC          -- ^ pseudo-random function (HMAC)
  -> BS.ByteString -- ^ password
  -> BS.ByteString -- ^ salt
  -> Word64        -- ^ iteration count
  -> Word32        -- ^ bytelength of derived key (max 0xffff_ffff * hlen)
  -> Maybe BS.ByteString -- ^ derived key
derive prf p s c dklen = do
    guard (dklen <= 0xffff_ffff * fi hlen)
    pure (loop mempty 1)
  where
    !hlen = BS.length (prf mempty mempty)
    !l = ceiling (fi dklen / fi hlen :: Double) :: Word32
    !r = fi (dklen - (l - 1) * fi hlen)

    f !i =
      let go j !acc !las
            | j == c = acc
            | otherwise =
                let u = prf p las
                    nacc = acc `xor` u
                in  go (j + 1) nacc u

          org = prf p (s <> ser32 i)

      in  go 1 org org
    {-# INLINE f #-}

    loop !acc !i
      | i == l =
          let t = f i
              fin = BS.take r t
          in  BS.toStrict $
                if   dklen <= 128
                then BE.toLazyByteStringWith
                       (BE.safeStrategy 128 BE.smallChunkSize) mempty $
                       acc <> BSB.byteString fin
                else BSB.toLazyByteString $
                       acc <> BSB.byteString fin
      | otherwise =
          let t = f i
              nacc = acc <> BSB.byteString t
          in  loop nacc (i + 1)