age-0.0.1.0: test/Test/Crypto/Age/TestVector/Property.hs
module Test.Crypto.Age.TestVector.Property
( mkTestVectorProperties
) where
import Conduit ( ResourceT )
import Control.Monad ( filterM )
import Control.Monad.Except ( ExceptT, withExceptT )
import Crypto.Age.Armor ( UnarmorError (..), conduitUnarmor )
import Crypto.Age.Conduit
( DecryptError (..), DecryptPayloadError (..), sinkDecrypt )
import qualified Crypto.Hash as Crypto
import Data.ByteString ( ByteString )
import Data.Conduit
( ConduitT, awaitForever, transPipe, yield, ($$+), ($$+-), (.|) )
import Data.Conduit.Attoparsec ( sinkParser )
import qualified Data.Conduit.Combinators as C
import Data.Conduit.Lift ( runExceptC )
import qualified Data.Conduit.Zlib as Zlib
import Data.Foldable ( for_ )
import Data.List ( sort )
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import Hedgehog ( PropertyT, footnote, (===) )
import Prelude
import System.Directory ( doesFileExist, listDirectory )
import Test.Crypto.Age.TestVector.Header
( Compressed (..), Expect (..), Header (..), headerParser )
testVectorDirectory :: FilePath
testVectorDirectory = "test/test-vectors"
getTestVectorFileNames :: IO [FilePath]
getTestVectorFileNames = do
fileNames <- sort <$> listDirectory testVectorDirectory
flip filterM fileNames $ \fileName ->
doesFileExist $ concat [testVectorDirectory, "/", fileName]
data UnarmorOrDecryptError
= UnarmorOrDecryptUnarmorError !UnarmorError
| UnarmorOrDecryptDecryptError !DecryptError
deriving stock (Show)
mkTestVectorProperty :: FilePath -> (String, PropertyT (ResourceT IO) ())
mkTestVectorProperty fileName = (fileName, prop)
where
conduitUnarmorIfExpected :: Monad m => Bool -> ConduitT ByteString ByteString (ExceptT UnarmorError m) ()
conduitUnarmorIfExpected isArmored
| isArmored = conduitUnarmor
| otherwise = awaitForever yield
prop :: PropertyT (ResourceT IO) ()
prop = do
let path = concat [testVectorDirectory, "/", fileName]
(sealedSrc, header) <- C.sourceFile path $$+ sinkParser headerParser
let Header
{ hExpect
, hCompressed
, hPayload
, hIdentity
, hArmored
, hComment
} = header
for_ hComment (\comment -> footnote . T.unpack $ "test comment: " <> comment)
let conduitDecompress =
case hCompressed of
Just CompressedGzip -> Zlib.ungzip
Just CompressedZlib -> Zlib.decompress Zlib.defaultWindowBits
Nothing -> awaitForever yield
res <-
sealedSrc
$$+-
( runExceptC $
conduitDecompress
.| transPipe (withExceptT UnarmorOrDecryptUnarmorError) (conduitUnarmorIfExpected hArmored)
.| transPipe (withExceptT UnarmorOrDecryptDecryptError) (sinkDecrypt (NE.singleton hIdentity))
)
case (hExpect, res) of
(ExpectSuccess, Right actualPayload) ->
case hPayload of
Just expectedPayloadDigest -> expectedPayloadDigest === Crypto.hash actualPayload
Nothing -> fail "expected payload field in test vector file header"
(ExpectNoMatch, Left (UnarmorOrDecryptDecryptError DecryptNoMatchingRecipientError)) -> pure ()
(ExpectHmacFailure, Left (UnarmorOrDecryptDecryptError (DecryptInvalidHeaderMacError _ _))) -> pure ()
(ExpectHeaderFailure, Left (UnarmorOrDecryptDecryptError (DecryptHeaderParseError _))) -> pure ()
(ExpectHeaderFailure, Left (UnarmorOrDecryptDecryptError DecryptScryptStanzaNotAloneError)) -> pure ()
(ExpectHeaderFailure, Left (UnarmorOrDecryptDecryptError (DecryptUnwrapStanzaError _))) -> pure ()
(ExpectHeaderFailure, Left (UnarmorOrDecryptDecryptError (DecryptDecryptPayloadError (DecryptPayloadKeyNonceParseError _)))) ->
-- This is considered a header failure according to some of the test
-- vectors; which is a bit odd since the payload nonce is explicitly
-- described as being part of the payload:
--
-- > The binary payload... It begins with a 16-byte nonce generated
-- > by the sender...
--
-- https://github.com/C2SP/C2SP/blob/03ab74455beb3a6d6e0fb7dd1de5a932e2257cd0/age.md#payload
pure ()
(ExpectHeaderFailure, Left (UnarmorOrDecryptDecryptError DecryptNoMatchingRecipientError))
| fileName == "scrypt_work_factor_23.test" ->
-- In our implementation, the maximum work factor value permitted
-- is 64, but this test vector expects us to fail in the event that
-- it's 23.
--
-- So, in order to appease this test vector, we set the maximum
-- work factor for 'hIdentity' to 22 in the scrypt case. However,
-- in our implementation, this results in a \"no match\" error and
-- not a \"header failure\" error as is expected by the test
-- vector. So we make an exception for this particular case.
pure ()
(ExpectPayloadFailure, Left (UnarmorOrDecryptDecryptError (DecryptDecryptPayloadError _))) -> pure ()
(ExpectArmorFailure, Left (UnarmorOrDecryptUnarmorError _)) -> pure ()
(_, Left err) -> fail $ "expected " <> show hExpect <> " but got the result: " <> show err
(_, Right _) -> fail $ "expected " <> show hExpect <> " but got a success result"
mkTestVectorProperties :: IO [(String, PropertyT (ResourceT IO) ())]
mkTestVectorProperties = map mkTestVectorProperty <$> getTestVectorFileNames