packages feed

hstratus-auth-0.1.0.0: test/HStratus/PBKDF2Spec.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : HStratus.PBKDF2Spec
Copyright   : (c) 2023 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD3
-}
module HStratus.PBKDF2Spec (spec) where

import qualified Crypto.Hash.SHA1 as SHA1
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Crypto.Hash.SHA512 as SHA512
import Data.Aeson
  ( FromJSON (..)
  , Object
  , Value
  , eitherDecodeFileStrict
  , withArray
  , withObject
  , (.:)
  )
import Data.Aeson.Types (Parser)
import Data.ByteString (ByteString)
import Data.ByteString.Base16 (decode)
import Data.Foldable (toList)
import Data.Text.Encoding (encodeUtf8)
import Data.Word (Word32, Word64, Word8)
import Network.HStratus.Internal.PBKDF2 (PseudoRandomF, deriveKey, wrap)
import Paths_hstratus_auth (getDataFileName)
import Test.Hspec (Spec, context, describe, it, runIO, shouldBe)
import Test.Hspec.Runner (SpecWith)


spec :: Spec
spec = describe "module Network.HStratus.PBKDF2" $ do
  describe "deriveKey" $ do
    context "when using the wycheProof test cases" $ do
      specFrom "sha1" SHA1.hmac
      specFrom "sha256" SHA256.hmac
      specFrom "sha512" SHA512.hmac


specFrom :: String -> PseudoRandomF -> Spec
specFrom shaName pseudo = do
  context ("with " ++ shaName ++ ".hmac as the pseudorandom function") $ do
    testData <- runIO $ namedDataPath shaName >>= loadTestData
    mapM_ (specWithFrom pseudo) testData


specWithFrom :: PseudoRandomF -> TestDescription -> SpecWith ()
specWithFrom pseudo td = do
  let TestDescription{tdSalt, tdIterationCount = count, tdDerivedKey = key} = td
  it ("test " ++ show (tdId td) ++ " should succeed") $ do
    let pseudo' = wrap pseudo (tdDerivedLength td)
        calc x = deriveKey x (tdPassword td) tdSalt count
    calc <$> pseudo' `shouldBe` Right key


loadTestData :: FilePath -> IO [TestDescription]
loadTestData src =
  let decodeDataFile aPath = fmap unTestFile <$> eitherDecodeFileStrict aPath
   in decodeDataFile src >>= either fail pure


namedDataPath :: String -> IO FilePath
namedDataPath shaName =
  let path = "testdata/pbkdf2_hmac" ++ shaName ++ "_test.json"
   in getDataFileName path


newtype TestFile = TestFile {unTestFile :: [TestDescription]}


instance FromJSON TestFile where
  parseJSON = fmap TestFile . parseTestFile


parseTestFile :: Value -> Parser [TestDescription]
parseTestFile =
  let oneTest = withObject "test" parseTestDescription
      manyTests = traverse oneTest . toList
      testsInTestGroup o = o .: "tests" >>= withArray "[test]" manyTests
      oneTestGroup = withObject "testsInTestGroup" testsInTestGroup
      manyTestGroups = traverse oneTestGroup . toList
      testGroupsAtTop o = o .: "testGroups" >>= withArray "[testGroups]" manyTestGroups
   in withObject "dataFile" (fmap concat . testGroupsAtTop)


data TestDescription = TestDescription
  { tdId :: !Word8
  , tdPassword :: !ByteString
  , tdSalt :: !ByteString
  , tdIterationCount :: !Word64
  , tdDerivedLength :: !Word32
  , tdDerivedKey :: !ByteString
  }
  deriving (Eq, Show)


parseTestDescription :: Object -> Parser TestDescription
parseTestDescription o = do
  let parseBase16Bytes = either fail pure . decode . encodeUtf8
  tdId <- o .: "tcId"
  tdPassword <- o .: "password" >>= parseBase16Bytes
  tdSalt <- o .: "salt" >>= parseBase16Bytes
  tdIterationCount <- o .: "iterationCount"
  tdDerivedLength <- o .: "dkLen"
  tdDerivedKey <- o .: "dk" >>= parseBase16Bytes
  pure
    TestDescription
      { tdId
      , tdPassword
      , tdSalt
      , tdIterationCount
      , tdDerivedKey
      , tdDerivedLength
      }