packages feed

age-0.0.1.0: test/Test/Crypto/Age/Conduit.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}

module Test.Crypto.Age.Conduit
  ( tests
  ) where

import Crypto.Age.Conduit
  ( RecipientEncryptionParams (..)
  , conduitEncryptEitherPure
  , decryptPayloadChunk
  , encryptPayloadChunk
  , sinkDecryptEither
  )
import Crypto.Age.Identity
  ( Identity (..), ScryptIdentity (..), toX25519Recipient )
import Crypto.Age.Recipient ( Recipients (..), ScryptRecipient (..) )
import Crypto.Age.Scrypt ( WorkFactor (..) )
import qualified Data.ByteString as BS
import qualified Data.Conduit as C
import qualified Data.Conduit.List as CL
import qualified Data.List.NonEmpty as NE
import Hedgehog
  ( Gen
  , Property
  , annotateShow
  , checkParallel
  , discover
  , forAll
  , forAllWith
  , property
  , tripping
  , (===)
  )
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Prelude
import Test.Crypto.Age.Identity.Gen ( genScryptIdentity, genX25519Identity )
import Test.Crypto.Age.Key.Gen ( genFileKey, genPayloadKey, genPayloadKeyNonce )
import Test.Crypto.Age.Key.Render
  ( unsafeRenderFileKey, unsafeRenderPayloadKey )
import Test.Crypto.Age.Payload.Counter.Gen ( genPayloadChunkCounter )
import Test.Crypto.Age.Payload.Plaintext.Gen ( genPlaintextPayloadChunk )
import Test.Crypto.Age.Recipient.Gen ( genX25519Recipients )
import Test.Crypto.Age.Scrypt.Gen ( genSalt, genWorkFactorInRange )

tests :: IO Bool
tests = checkParallel $$(discover)

------------------------------------------------------------------------------
-- Properties
------------------------------------------------------------------------------

-- | Test that 'encryptPayloadChunk' and 'decryptPayloadChunk' round trip.
prop_roundTrip_encryptDecryptPayloadChunk :: Property
prop_roundTrip_encryptDecryptPayloadChunk = property $ do
  payloadKey <- forAllWith unsafeRenderPayloadKey genPayloadKey
  counter <- forAll genPayloadChunkCounter
  plaintext <- forAll genPlaintextPayloadChunk
  tripping
    plaintext
    (encryptPayloadChunk payloadKey counter)
    (decryptPayloadChunk payloadKey counter)

-- | Test that 'conduitEncryptEitherPure' (pure variant of 'conduitEncrypt')
-- and 'conduitDecryptEither' round trip.
prop_roundTrip_conduitEncryptDecrypt :: Property
prop_roundTrip_conduitEncryptDecrypt = property $ do
  (senderId, recipients) <- forAll genSenderIdentityAndRecipients
  recipientParams <- forAll (genRecipientEncryptionParams recipients)
  fileKey <- forAllWith unsafeRenderFileKey genFileKey
  payloadKeyNonce <- forAll genPayloadKeyNonce
  expectedPlaintext <- forAll $ Gen.maybe $ Gen.bytes (Range.constant 0 1024)
  let sourcePlaintext =
        case expectedPlaintext of
          Just bs -> C.yield bs
          Nothing ->
            -- Testing the case where no plaintext is streamed.
            CL.sourceNull

  let ciphertextRes =
        C.runConduitPure $
          sourcePlaintext
            C..| sinkEncryptPure recipientParams fileKey payloadKeyNonce
  annotateShow ciphertextRes
  ciphertext <-
    case ciphertextRes of
      Left err -> fail $ "failed to encrypt plaintext: " <> show err
      Right c -> pure c

  let actualPlaintextRes =
        C.runConduitPure $
          C.yield ciphertext
            C..| sinkDecryptEither (NE.singleton senderId)
  actualPlaintext <-
    case actualPlaintextRes of
      Left err -> fail $ "failed to decrypt ciphertext: " <> show err
      Right p -> pure p

  case expectedPlaintext of
    Just bs -> bs === actualPlaintext
    Nothing ->
      -- In the case where we didn't stream any plaintext, we should've still
      -- created a valid age file where an empty byte string was encrypted.
      BS.empty === actualPlaintext
  where
    sinkEncryptPure recipientParams fileKey payloadKeyNonce =
      conduitEncryptEitherPure recipientParams fileKey payloadKeyNonce
        C..| go mempty
      where
        go acc = C.await >>= \case
          Nothing -> pure (Right acc)
          Just (Left err) -> pure (Left err)
          Just (Right bs) -> go (acc <> bs)

------------------------------------------------------------------------------
-- Generators
------------------------------------------------------------------------------

genSenderIdentityAndRecipients :: Gen (Identity, Recipients)
genSenderIdentityAndRecipients =
  Gen.choice
    [ genSenderScryptIdentityAndRecipients
    , genSenderX25519IdentityAndRecipients
    ]
  where
    genSenderScryptIdentityAndRecipients :: Gen (Identity, Recipients)
    genSenderScryptIdentityAndRecipients = do
      identity@ScryptIdentity
        { siPassphrase
        , siMaxWorkFactor = WorkFactor maxWorkFactor
        } <- genScryptIdentity
      salt <- genSalt
      workFactor <- genWorkFactorInRange (Range.constant 1 maxWorkFactor)
      let recipient =
            ScryptRecipient
              { srPassphrase = siPassphrase
              , srSalt = salt
              , srWorkFactor = workFactor
              }
      pure (IdentityScrypt identity, RecipientsScrypt recipient)

    genSenderX25519IdentityAndRecipients :: Gen (Identity, Recipients)
    genSenderX25519IdentityAndRecipients = do
      senderId <- genX25519Identity
      let senderRecip = toX25519Recipient senderId
      moreRecipients <- genX25519Recipients 10
      pure (IdentityX25519 senderId, RecipientsX25519 (NE.singleton senderRecip <> moreRecipients))

genRecipientEncryptionParams :: Recipients -> Gen RecipientEncryptionParams
genRecipientEncryptionParams recipients =
  case recipients of
    RecipientsScrypt r -> pure (RecipientEncryptionParamsScrypt r)
    RecipientsX25519 rs ->
      RecipientEncryptionParamsX25519
        <$> mapM (\r -> (,) r <$> genX25519Identity) rs