{-# LANGUAGE OverloadedStrings #-} module CryptoTest (tests, cbTests) where import System.IO import System.IO.Temp import System.Posix.IO import Control.Monad (when) import Control.Monad.Trans.Maybe import Control.Monad.IO.Class import Control.Monad.Catch import Control.Concurrent import Data.List (isInfixOf) import Data.ByteString.Char8 () import Data.Maybe ( fromJust ) import qualified Data.ByteString as BS import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase) import Test.Tasty.QuickCheck import Test.HUnit hiding (assert) import Test.QuickCheck.Monadic import Crypto.Gpgme import Crypto.Gpgme.Types ( GpgmeError (GpgmeError) ) import TestUtil tests :: TestTree tests = testGroup "crypto" [ testProperty "bobEncryptForAliceDecryptPromptNoCi" $ bobEncryptForAliceDecrypt False , testProperty "bobEncryptSignForAliceDecryptVerifyPromptNoCi" $ bobEncryptSignForAliceDecryptVerify False , testProperty "bobEncryptForAliceDecryptShortPromptNoCi" bobEncryptForAliceDecryptShort , testProperty "bobEncryptSignForAliceDecryptVerifyShortPromptNoCi" bobEncryptSignForAliceDecryptVerifyShort , testCase "decryptGarbage" decryptGarbage , testCase "encryptWrongKey" encryptWrongKey , testCase "bobEncryptSymmetricallyPromptNoCi" bobEncryptSymmetrically , testCase "bobDetachSignAndVerifySpecifyKeyPromptNoCi" bobDetachSignAndVerifySpecifyKeyPrompt , testCase "bobClearSignAndVerifySpecifyKeyPromptNoCi" bobClearSignAndVerifySpecifyKeyPrompt , testCase "bobClearSignAndVerifyDefaultKeyPromptNoCi" bobClearSignAndVerifyDefaultKeyPrompt , testCase "bobNormalSignAndVerifySpecifyKeyPromptNoCi" bobNormalSignAndVerifySpecifyKeyPrompt , testCase "bobNormalSignAndVerifyDefaultKeyPromptNoCi" bobNormalSignAndVerifyDefaultKeyPrompt , testCase "encryptFileNoCi" encryptFile , testCase "encryptStreamNoCi" encryptStream ] cbTests :: IO TestTree cbTests = do supported <- withCtx "test/bob" "C" OpenPGP $ \ctx -> return $ isPassphraseCbSupported ctx if supported then return $ testGroup "passphrase-cb" [ testProperty "bobEncryptForAliceDecrypt" $ bobEncryptForAliceDecrypt True , testProperty "bobEncryptSignForAliceDecryptVerifyWithPassphraseCbPromptNoCi" $ bobEncryptSignForAliceDecryptVerify True ] else return $ testGroup "passphrase-cb" [] hush :: Monad m => m (Either e a) -> MaybeT m a hush = MaybeT . fmap (either (const Nothing) Just) withPassphraseCb :: String -> Ctx -> IO () withPassphraseCb passphrase ctx = do setPassphraseCallback ctx (Just callback) where callback _ _ _ = return (Just passphrase) bobEncryptForAliceDecrypt :: Bool -> Plain -> Property bobEncryptForAliceDecrypt passphrCb plain = not (BS.null plain) ==> monadicIO $ do dec <- run encrAndDecr assert $ dec == plain where encrAndDecr = do -- encrypt Just enc <- withCtx "test/bob" "C" OpenPGP $ \bCtx -> runMaybeT $ do aPubKey <- MaybeT $ getKey bCtx alicePubFpr NoSecret hush $ encrypt bCtx [aPubKey] NoFlag plain -- decrypt dec <- withCtx "test/alice" "C" OpenPGP $ \aCtx -> do when passphrCb $ withPassphraseCb "alice123" aCtx decrypt aCtx enc return $ fromRight dec bobEncryptForAliceDecryptShort :: Plain -> Property bobEncryptForAliceDecryptShort plain = not (BS.null plain) ==> monadicIO $ do dec <- run encrAndDecr assert $ dec == plain where encrAndDecr = do -- encrypt enc <- encrypt' "test/bob" alicePubFpr plain -- decrypt dec <- decrypt' "test/alice" (fromRight enc) return $ fromRight dec bobEncryptSignForAliceDecryptVerify :: Bool -> Plain -> Property bobEncryptSignForAliceDecryptVerify passphrCb plain = not (BS.null plain) ==> monadicIO $ do dec <- run encrAndDecr assert $ dec == plain where encrAndDecr = do -- encrypt Just enc <- withCtx "test/bob" "C" OpenPGP $ \bCtx -> runMaybeT $ do aPubKey <- MaybeT $ getKey bCtx alicePubFpr NoSecret hush $ encryptSign bCtx [aPubKey] NoFlag plain -- decrypt dec <- withCtx "test/alice" "C" OpenPGP $ \aCtx -> do when passphrCb $ withPassphraseCb "alice123" aCtx decryptVerify aCtx enc return $ fromRight dec bobEncryptSignForAliceDecryptVerifyShort :: Plain -> Property bobEncryptSignForAliceDecryptVerifyShort plain = not (BS.null plain) ==> monadicIO $ do dec <- run encrAndDecr assert $ dec == plain where encrAndDecr = do -- encrypt enc <- encryptSign' "test/bob" alicePubFpr plain -- decrypt dec <- decryptVerify' "test/alice" (fromRight enc) return $ fromRight dec encryptWrongKey :: Assertion encryptWrongKey = do res <- encrypt' "test/bob" "INEXISTENT" "plaintext" assertBool "should fail" (isLeft res) let err = fromLeft res assertBool "should contain key" ("INEXISTENT" `isInfixOf` err) decryptGarbage :: Assertion decryptGarbage = do val <- withCtx "test/bob" "C" OpenPGP $ \bCtx -> decrypt bCtx (BS.pack [1,2,3,4,5,6]) isLeft val @? "should be left " ++ show val bobEncryptSymmetrically :: Assertion bobEncryptSymmetrically = do -- encrypt cipher <- fmap fromRight $ withCtx "test/bob" "C" OpenPGP $ \ctx -> encrypt ctx [] NoFlag "plaintext" assertBool "must not be plain" (cipher /= "plaintext") -- decrypt plain <- fmap fromRight $ withCtx "test/alice" "C" OpenPGP $ \ctx -> decrypt ctx cipher assertEqual "should decrypt to same" "plaintext" plain bobDetachSignAndVerifySpecifyKeyPrompt :: Assertion bobDetachSignAndVerifySpecifyKeyPrompt = do resVerify <- withCtx "test/bob/" "C" OpenPGP $ \ctx -> do key <- getKey ctx bobPubFpr NoSecret let msgToSign = "Clear text message from bob!!" resSign <-sign ctx [fromJust key] Detach msgToSign verifyDetached ctx (fromRight resSign) msgToSign assertBool "Could not verify bob's signature was correct" $ isVerifyDetachValid resVerify bobClearSignAndVerifySpecifyKeyPrompt :: Assertion bobClearSignAndVerifySpecifyKeyPrompt = do resVerify <- withCtx "test/bob/" "C" OpenPGP $ \ctx -> do key <- getKey ctx bobPubFpr NoSecret resSign <- sign ctx [fromJust key] Clear "Clear text message from bob specifying signing key" verifyPlain ctx (fromRight resSign) "" assertBool "Could not verify bob's signature was correct" $ isVerifyValid resVerify bobClearSignAndVerifyDefaultKeyPrompt :: Assertion bobClearSignAndVerifyDefaultKeyPrompt = do resVerify <- withCtx "test/bob/" "C" OpenPGP $ \ctx -> do resSign <- sign ctx [] Clear "Clear text message from bob with default key" verifyPlain ctx (fromRight resSign) "" assertBool "Could not verify bob's signature was correct" $ isVerifyValid resVerify bobNormalSignAndVerifySpecifyKeyPrompt :: Assertion bobNormalSignAndVerifySpecifyKeyPrompt = do resVerify <- withCtx "test/bob/" "C" OpenPGP $ \ctx -> do key <- getKey ctx bobPubFpr NoSecret resSign <- sign ctx [fromJust key] Normal "Normal text message from bob specifying signing key" verify ctx (fromRight resSign) assertBool "Could not verify bob's signature was correct" $ isVerifyValid resVerify bobNormalSignAndVerifyDefaultKeyPrompt :: Assertion bobNormalSignAndVerifyDefaultKeyPrompt = do resVerify <- withCtx "test/bob/" "C" OpenPGP $ \ctx -> do resSign <- sign ctx [] Normal "Normal text message from bob with default key" verify ctx (fromRight resSign) assertBool "Could not verify bob's signature was correct" $ isVerifyValid resVerify encryptFile :: Assertion encryptFile = withCtx "test/bob/" "C" OpenPGP $ \ctx -> do withPassphraseCb "bob123" ctx withTestTmpFiles $ \pp ph cp ch dp dh -> do plainFd <- handleToFd ph cipherFd <- handleToFd ch decryptedFd <- handleToFd dh key <- getKey ctx bobPubFpr NoSecret -- Add plaintext content writeFile pp "Plaintext contents. 1234go!" -- Encrypt plaintext resEnc <- encryptFd ctx [fromJust key] NoFlag plainFd cipherFd if resEnc == Right () then return () else assertFailure $ show resEnc -- Recreate the cipher FD because it is closed (or something) from the encrypt command cipherHandle' <- openFile cp ReadWriteMode cipherFd' <- handleToFd cipherHandle' -- Decrypt ciphertext resDec <- decryptFd ctx cipherFd' decryptedFd if resDec == Right () then return () else assertFailure $ show resDec -- Compare plaintext and decrypted text plaintext <- readFile pp decryptedtext <- readFile dp plaintext @=? decryptedtext -- Encrypt from FD pipe into a FD file encryptStream :: Assertion encryptStream = withCtx "test/bob/" "C" OpenPGP $ \ctx -> do withPassphraseCb "bob123" ctx withTestTmpFiles $ \_ _ cp ch dp dh -> do cipherFd <- handleToFd ch decryptedFd <- handleToFd dh -- Use bob's key key <- getKey ctx bobPubFpr NoSecret -- Create pipe (pipeRead, pipeWrite) <- createPipe -- Write to pipe -- Add plaintext content let testString = replicate 1000 '.' _ <- forkIO $ do threadWaitWrite pipeWrite _ <- fdWrite pipeWrite testString closeFd pipeWrite -- Start encrypting in thread _ <- forkIO $ do threadWaitRead pipeRead _ <- encryptFd ctx [fromJust key] NoFlag pipeRead cipherFd closeFd pipeRead -- Wait a second for threads to finish threadDelay (1000 * 1000) -- Check result -- Recreate the cipher FD because it is closed (or something) from the encrypt command threadWaitRead cipherFd ch' <- openFile cp ReadWriteMode cipherFd' <- handleToFd ch' -- Decrypt ciphertext resDec <- decryptFd ctx cipherFd' decryptedFd if resDec == Right () then return () else assertFailure $ show resDec -- Compare plaintext and decrypted text decryptedtext <-readFile dp testString @=? decryptedtext withTestTmpFiles :: (MonadIO m, MonadMask m) => ( FilePath -> Handle -- Plaintext -> FilePath -> Handle -- Ciphertext -> FilePath -> Handle -- Decrypted text -> m a) -> m a withTestTmpFiles f = withSystemTempFile "plain" $ \pp ph -> withSystemTempFile "cipher" $ \cp ch -> withSystemTempFile "decrypt" $ \dp dh -> f pp ph cp ch dp dh -- Verify that the signature verification is successful isVerifyValid :: Either t ([(GpgmeError, [SignatureSummary], t1)], t2) -> Bool isVerifyValid (Right ([v], _)) = isVerifyValid' v isVerifyValid (Right (v:vs, t)) = isVerifyValid' v && isVerifyValid (Right (vs,t)) isVerifyValid _ = False isVerifyValid' :: (GpgmeError, [SignatureSummary], t) -> Bool isVerifyValid' (GpgmeError 0, [Green,Valid], _) = True isVerifyValid' _ = False -- Verify that the signature verification is successful for verifyDetach isVerifyDetachValid :: Either t [(GpgmeError, [SignatureSummary], t1)] -> Bool isVerifyDetachValid (Right [v]) = isVerifyDetachValid' v isVerifyDetachValid (Right ((v:vs))) = isVerifyDetachValid' v && isVerifyDetachValid (Right vs) isVerifyDetachValid _ = False isVerifyDetachValid' :: (GpgmeError, [SignatureSummary], t) -> Bool isVerifyDetachValid' (GpgmeError 0, [Green,Valid], _) = True isVerifyDetachValid' _ = False