h-gpgme (empty) → 0.1.0.0
raw patch · 10 files changed
+598/−0 lines, 10 filesdep +HUnitdep +QuickCheckdep +basesetup-changed
Dependencies added: HUnit, QuickCheck, base, bindings-gpgme, bytestring, test-framework, test-framework-hunit, test-framework-quickcheck2, unix
Files
- LICENSE +20/−0
- Setup.hs +2/−0
- h-gpgme.cabal +52/−0
- src/Crypto/Gpgme.hs +82/−0
- src/Crypto/Gpgme/Crypto.hs +173/−0
- src/Crypto/Gpgme/Ctx.hs +92/−0
- src/Crypto/Gpgme/Internal.hs +48/−0
- src/Crypto/Gpgme/Key.hs +52/−0
- src/Crypto/Gpgme/Types.hs +64/−0
- test/Main.hs +13/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2014 Reto Hablützel++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ h-gpgme.cabal view
@@ -0,0 +1,52 @@+Name: h-gpgme+Version: 0.1.0.0+Description: High Level Binding for GnuPG Made Easy (gpgme)+License: MIT+License-file: LICENSE+Author: Reto Habluetzel+Maintainer: rethab@rethab.ch+Copyright: (c) Reto Habluetzel 2014+Author: Reto Habluetzel 2014+Homepage: https://github.com/rethab/h-gpgme+Bug-reports: https://github.com/rethab/h-gpgme/issues+Tested-With: GHC==7.8.2+Category: Cryptography+Build-Type: Simple+Cabal-Version: >=1.10++source-repository head+ type: git+ location: https://github.com/rethab/h-gpgme++library+ hs-source-dirs: src+ ghc-options: -Wall+ -O2+ -fno-warn-orphans+ exposed-modules: Crypto.Gpgme+ other-modules: Crypto.Gpgme.Key+ , Crypto.Gpgme.Ctx+ , Crypto.Gpgme.Crypto+ , Crypto.Gpgme.Internal+ , Crypto.Gpgme.Types+ build-depends: base == 4.*+ , bindings-gpgme >= 0.1 && <0.2+ , bytestring >= 0.9+ , unix >= 2.5+ default-language: Haskell2010++test-suite tests+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ hs-source-dirs: src, test+ main-is: Main.hs+ build-depends: base == 4.*+ , bindings-gpgme >= 0.1 && <0.2+ , bytestring >= 0.9+ , unix >= 2.5++ , HUnit == 1.2.*+ , test-framework == 0.8.*+ , test-framework-quickcheck2 == 0.3.*+ , test-framework-hunit == 0.3.*+ , QuickCheck
+ src/Crypto/Gpgme.hs view
@@ -0,0 +1,82 @@++-- |+-- Module : Crypto.Gpgme+-- Copyright : (c) Reto Hablützel 2014+-- License : MIT+--+-- Maintainer : rethab@rethab.ch+-- Stability : experimental+-- Portability : untested+--+-- High Level Binding for GnuPG Made Easy (gpgme)+-- +-- Most of these functions are a one-to-one translation+-- from GnuPG API with some Haskell idiomatics to make+-- the API more convenient.+--+-- See the GnuPG manual for more information: <https://www.gnupg.org/documentation/manuals/gpgme.pdf>+--+--+-- == Example (from the tests):+--+-- >let alice_pub_fpr = "EAACEB8A"+-- >+-- >-- encrypt+-- >enc <- withCtx "test/bob" "C" openPGP $ \bCtx ->+-- > withKey bCtx alice_pub_fpr noSecret $ \aPubKey ->+-- > encrypt bCtx [aPubKey] noFlag plain+-- >+-- >-- decrypt+-- >dec <- withCtx "test/alice" "C" openPGP $ \aCtx ->+-- > decrypt aCtx (fromJustAndRight enc)+--++module Crypto.Gpgme (+ -- * Context+ Ctx+ , newCtx+ , freeCtx+ , withCtx++ -- currently not exported as it does not work as expected:+ -- , withPWCtx+ + -- * Keys+ , Key+ , getKey+ , freeKey+ , withKey++ -- * Encryption+ , encrypt+ , encryptSign+ , encrypt'+ , encryptSign'+ , decrypt+ , decrypt'+ , decryptVerify+ , decryptVerify'++ -- * Other Types+ , Protocol+ , openPGP++ , InvalidKey++ , IncludeSecret+ , noSecret+ , secret++ , Flag+ , alwaysTrust+ , noFlag++ , DecryptError(..)++) where+++import Crypto.Gpgme.Ctx+import Crypto.Gpgme.Crypto+import Crypto.Gpgme.Types+import Crypto.Gpgme.Key
+ src/Crypto/Gpgme/Crypto.hs view
@@ -0,0 +1,173 @@+module Crypto.Gpgme.Crypto (++ encrypt+ , encryptSign+ , encrypt'+ , encryptSign'+ , decrypt+ , decrypt'+ , decryptVerify+ , decryptVerify'++) where++import Bindings.Gpgme+import qualified Data.ByteString as BS+import Foreign+import GHC.Ptr++import Crypto.Gpgme.Ctx+import Crypto.Gpgme.Internal+import Crypto.Gpgme.Key+import Crypto.Gpgme.Types++locale :: String+locale = "C"++-- | Convenience wrapper around 'withCtx' and 'withKey' to+-- encrypt a single plaintext for a single recipient with+-- its homedirectory.+encrypt' :: String -> Fpr -> Plain -> IO (Either String Encrypted)+encrypt' = encryptIntern' encrypt++-- | Convenience wrapper around 'withCtx' and 'withKey' to+-- encrypt and sign a single plaintext for a single recipient+-- with its homedirectory.+encryptSign' :: String -> Fpr -> Plain -> IO (Either String Encrypted)+encryptSign' = encryptIntern' encryptSign++encryptIntern' :: (Ctx -> [Key] -> Flag -> Plain+ -> IO (Either [InvalidKey] Encrypted)+ ) -> String -> Fpr -> Plain -> IO (Either String Encrypted)+encryptIntern' encrFun gpgDir recFpr plain = do+ withCtx gpgDir locale openPGP $ \ctx ->+ do mbRes <- withKey ctx recFpr noSecret $ \pubKey ->+ encrFun ctx [pubKey] noFlag plain+ return $ mapErr mbRes+ where mapErr Nothing = Left $ "no such key: " ++ show recFpr+ mapErr (Just (Left err)) = Left (show err)+ mapErr (Just (Right res)) = Right res++-- | encrypt for a list of recipients+encrypt :: Ctx -> [Key] -> Flag -> Plain -> IO (Either [InvalidKey] Encrypted)+encrypt = encryptIntern c'gpgme_op_encrypt++-- | encrypt and sign for a list of recipients+encryptSign :: Ctx -> [Key] -> Flag -> Plain -> IO (Either [InvalidKey] Encrypted) +encryptSign = encryptIntern c'gpgme_op_encrypt_sign++encryptIntern :: (C'gpgme_ctx_t+ -> GHC.Ptr.Ptr C'gpgme_key_t+ -> C'gpgme_encrypt_flags_t+ -> C'gpgme_data_t+ -> C'gpgme_data_t+ -> IO C'gpgme_error_t+ )+ -> Ctx+ -> [Key]+ -> Flag+ -> Plain+ -> IO (Either [InvalidKey] Encrypted) +encryptIntern enc_op (Ctx ctxPtr _) recPtrs (Flag flag) plain = do+ -- init buffer with plaintext+ plainBufPtr <- malloc+ BS.useAsCString plain $ \bs -> do+ let copyData = 1 -- gpgme shall copy data, as bytestring will free it+ let plainlen = fromIntegral (BS.length plain)+ ret <- c'gpgme_data_new_from_mem plainBufPtr bs plainlen copyData+ check_error "data_new_from_mem" ret+ plainBuf <- peek plainBufPtr++ -- init buffer for result+ resultBufPtr <- malloc+ check_error "data_new" =<< c'gpgme_data_new resultBufPtr+ resultBuf <- peek resultBufPtr++ -- null terminated array of recipients+ recArray <- if null recPtrs+ then return nullPtr+ else do keys <- mapM (peek . unKey) recPtrs+ newArray (keys ++ [nullPtr])++ ctx <- peek ctxPtr++ -- encrypt+ check_error "op_encrypt" =<< enc_op ctx recArray flag plainBuf resultBuf+ free plainBufPtr++ -- check whether all keys could be used for encryption+ encResPtr <- c'gpgme_op_encrypt_result ctx+ encRes <- peek encResPtr+ let recPtr = c'_gpgme_op_encrypt_result'invalid_recipients encRes++ let res = if recPtr /= nullPtr+ then Left (collectFprs recPtr)+ else Right (collectResult resultBuf)++ free resultBufPtr++ return res++-- | Convenience wrapper around 'withCtx' and 'withKey' to+-- decrypt a single ciphertext with its homedirectory.+decrypt' :: String -> Encrypted -> IO (Either DecryptError Plain)+decrypt' = decryptInternal' decrypt++-- | Convenience wrapper around 'withCtx' and 'withKey' to+-- decrypt and verify a single ciphertext with its homedirectory.+decryptVerify' :: String -> Encrypted -> IO (Either DecryptError Plain)+decryptVerify' = decryptInternal' decryptVerify++decryptInternal' :: (Ctx -> Encrypted -> IO (Either DecryptError Plain))+ -> String+ -> Encrypted+ -> IO (Either DecryptError Plain)+decryptInternal' decrFun gpgDir cipher =+ withCtx gpgDir locale openPGP $ \ctx ->+ decrFun ctx cipher++-- | Decrypts a ciphertext+decrypt :: Ctx -> Encrypted -> IO (Either DecryptError Plain)+decrypt = decryptIntern c'gpgme_op_decrypt++-- | Decrypts and verifies a ciphertext+decryptVerify :: Ctx -> Encrypted -> IO (Either DecryptError Plain)+decryptVerify = decryptIntern c'gpgme_op_decrypt_verify+++decryptIntern :: (C'gpgme_ctx_t+ -> C'gpgme_data_t+ -> C'gpgme_data_t+ -> IO C'gpgme_error_t+ )+ -> Ctx+ -> Encrypted+ -> IO (Either DecryptError Plain)+decryptIntern dec_op (Ctx ctxPtr _) cipher = do+ -- init buffer with cipher+ cipherBufPtr <- malloc+ BS.useAsCString cipher $ \bs -> do+ let copyData = 1 -- gpgme shall copy data, as bytestring will free it+ let cipherlen = fromIntegral (BS.length cipher)+ ret <- c'gpgme_data_new_from_mem cipherBufPtr bs cipherlen copyData+ check_error "data_new_from_mem" ret+ cipherBuf <- peek cipherBufPtr++ -- init buffer for result+ resultBufPtr <- malloc+ check_error "data_new" =<< c'gpgme_data_new resultBufPtr+ resultBuf <- peek resultBufPtr++ ctx <- peek ctxPtr++ -- decrypt+ errcode <- dec_op ctx cipherBuf resultBuf++ let res = if errcode /= noError+ then Left (toDecryptError errcode)+ else Right (collectResult resultBuf)++ free cipherBufPtr+ free resultBufPtr++ return res
+ src/Crypto/Gpgme/Ctx.hs view
@@ -0,0 +1,92 @@+module Crypto.Gpgme.Ctx where++import Bindings.Gpgme+import Foreign+import Foreign.C.String+import Foreign.C.Types+import System.Posix.IO (fdWrite)++import Crypto.Gpgme.Types+import Crypto.Gpgme.Internal++-- | Creates a new 'Ctx' from a @homedirectory@, a @locale@+-- and a @protocol@. Needs to be freed with 'freeCtx', which+-- is why you are encouraged to use 'withCtx'.+newCtx :: String -- ^ path to gpg homedirectory+ -> String -- ^ locale+ -> Protocol -- ^ protocol+ -> IO Ctx+newCtx homedir localeStr (Protocol protocol) =+ do homedirPtr <- newCString homedir++ -- check version: necessary for initialization!!+ version <- c'gpgme_check_version nullPtr >>= peekCString++ -- create context+ ctxPtr <- malloc + check_error "gpgme_new" =<< c'gpgme_new ctxPtr++ ctx <- peek ctxPtr++ -- set locale+ locale <- newCString localeStr+ check_error "set_locale" =<< c'gpgme_set_locale ctx lcCtype locale++ -- set protocol in ctx+ check_error "set_protocol" =<< c'gpgme_set_protocol ctx (fromIntegral protocol)++ -- set homedir in ctx+ check_error "set_engine_info" =<< c'gpgme_ctx_set_engine_info ctx+ (fromIntegral protocol) nullPtr homedirPtr++ return (Ctx ctxPtr version)+ where lcCtype :: CInt+ lcCtype = 0++-- | Free a previously created 'Ctx'+freeCtx :: Ctx -> IO ()+freeCtx (Ctx ctxPtr _) =+ do ctx <- peek ctxPtr+ c'gpgme_release ctx+ free ctxPtr++-- | Runs the action with a new 'Ctx' and frees it afterwards+--+-- See 'newCtx' for a descrption of the parameters.+withCtx :: String -- ^ path to gpg homedirectory+ -> String -- ^ locale+ -> Protocol -- ^ protocol+ -> (Ctx -> IO a) -- ^ action to be run with ctx+ -> IO a+withCtx homedir localeStr prot f = do+ ctx <- newCtx homedir localeStr prot+ res <- f ctx+ freeCtx ctx+ return res++withPWCtx :: String -> String -> String -> Protocol -> (Ctx -> IO a) -> IO a+withPWCtx pw homedir localeStr prot f = do+ ctx <- newCtx homedir localeStr prot+ setPassphrase ctx pw+ res <- f ctx+ freeCtx ctx+ return res++setPassphrase :: Ctx -> String -> IO ()+setPassphrase (Ctx ctxPtr _) passphrase =+ do ctx <- peek ctxPtr+ passcb <- wrap (passphrase_cb passphrase)+ c'gpgme_set_passphrase_cb ctx passcb nullPtr++passphrase_cb :: String -> Ptr () -> CString -> CString -> CInt -> CInt -> IO C'gpgme_error_t+passphrase_cb passphrase _ uid_hint passphrase_info prev_was_bad fd =+ do peekCString uid_hint >>= putStrLn+ peekCString passphrase_info >>= putStrLn+ putStrLn ("Prev was bad: " ++ show prev_was_bad)+ _ <- fdWrite (fromIntegral fd) (passphrase ++ "\n")+ return 0++-- from: http://www.haskell.org/haskellwiki/GHC/Using_the_FFI#Callbacks_into_Haskell_from_foreign_code+foreign import ccall "wrapper"+ wrap :: (Ptr () -> CString -> CString -> CInt -> CInt -> IO C'gpgme_error_t)+ -> IO (FunPtr (Ptr () -> CString -> CString -> CInt -> CInt -> IO C'gpgme_error_t))
+ src/Crypto/Gpgme/Internal.hs view
@@ -0,0 +1,48 @@+module Crypto.Gpgme.Internal where++import Bindings.Gpgme+import Control.Monad (unless)+import qualified Data.ByteString as BS+import Foreign (allocaBytes, castPtr, peek)+import Foreign.C.String (peekCString)+import System.IO.Unsafe (unsafePerformIO)++import Crypto.Gpgme.Types++collectFprs :: C'gpgme_invalid_key_t -> [InvalidKey]+collectFprs result = unsafePerformIO $ peek result >>= go+ where go :: C'_gpgme_invalid_key -> IO [InvalidKey]+ go invalid = do+ fpr <- peekCString (c'_gpgme_invalid_key'fpr invalid)+ let reason = fromIntegral (c'_gpgme_invalid_key'reason invalid)+ rest <- go (c'_gpgme_invalid_key'next invalid)+ return ((fpr, reason) : rest)++collectResult :: C'gpgme_data_t -> BS.ByteString+collectResult dat' = unsafePerformIO $ do+ -- make sure we start at the beginning+ _ <- c'gpgme_data_seek dat' 0 seekSet+ go dat'+ where go :: C'gpgme_data_t -> IO BS.ByteString+ go dat = allocaBytes 1 $ \buf -> + do read_bytes <- c'gpgme_data_read dat buf 1+ if read_bytes == 1+ then do byte <- peek (castPtr buf)+ rest <- go dat+ return (byte `BS.cons` rest)+ else return BS.empty+ seekSet = 0++check_error :: String -> C'gpgme_error_t -> IO ()+check_error fun gpgme_err =+ unless (gpgme_err == noError) $+ do errstr <- c'gpgme_strerror gpgme_err+ str <- peekCString errstr+ srcstr <- c'gpgme_strsource gpgme_err+ src <- peekCString srcstr+ error ("Fun: " ++ fun +++ ", Error: " ++ str ++ + ", Source: " ++ show src)++noError :: Num a => a+noError = 0
+ src/Crypto/Gpgme/Key.hs view
@@ -0,0 +1,52 @@+module Crypto.Gpgme.Key (+ getKey+ , freeKey+ , withKey+ ) where++import Bindings.Gpgme+import qualified Data.ByteString as BS+import Foreign++import Crypto.Gpgme.Types+import Crypto.Gpgme.Internal++-- | Returns a 'Key' from the @context@ based on its @fingerprint@.+-- As a 'Key' returned from the function needs to be freed+-- with 'freeKey', the use of 'withKey' is encouraged. 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+ -> IO (Maybe Key)+getKey (Ctx ctxPtr _) fpr (IncludeSecret is) = do+ keyPtr <- malloc+ ret <- BS.useAsCString fpr $ \cFpr ->+ peek ctxPtr >>= \ctx ->+ c'gpgme_get_key ctx cFpr keyPtr is+ if ret == noError+ then return . Just . Key $ keyPtr+ else free keyPtr >> return Nothing++-- | Frees a key previously created with 'getKey'+freeKey :: Key -> IO ()+freeKey (Key keyPtr) = free keyPtr++-- | Conveniently runs the @action@ with the 'Key' associated+-- with the 'Fpr' in the 'Ctx' and frees it afterwards.+-- If no 'Key' with this 'Fpr' exists, Nothing is returned.++withKey :: Ctx -- ^ context to operate in+ -> Fpr -- ^ fingerprint+ -> IncludeSecret -- ^ whether to include secrets when searching for the key+ -> (Key -> IO a) -- ^ action to be run with key+ -> IO (Maybe a)+withKey ctx fpr is f = do + mbkey <- getKey ctx fpr is+ case mbkey of+ Just key -> do res <- f key+ freeKey key+ return (Just res)+ Nothing -> return Nothing++
+ src/Crypto/Gpgme/Types.hs view
@@ -0,0 +1,64 @@+module Crypto.Gpgme.Types where++import Bindings.Gpgme+import qualified Data.ByteString as BS+import Foreign.C.Types (CInt, CUInt)+import Foreign++-- | the protocol to be used in the crypto engine+newtype Protocol = Protocol Int++openPGP :: Protocol +openPGP = Protocol c'GPGME_PROTOCOL_OpenPGP+-- TODO other protocols++-- | Context to be passed around with operations. Use 'newCtx' or+-- 'withCtx' in order to obtain an instance.+data Ctx = Ctx {+ _ctx :: Ptr C'gpgme_ctx_t+ , _version :: String+}++type Fpr = BS.ByteString+type Plain = BS.ByteString+type Encrypted = BS.ByteString++-- | The fingerprint and an error code+type InvalidKey = (String, Int)+-- TODO map intot better error code++-- | A key from the context+newtype Key = Key { unKey :: Ptr C'gpgme_key_t }++-- | Whether to include secret keys when searching+newtype IncludeSecret = IncludeSecret CInt++-- | do not consider secret keys when searching+noSecret :: IncludeSecret+noSecret = IncludeSecret 0++-- | consider secret keys when searching+secret :: IncludeSecret+secret = IncludeSecret 1++newtype Flag = Flag CUInt++alwaysTrust :: Flag+alwaysTrust = Flag c'GPGME_ENCRYPT_ALWAYS_TRUST++noFlag :: Flag+noFlag = Flag 0++-- | error indicating what went wrong in decryption+data DecryptError =+ NoData -- ^ no data to decrypt+ | Failed -- ^ not a valid cipher+ | BadPass -- ^ passphrase for secret was wrong+ | Unknown Int -- ^ something else went wrong+ deriving (Eq, Show)++toDecryptError :: C'gpgme_err_code_t -> DecryptError+toDecryptError 58 = NoData+toDecryptError 152 = Failed+toDecryptError 11 = BadPass+toDecryptError x = Unknown (fromIntegral x)
+ test/Main.hs view
@@ -0,0 +1,13 @@+module Main where++import Test.Framework (defaultMain, testGroup)++import KeyTest +import CtxTest +import CryptoTest ++main = defaultMain+ [ testGroup "key" KeyTest.tests+ , testGroup "ctx" CtxTest.tests+ , testGroup "crypto" CryptoTest.tests+ ]