packages feed

h-gpgme 0.6.2.0 → 0.6.3.0

raw patch · 11 files changed

+303/−71 lines, 11 filesdep ~HUnitdep ~QuickCheckdep ~base

Dependency ranges changed: HUnit, QuickCheck, base, bindings-gpgme, bytestring, data-default, directory, email-validate, exceptions, filepath, tasty, tasty-hunit, tasty-quickcheck, temporary, time, transformers, unix

Files

CHANGELOG.markdown view
@@ -1,5 +1,27 @@ # Changelog +## 0.6.3.0++### New Features++- feat: add importKeyFromBytes function: https://github.com/rethab/h-gpgme/pull/66+- feat: add exportKey, exportSecretKey and exportKeys functions: https://github.com/rethab/h-gpgme/pull/70+- feat: support gpgme 2.x by allowing bindings-gpgme 0.2: https://github.com/rethab/h-gpgme/pull/69++### Bug fixes++- fix: release the gpgme data buffer after import and export operations: https://github.com/rethab/h-gpgme/pull/70+- fix: importKeyFromFile and importKeyFromBytes reported success instead of the actual error when the import operation failed: https://github.com/rethab/h-gpgme/pull/70+- docs: fix reversed doc comments on IncludeSecret: https://github.com/rethab/h-gpgme/pull/68+- fix(test): skip gpg-agent sockets when copying the key fixture, which broke removeAliceKey wherever GnuPG puts its sockets in the homedir: https://github.com/rethab/h-gpgme/pull/69++### Maintenance++- chore(ci): modernize CI with a real multi-GHC matrix, latest actions and hardened permissions: https://github.com/rethab/h-gpgme/pull/67+- chore(ci): release to Hackage from a version tag: https://github.com/rethab/h-gpgme/pull/69+- chore(ci): test against gpgme 1.x and 2.x, both built from source: https://github.com/rethab/h-gpgme/pull/69+- chore: give every dependency a PVP upper bound: https://github.com/rethab/h-gpgme/pull/69+ ## 0.6.2.0  ### New Features
h-gpgme.cabal view
@@ -1,5 +1,5 @@ Name:                h-gpgme-Version:             0.6.2.0+Version:             0.6.3.0 Description:         High Level Binding for GnuPG Made Easy (gpgme): A Haskell API for the gpgme C library. Synopsis:            High Level Binding for GnuPG Made Easy (gpgme) License:             MIT@@ -9,9 +9,12 @@ Copyright:           (c) Reto 2022 Homepage:            https://github.com/rethab/h-gpgme Bug-reports:         https://github.com/rethab/h-gpgme/issues-Tested-With:           GHC==8.8-                     , GHC==9.0-                     , GHC==9.2+Tested-With:           GHC==8.10.7+                     , GHC==9.2.8+                     , GHC==9.4.8+                     , GHC==9.6.6+                     , GHC==9.8.4+                     , GHC==9.10.3 Category:            Cryptography Build-Type:          Simple Cabal-Version:       >=1.10@@ -36,15 +39,14 @@                      , Crypto.Gpgme.Crypto                      , Crypto.Gpgme.Internal                      , Crypto.Gpgme.Types-  build-depends:       base           == 4.*-                     , bindings-gpgme >= 0.1.8 && <0.2-                     , bytestring     >= 0.9-                     , transformers   >= 0.4.1 && <0.6-                     , time           >= 1.4 && <2.0-                     , unix           >= 2.5-                     , email-validate-                     , time-                     , data-default+  build-depends:       base           >= 4.14 && <5+                     , bindings-gpgme >= 0.1.8 && <0.3+                     , bytestring     >= 0.10 && <0.13+                     , transformers   >= 0.4.1 && <0.7+                     , time           >= 1.4 && <2+                     , unix           >= 2.5 && <2.9+                     , email-validate >= 2.0 && <2.4+                     , data-default   >= 0.5 && <0.9   default-language:    Haskell2010  test-suite tests@@ -53,24 +55,24 @@   ghc-options:         -Wall   hs-source-dirs:      src, test   main-is:             Main.hs-  build-depends:       base           == 4.*-                     , bindings-gpgme >= 0.1.6 && <0.2-                     , bytestring     >= 0.9-                     , transformers   >= 0.4.1 && <0.6-                     , time           >= 1.4 && <2.0-                     , unix           >= 2.5-                     , directory-                     , filepath-                     , email-validate-                     , data-default-                     , temporary-                     , exceptions+  build-depends:       base           >= 4.14 && <5+                     , bindings-gpgme >= 0.1.8 && <0.3+                     , bytestring     >= 0.10 && <0.13+                     , transformers   >= 0.4.1 && <0.7+                     , time           >= 1.4 && <2+                     , unix           >= 2.5 && <2.9+                     , email-validate >= 2.0 && <2.4+                     , data-default   >= 0.5 && <0.9+                     , directory      >= 1.2 && <2+                     , filepath       >= 1.3 && <2+                     , temporary      >= 1.2 && <2+                     , exceptions     >= 0.8 && <0.11 -                     , HUnit-                     , tasty-                     , tasty-quickcheck-                     , tasty-hunit-                     , QuickCheck                 +                     , HUnit          >= 1.3 && <2+                     , tasty          >= 1.0 && <2+                     , tasty-quickcheck >= 0.9 && <0.12+                     , tasty-hunit    >= 0.10 && <0.11+                     , QuickCheck     >= 2.10 && <3   other-modules:       Crypto.Gpgme                      , Crypto.Gpgme.Crypto                      , Crypto.Gpgme.Ctx
src/Crypto/Gpgme.hs view
@@ -50,12 +50,18 @@     -- * Keys     , Key     , importKeyFromFile+    , importKeyFromBytes     , getKey     , listKeys     , removeKey     , RemoveKeyFlags(..)      , searchKeys+    -- * Exporting keys+    , exportKey+    , exportSecretKey+    , exportKeys+    , ExportMode(..)     -- * Information about keys     , Validity (..)     , PubKeyAlgo (..)
src/Crypto/Gpgme/Internal.hs view
@@ -94,6 +94,17 @@ fromKeyListingMode KeyListingValidate     = c'GPGME_KEYLIST_MODE_VALIDATE  +-- The GPGME_EXPORT_MODE_* values are replicated from gpgme.h:+-- bindings-gpgme 0.1 does not bind these constants and 0.2 only+-- binds the ones known to the gpgme version it was built against.+fromExportMode :: ExportMode -> CUInt+fromExportMode ExportMinimal      = 4   -- GPGME_EXPORT_MODE_MINIMAL+fromExportMode ExportSecret       = 16  -- GPGME_EXPORT_MODE_SECRET+fromExportMode ExportRaw          = 32  -- GPGME_EXPORT_MODE_RAW+fromExportMode ExportPKCS12       = 64  -- GPGME_EXPORT_MODE_PKCS12+fromExportMode ExportSSH          = 256 -- GPGME_EXPORT_MODE_SSH+fromExportMode ExportSecretSubkey = 512 -- GPGME_EXPORT_MODE_SECRET_SUBKEY+ fromProtocol :: (Num a) => Protocol -> a fromProtocol CMS     =  c'GPGME_PROTOCOL_CMS fromProtocol GPGCONF =  c'GPGME_PROTOCOL_GPGCONF
src/Crypto/Gpgme/Key.hs view
@@ -1,9 +1,15 @@ module Crypto.Gpgme.Key (       getKey     , importKeyFromFile+    , importKeyFromBytes     , listKeys     , removeKey     , searchKeys+      -- * Exporting keys+    , exportKey+    , exportSecretKey+    , exportKeys+    , ExportMode (..)       -- * Information about keys     , Validity (..)     , PubKeyAlgo (..)@@ -18,6 +24,7 @@     ) where  import Bindings.Gpgme+import Control.Monad (when) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BSC8 import Data.Time.Clock@@ -31,13 +38,13 @@  -- | Returns a list of all known 'Key's from the @context@. listKeys :: Ctx            -- ^ context to operate in-         -> IncludeSecret  -- ^ whether to include the secrets+         -> IncludeSecret  -- ^ whether to restrict to secret keys          -> IO [Key] listKeys ctx secret = listKeys' ctx secret nullPtr  -- | Returns a list of known 'Key's from the @context@ that match a given pattern. searchKeys :: Ctx            -- ^ context to operate in-           -> IncludeSecret  -- ^ whether to include the secrets+           -> IncludeSecret  -- ^ whether to restrict to secret keys            -> String         -- ^ The pattern to look for; It is typically                              -- matched against the user ids of a key.            -> IO [Key]@@ -45,7 +52,7 @@  -- | Internal helper function used by both `listKeys` and `searchKeys`. listKeys' :: Ctx            -- ^ context to operate in-          -> IncludeSecret  -- ^ whether to include the secrets+          -> IncludeSecret  -- ^ whether to restrict to secret keys           -> CString        -- ^ The pattern to look for; It is typically                             -- matched against the user ids of a key.           -> IO [Key]@@ -68,7 +75,7 @@ --   Returns 'Nothing' if no 'Key' with this 'Fpr' exists. getKey :: Ctx           -- ^ context to operate in        -> Fpr           -- ^ fingerprint-       -> IncludeSecret -- ^ whether to include secrets when searching for the key+       -> IncludeSecret -- ^ whether to look for a secret key        -> IO (Maybe Key) getKey Ctx {_ctx=ctxPtr} fpr secret = do     key <- allocKey@@ -85,11 +92,38 @@ importKeyFromFile :: Ctx -- ^ context to operate in                   -> FilePath -- ^ file path to read from                   -> IO (Maybe GpgmeError)-importKeyFromFile Ctx {_ctx=ctxPtr} fp = do-  dataPtr <- newDataBuffer-  ret <-+importKeyFromFile ctx fp =+  importData ctx $ \dataPtr ->     BS.useAsCString (BSC8.pack fp) $ \cFp ->       c'gpgme_data_new_from_file dataPtr cFp 1++-- | Import a key from a 'BS.ByteString', this happens in two steps: populate a+-- @gpgme_data_t@ with the contents of the buffer, import the @gpgme_data_t@+importKeyFromBytes :: Ctx -- ^ context to operate in+                   -> BS.ByteString -- ^ buffer to read the key from+                   -> IO (Maybe GpgmeError)+importKeyFromBytes ctx key =+  importData ctx $ \dataPtr ->+    BS.useAsCString key $ \cKey -> do+      -- gpgme must copy the buffer: the import in 'importData' runs after+      -- useAsCString returns, at which point cKey has been freed+      let copyData = 1+      let keylen = fromIntegral (BS.length key)+      c'gpgme_data_new_from_mem dataPtr cKey keylen copyData++-- | Populate a fresh @gpgme_data_t@ using the given action and import it.+-- The action is responsible for filling the buffer and returns the result+-- of doing so.+importData :: Ctx -- ^ context to operate in+           -> (Ptr C'gpgme_data_t -> IO C'gpgme_error_t) -- ^ populate the buffer+           -> IO (Maybe GpgmeError)+importData Ctx {_ctx=ctxPtr} populate = do+  -- the populate action allocates the data object itself, so it only+  -- needs an empty cell; it is initialized to NULL because gpgme+  -- leaves it untouched if populating fails+  dataPtr <- malloc+  poke dataPtr nullData+  ret <- populate dataPtr   mGpgErr <-     case ret of       x | x == noError -> do@@ -99,10 +133,64 @@           c'gpgme_op_import ctx dat         pure $ if retIn == noError           then Nothing-          else Just $ GpgmeError ret+          else Just $ GpgmeError retIn       err -> pure $ Just $ GpgmeError err+  dat <- peek dataPtr+  when (dat /= nullData) $ c'gpgme_data_release dat   free dataPtr   pure mGpgErr+  where+    -- gpgme_data_t is bound as an integral type, so NULL is 0+    nullData = 0 :: C'gpgme_data_t++-- | Export the public key with the given @fingerprint@ from the+--   @context@. Returns an empty 'BS.ByteString' if no key with+--   this 'Fpr' exists.+exportKey :: Ctx -- ^ context to operate in+          -> Fpr -- ^ fingerprint of the key to export+          -> IO (Either GpgmeError BS.ByteString)+exportKey ctx fpr = exportKeys ctx [] [fpr]++-- | Export the secret key with the given @fingerprint@ from the+--   @context@.+--+--   Exporting a protected secret key requires its passphrase, which+--   may be supplied through @setPassphraseCallback@.+exportSecretKey :: Ctx -- ^ context to operate in+                -> Fpr -- ^ fingerprint of the key to export+                -> IO (Either GpgmeError BS.ByteString)+exportSecretKey ctx fpr = exportKeys ctx [ExportSecret] [fpr]++-- | Export all keys matching the given @fingerprints@ from the+--   @context@, or all keys if no fingerprint is given.+--+--   The keys are returned in armored format if armor has been+--   enabled on the @context@ (see @setArmor@) and in binary+--   format otherwise.+exportKeys :: Ctx          -- ^ context to operate in+           -> [ExportMode] -- ^ modes for the export+           -> [Fpr]        -- ^ fingerprints of the keys to export,+                           --   or empty to export all keys+           -> IO (Either GpgmeError BS.ByteString)+exportKeys Ctx {_ctx=ctxPtr} modes fprs = do+    dataPtr <- newDataBuffer+    dat <- peek dataPtr+    ret <- withMany BS.useAsCString fprs $ \cFprs ->+        withArray0 nullPtr cFprs $ \pats -> do+            ctx <- peek ctxPtr+            c'gpgme_op_export_ext ctx pats cMode dat+    result <- if ret == noError+        then do+            -- collectResult is lazy: force the copy while the buffer+            -- it reads from is still alive+            let key = collectResult dat+            key `seq` return (Right key)+        else return (Left (GpgmeError ret))+    c'gpgme_data_release dat+    free dataPtr+    return result+  where+    cMode = foldl (\memo -> (memo .|.) . fromExportMode) 0 modes  -- | Removes the 'Key' from @context@ removeKey :: Ctx                    -- ^ context to operate in
src/Crypto/Gpgme/Types.hs view
@@ -34,6 +34,17 @@     | KeyListingSigNotations     | KeyListingValidate +-- | Modes for exporting keys. Multiple modes can be combined,+--   e.g. @[ExportSecret, ExportPKCS12]@.+data ExportMode+    = ExportMinimal       -- ^ Strip all signatures except the most recent self-signatures+    | ExportSecret        -- ^ Export the secret keys instead of the public keys+    | ExportRaw           -- ^ Only with 'ExportSecret' and 'CMS': export in raw format+    | ExportPKCS12        -- ^ Only with 'ExportSecret' and 'CMS': export in PKCS#12 format+    | ExportSSH           -- ^ Export the public key in SSH format+    | ExportSecretSubkey  -- ^ Export the secret subkeys instead of the public keys+    deriving (Show, Eq, Ord)+ -- | Modes for signing with GPG data SignMode = Normal | Detach | Clear deriving Show @@ -103,10 +114,10 @@ withKeyPtr :: Key -> (Ptr C'gpgme_key_t -> IO a) -> IO a withKeyPtr (Key fPtr) = withForeignPtr fPtr --- | Whether to include secret keys when searching+-- | Whether to restrict an operation to secret keys data IncludeSecret =-      WithSecret -- ^ do not include secret keys-    | NoSecret   -- ^ include secret keys+      WithSecret -- ^ only consider secret keys+    | NoSecret   -- ^ do not consider secret keys     deriving (Show, Eq, Ord)  data Flag =
test/CryptoTest.hs view
@@ -176,14 +176,14 @@   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) ""+    verify 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) ""+    verify ctx (fromRight resSign)   assertBool "Could not verify bob's signature was correct" $ isVerifyValid resVerify  bobNormalSignAndVerifySpecifyKeyPrompt :: Assertion
test/KeyGenTest.hs view
@@ -18,6 +18,7 @@ import Data.Time.Clock import Data.Default import Data.List          ( isPrefixOf )+import Data.Maybe         ( fromJust ) import Data.ByteString.Char8    ( unpack )  import            Crypto.Gpgme@@ -118,7 +119,7 @@ -- Other ExpireDate to string possibilities expireDateDays :: Assertion expireDateDays =-  let (Just p) = G.toPositive 10+  let p = fromJust $ G.toPositive 10       params = (def :: G.GenKeyParams) {           G.expireDate = Just $ G.ExpireD p         }@@ -130,7 +131,7 @@  expireDateWeeks :: Assertion expireDateWeeks =-  let (Just p) = G.toPositive 10+  let p = fromJust $ G.toPositive 10       params = (def :: G.GenKeyParams) {           G.expireDate = Just $ G.ExpireW p         }@@ -142,7 +143,7 @@  expireDateMonths :: Assertion expireDateMonths =-  let (Just p) = G.toPositive 10+  let p = fromJust $ G.toPositive 10       params = (def :: G.GenKeyParams) {           G.expireDate = Just $ G.ExpireM p         }@@ -154,7 +155,7 @@  expireDateYears :: Assertion expireDateYears =-  let (Just p) = G.toPositive 10+  let p = fromJust $ G.toPositive 10       params = (def :: G.GenKeyParams) {           G.expireDate = Just $ G.ExpireY p         }@@ -166,7 +167,7 @@  expireDateSeconds :: Assertion expireDateSeconds =-  let (Just p) = G.toPositive 123456+  let p = fromJust $ G.toPositive 123456       params = (def :: G.GenKeyParams) {           G.expireDate = Just $ G.ExpireS p         }@@ -178,7 +179,7 @@  creationDateSeconds :: Assertion creationDateSeconds =-  let (Just p) = G.toPositive 123456+  let p = fromJust $ G.toPositive 123456       params = (def :: G.GenKeyParams) {           G.creationDate = Just $ G.CreationS p         }
test/KeyTest.hs view
@@ -1,18 +1,20 @@ {-# LANGUAGE OverloadedStrings #-}-module KeyTest (tests) where+module KeyTest (tests, cbTests) where +import qualified Data.ByteString as BS import Data.Maybe-import Data.List-import Test.Tasty (TestTree)+import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase) import Test.HUnit +import Control.Monad      (filterM) import System.FilePath    ((</>)) import System.Directory   ( removeDirectoryRecursive                           , createDirectory                           , listDirectory                           , copyFile                           )+import System.Posix.Files (getFileStatus, isRegularFile)  import Crypto.Gpgme import TestUtil@@ -29,8 +31,23 @@         , testCase "removeAliceKey" removeAliceKey         , testCase "readFromFileWorks" readFromFileWorks         , testCase "readFromFileDoesn'tExist" readFromFileDoesn'tExist+        , testCase "readFromBytesWorks" readFromBytesWorks+        , testCase "exportAlicePubArmored" exportAlicePubArmored+        , testCase "exportImportRoundtrip" exportImportRoundtrip+        , testCase "exportInexistentIsEmpty" exportInexistentIsEmpty+        , testCase "exportAllKeys" exportAllKeys+        , testCase "exportMinimal" exportMinimal         ] +cbTests :: IO TestTree+cbTests = do+    supported <- withCtx "test/alice" "C" OpenPGP $ \ctx ->+        return $ isPassphraseCbSupported ctx+    if supported+       then return $ testGroup "key-passphrase-cb"+                [ testCase "exportAliceSecretArmored" exportAliceSecretArmored ]+       else return $ testGroup "key-passphrase-cb" []+ getAlicePubFromAlice :: Assertion getAlicePubFromAlice = do     withCtx "test/alice" "C" OpenPGP $ \ctx ->@@ -77,27 +94,29 @@ checkAlicePubUserIds = do     withCtx "test/alice" "C" OpenPGP $ \ctx ->         do Just key <- getKey ctx alicePubFpr NoSecret-           let uids = keyUserIds key-           length uids @?= 1-           let kuid = head uids-               uid = keyuserId kuid-           keyuserValidity kuid @?= ValidityUltimate-           userId uid @?= "Alice (Test User A) <alice@email.com>"-           userName uid @?= "Alice"-           userEmail uid @?= "alice@email.com"-           userComment uid @?= "Test User A"+           case keyUserIds key of+             [kuid] -> do+               let uid = keyuserId kuid+               keyuserValidity kuid @?= ValidityUltimate+               userId uid @?= "Alice (Test User A) <alice@email.com>"+               userName uid @?= "Alice"+               userEmail uid @?= "alice@email.com"+               userComment uid @?= "Test User A"+             uids -> assertFailure $+               "expected exactly one user id, got " ++ show (length uids)  checkAlicePubSubkeys :: Assertion checkAlicePubSubkeys = do     withCtx "test/alice" "C" OpenPGP $ \ctx ->         do Just key <- getKey ctx alicePubFpr NoSecret-           let subs = keySubKeys key-           length subs @?= 2-           let sub = head subs-           subkeyAlgorithm sub @?= Rsa-           subkeyLength sub @?= 2048-           subkeyKeyId sub @?= "6B9809775CF91391"-           subkeyFpr sub @?= "3F10159E56ECB494ED42EFA36B9809775CF91391"+           case keySubKeys key of+             [sub, _] -> do+               subkeyAlgorithm sub @?= Rsa+               subkeyLength sub @?= 2048+               subkeyKeyId sub @?= "6B9809775CF91391"+               subkeyFpr sub @?= "3F10159E56ECB494ED42EFA36B9809775CF91391"+             subs -> assertFailure $+               "expected exactly two subkeys, got " ++ show (length subs)  removeAliceKey :: Assertion removeAliceKey = do@@ -106,11 +125,13 @@   -- Copy alice's key into temporary directory so we can safely remove it   let aliceTmpDir = tmpDir </> "alice"   createDirectory aliceTmpDir+  -- Where GnuPG has no /run/user to fall back on (macOS, some containers) it+  -- puts its agent sockets straight into the homedir, so skip anything that is+  -- not a regular file rather than listing socket names one by one.   aliceFiles <- listDirectory "test/alice"+              >>= filterM (fmap isRegularFile . getFileStatus . ("test/alice" </>))   mapM_ (\f -> copyFile ("test/alice" </> f) (tmpDir </> "alice" </> f))-    $ filter (\f -> not ("S.gpg-agent" `isPrefixOf` f)-                 && f /= "private-keys-v1.d"-                 && f /= ".gpg-v21-migrated"+    $ filter (\f -> f /= ".gpg-v21-migrated"                  && f /= "randomSeed"              ) aliceFiles @@ -137,3 +158,68 @@     withCtx "test/real-person" "C" OpenPGP $ \ctx -> do       mRet <- importKeyFromFile ctx "this-file-doesn't-exist"       isJust mRet @? "shouldn't be able to read this file"++exportAlicePubArmored :: Assertion+exportAlicePubArmored =+    withCtx "test/alice" "C" OpenPGP $ \ctx -> do+        setArmor True ctx+        key <- fromRight <$> exportKey ctx alicePubFpr+        ("-----BEGIN PGP PUBLIC KEY BLOCK-----" `BS.isPrefixOf` key)+            @? "exported key must be armored"++exportImportRoundtrip :: Assertion+exportImportRoundtrip = do+    key <- withCtx "test/alice" "C" OpenPGP $ \ctx ->+        fromRight <$> exportKey ctx alicePubFpr+    tmpDir <- createTemporaryTestDir "exportImportRoundtrip"+    withCtx tmpDir "C" OpenPGP $ \ctx -> do+        mErr <- importKeyFromBytes ctx key+        mErr @?= Nothing+        imported <- getKey ctx alicePubFpr NoSecret+        isJust imported @? "imported key should be present"+    removeDirectoryRecursive tmpDir++exportInexistentIsEmpty :: Assertion+exportInexistentIsEmpty =+    withCtx "test/alice" "C" OpenPGP $ \ctx -> do+        key <- fromRight <$> exportKey ctx "ABCDEF"+        key @?= BS.empty++exportAllKeys :: Assertion+exportAllKeys =+    withCtx "test/alice" "C" OpenPGP $ \ctx -> do+        single <- fromRight <$> exportKeys ctx [] [alicePubFpr]+        all' <- fromRight <$> exportKeys ctx [] []+        (BS.length all' > BS.length single)+            @? "exporting all keys must yield more than a single key"++exportMinimal :: Assertion+exportMinimal =+    withCtx "test/bob" "C" OpenPGP $ \ctx -> do+        full <- fromRight <$> exportKeys ctx [] [alicePubFpr]+        minimal <- fromRight <$> exportKeys ctx [ExportMinimal] [alicePubFpr]+        not (BS.null minimal) @? "minimal export must not be empty"+        (BS.length minimal <= BS.length full)+            @? "minimal export must not be larger than the full export"++exportAliceSecretArmored :: Assertion+exportAliceSecretArmored =+    withCtx "test/alice" "C" OpenPGP $ \ctx -> do+        setPassphraseCallback ctx (Just (\_ _ _ -> return (Just "alice123")))+        setArmor True ctx+        key <- fromRight <$> exportSecretKey ctx alicePubFpr+        ("-----BEGIN PGP PRIVATE KEY BLOCK-----" `BS.isPrefixOf` key)+            @? "exported secret key must be armored"++readFromBytesWorks :: Assertion+readFromBytesWorks = do+    key <- BS.readFile "test/real-person/real-person.key"+    tmpDir <- createTemporaryTestDir "readFromBytesWorks"+    withCtx tmpDir "C" OpenPGP $ \ctx -> do+      before <- getKey ctx realPersonPubFpr NoSecret+      isNothing before @? "key shouldn't be present before import"+      mRet <- importKeyFromBytes ctx key+      mRet @?= Nothing+      after <- getKey ctx realPersonPubFpr NoSecret+      isJust after @? "key should be present after import"+    removeDirectoryRecursive tmpDir
test/Main.hs view
@@ -10,8 +10,10 @@ main :: IO () main = do     passphraseCbTests <- CryptoTest.cbTests+    keyCbTests <- KeyTest.cbTests     defaultMain $ testGroup "tests"         [ testGroup "key"    KeyTest.tests+        , keyCbTests         , testGroup "keyGen" KeyGenTest.tests         , testGroup "ctx"    CtxTest.tests         , CryptoTest.tests
test/TestUtil.hs view
@@ -20,6 +20,9 @@ bobPubFpr :: BS.ByteString bobPubFpr = "6C4FB8F2" +realPersonPubFpr :: BS.ByteString+realPersonPubFpr = "2DA4C89E28F515B4"+ -- Orphan instance here! Because this is only a test, orphans are probably OK. -- http://stackoverflow.com/a/3081367/350221 instance Arbitrary BS.ByteString where