packages feed

ppad-pbkdf (empty) → 0.1.0

raw patch · 7 files changed

+360/−0 lines, 7 filesdep +aesondep +basedep +bytestring

Dependencies added: aeson, base, bytestring, criterion, ppad-base16, ppad-pbkdf, ppad-sha256, ppad-sha512, tasty, tasty-hunit, text

Files

+ CHANGELOG view
@@ -0,0 +1,5 @@+# Changelog++- 0.1.0 (2025-02-24)+  * Initial release, supporting a generic RFC2898 PBKDF2 function.+
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2025 Jared Tobin++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ bench/Main.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Criterion.Main+import qualified Crypto.KDF.PBKDF as KDF+import qualified Crypto.Hash.SHA256 as SHA256+import qualified Crypto.Hash.SHA512 as SHA512++main :: IO ()+main = defaultMain [+    suite+  ]++suite :: Benchmark+suite =+  bgroup "ppad-pbkdf" [+    bgroup "PBKDF-SHA256" [+      bench "derive (outlen 32)" $+        nf (KDF.derive SHA256.hmac "muh password" "muh salt" 32) 64+    ]+  , bgroup "PBKDF-SHA512" [+      bench "derive (outlen 32)" $+        nf (KDF.derive SHA512.hmac "muh password" "muh salt" 32) 64+    ]+  ]+
+ lib/Crypto/KDF/PBKDF.hs view
@@ -0,0 +1,107 @@+{-# 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 where++import Data.Bits ((.>>.), (.&.))+import qualified Data.Bits as B+import qualified Data.ByteString as BS+import qualified Data.ByteString.Builder as BSB+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+--   >>> BS.take 16 (B16.encode (derive SHA256.hmac "passwd" "salt" 1 64))+--   "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)+  -> BS.ByteString -- ^ derived key+derive prf p s c dklen+    | dklen > 0xffff_ffff * fi hlen =      -- 2 ^ 32 - 1+        error "ppad-pbkdf (derive): derived key too long"+    | otherwise =+        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 . BSB.toLazyByteString $+                acc <> BSB.byteString fin+      | otherwise =+          let t = f i+              nacc = acc <> BSB.byteString t+          in  loop nacc (i + 1)+
+ ppad-pbkdf.cabal view
@@ -0,0 +1,71 @@+cabal-version:      3.0+name:               ppad-pbkdf+version:            0.1.0+synopsis:           A password-based key derivation function+license:            MIT+license-file:       LICENSE+author:             Jared Tobin+maintainer:         jared@ppad.tech+category:           Cryptography+build-type:         Simple+tested-with:        GHC == { 9.8.1 }+extra-doc-files:    CHANGELOG+description:+  A pure implementation of the password-based key derivation function PBKDF2,+  per RFC 2898.++source-repository head+  type:     git+  location: git.ppad.tech/pbkdf.git++library+  default-language: Haskell2010+  hs-source-dirs:   lib+  ghc-options:+      -Wall+  exposed-modules:+      Crypto.KDF.PBKDF+  build-depends:+      base >= 4.9 && < 5+    , bytestring >= 0.9 && < 0.13++test-suite pbkdf-tests+  type:                exitcode-stdio-1.0+  default-language:    Haskell2010+  hs-source-dirs:      test+  main-is:             Main.hs+  other-modules:+    Wycheproof++  ghc-options:+    -rtsopts -Wall -O2++  build-depends:+      aeson+    , base+    , bytestring+    , ppad-base16+    , ppad-pbkdf+    , ppad-sha256+    , ppad-sha512+    , tasty+    , tasty-hunit+    , text++benchmark pbkdf-bench+  type:                exitcode-stdio-1.0+  default-language:    Haskell2010+  hs-source-dirs:      bench+  main-is:             Main.hs++  ghc-options:+    -rtsopts -O2 -Wall++  build-depends:+      base+    , bytestring+    , criterion+    , ppad-pbkdf+    , ppad-sha256+    , ppad-sha512+
+ test/Main.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Main where++import Control.Exception+import qualified Crypto.Hash.SHA256 as SHA256+import qualified Crypto.Hash.SHA512 as SHA512+import qualified Crypto.KDF.PBKDF as KDF+import qualified Data.ByteString as BS+import qualified Data.Aeson as A+import qualified Data.Text.IO as TIO+import Test.Tasty+import Test.Tasty.HUnit+import qualified Wycheproof as W++main :: IO ()+main = do+  wycheproof_hmacsha256 <- TIO.readFile "etc/pbkdf2_hmacsha256_test.json"+  wycheproof_hmacsha512 <- TIO.readFile "etc/pbkdf2_hmacsha512_test.json"+  let wycheproofs = do+        a <- A.decodeStrictText wycheproof_hmacsha256 :: Maybe W.Wycheproof+        b <- A.decodeStrictText wycheproof_hmacsha512 :: Maybe W.Wycheproof+        pure (a, b)+  case wycheproofs of+    Nothing -> error "couldn't parse wycheproof vectors"+    Just (w256, w512) -> defaultMain $ testGroup "ppad-pbkdf" [+        wycheproof_tests SHA256 w256+      , wycheproof_tests SHA512 w512+      ]++data Hash = SHA256 | SHA512+  deriving Show++wycheproof_tests :: Hash -> W.Wycheproof -> TestTree+wycheproof_tests h W.Wycheproof {..} =+  testGroup ("wycheproof vectors (pbkdf, " <> show h <> ")") $+    fmap (execute_group h) wp_testGroups++execute_group :: Hash -> W.PbkdfTestGroup -> TestTree+execute_group h W.PbkdfTestGroup {..} =+  testGroup mempty (fmap (execute h) ptg_tests)++execute :: Hash -> W.PbkdfTest -> TestTree+execute h W.PbkdfTest {..} = testCase t_msg $ do+    let pas = pt_password+        sal = pt_salt+        cow = pt_iterationCount+        siz = pt_dkLen+        pec = pt_dk+    if   pt_result == "invalid"+    then do+      out <- try (pure $! KDF.derive hmac pas sal cow siz)+               :: IO (Either ErrorCall BS.ByteString)+      case out of+        Left _  -> assertBool "invalid" True+        Right o -> assertBool "invalid" (pec /= o)+    else do+      let out = KDF.derive hmac pas sal cow siz+      assertEqual mempty pec out+  where+    hmac = case h of+      SHA256 -> SHA256.hmac+      SHA512 -> SHA512.hmac+    t_msg = "test " <> show pt_tcId -- XX embellish+
+ test/Wycheproof.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE OverloadedStrings #-}++module Wycheproof (+    Wycheproof(..)+  , PbkdfTestGroup(..)+  , PbkdfTest(..)+  ) where++import Data.Aeson ((.:))+import qualified Data.Aeson as A+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base16 as B16+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Data.Word (Word32, Word64)++data Wycheproof = Wycheproof {+    wp_numberOfTests :: !Int+  , wp_testGroups    :: ![PbkdfTestGroup]+  } deriving Show++instance A.FromJSON Wycheproof where+  parseJSON = A.withObject "Wycheproof" $ \m -> Wycheproof+    <$> m .: "numberOfTests"+    <*> m .: "testGroups"++data PbkdfTestGroup = PbkdfTestGroup {+    ptg_type    :: !T.Text+  , ptg_tests   :: ![PbkdfTest]+  } deriving Show++instance A.FromJSON PbkdfTestGroup where+  parseJSON = A.withObject "PbkdfTestGroup" $ \m -> PbkdfTestGroup+    <$> m .: "type"+    <*> m .: "tests"++data PbkdfTest = PbkdfTest {+    pt_tcId           :: !Int+  , pt_comment        :: !T.Text+  , pt_password       :: !BS.ByteString+  , pt_salt           :: !BS.ByteString+  , pt_iterationCount :: !Word64+  , pt_dkLen          :: !Word32+  , pt_dk             :: !BS.ByteString+  , pt_result         :: !T.Text+  } deriving Show++decodehex :: T.Text -> BS.ByteString+decodehex t = case B16.decode (TE.encodeUtf8 t) of+  Nothing -> error "bang"+  Just bs -> bs++instance A.FromJSON PbkdfTest where+  parseJSON = A.withObject "PbkdfTest" $ \m -> PbkdfTest+    <$> m .: "tcId"+    <*> m .: "comment"+    <*> fmap decodehex (m .: "password")+    <*> fmap decodehex (m .: "salt")+    <*> m .: "iterationCount"+    <*> m .: "dkLen"+    <*> fmap decodehex (m .: "dk")+    <*> m .: "result"++