2011-12-21 01:47:56 +00:00
|
|
|
|
{- gpg interface
|
|
|
|
|
-
|
2015-01-21 16:50:09 +00:00
|
|
|
|
- Copyright 2011 Joey Hess <id@joeyh.name>
|
2011-12-21 01:47:56 +00:00
|
|
|
|
-
|
2017-03-10 19:05:47 +00:00
|
|
|
|
- License: BSD-2-clause
|
2011-12-21 01:47:56 +00:00
|
|
|
|
-}
|
|
|
|
|
|
2013-09-05 03:16:33 +00:00
|
|
|
|
{-# LANGUAGE CPP #-}
|
2013-05-10 21:29:59 +00:00
|
|
|
|
|
2019-11-21 19:38:06 +00:00
|
|
|
|
module Utility.Gpg (
|
|
|
|
|
KeyId,
|
|
|
|
|
KeyIds(..),
|
|
|
|
|
GpgCmd(..),
|
|
|
|
|
mkGpgCmd,
|
|
|
|
|
boolGpgCmd,
|
|
|
|
|
pkEncTo,
|
|
|
|
|
stdEncryptionParams,
|
|
|
|
|
pipeStrict,
|
|
|
|
|
feedRead,
|
2020-06-16 21:03:19 +00:00
|
|
|
|
feedRead',
|
2019-11-21 19:38:06 +00:00
|
|
|
|
findPubKeys,
|
|
|
|
|
UserId,
|
|
|
|
|
secretKeys,
|
|
|
|
|
KeyType(..),
|
|
|
|
|
maxRecommendedKeySize,
|
|
|
|
|
genSecretKey,
|
|
|
|
|
genRandom,
|
|
|
|
|
testKeyId,
|
|
|
|
|
#ifndef mingw32_HOST_OS
|
|
|
|
|
testHarness,
|
|
|
|
|
testTestHarness,
|
|
|
|
|
checkEncryptionFile,
|
|
|
|
|
checkEncryptionStream,
|
|
|
|
|
#endif
|
|
|
|
|
) where
|
2011-12-21 01:47:56 +00:00
|
|
|
|
|
|
|
|
|
import Common
|
2017-12-14 16:46:57 +00:00
|
|
|
|
import qualified BuildInfo
|
2013-08-04 17:54:09 +00:00
|
|
|
|
#ifndef mingw32_HOST_OS
|
|
|
|
|
import System.Posix.Types
|
2017-12-31 20:08:31 +00:00
|
|
|
|
import System.Posix.IO
|
2013-08-04 17:54:09 +00:00
|
|
|
|
import Utility.Env
|
2017-12-31 20:08:31 +00:00
|
|
|
|
import Utility.Env.Set
|
2019-08-08 16:18:53 +00:00
|
|
|
|
import Utility.FileMode
|
2018-01-05 19:09:10 +00:00
|
|
|
|
#else
|
|
|
|
|
import Utility.Tmp
|
2013-08-04 16:10:00 +00:00
|
|
|
|
#endif
|
2013-09-16 16:57:39 +00:00
|
|
|
|
import Utility.Format (decode_c)
|
2011-12-21 01:47:56 +00:00
|
|
|
|
|
2020-06-05 17:58:21 +00:00
|
|
|
|
import Control.Concurrent.Async
|
2015-05-10 20:19:56 +00:00
|
|
|
|
import Control.Monad.IO.Class
|
|
|
|
|
import qualified Data.Map as M
|
2017-05-24 18:54:54 +00:00
|
|
|
|
import Data.Char
|
2015-05-10 20:19:56 +00:00
|
|
|
|
|
2013-09-16 16:57:39 +00:00
|
|
|
|
type KeyId = String
|
|
|
|
|
|
|
|
|
|
newtype KeyIds = KeyIds { keyIds :: [KeyId] }
|
2012-12-13 04:45:27 +00:00
|
|
|
|
deriving (Ord, Eq)
|
2011-12-21 01:47:56 +00:00
|
|
|
|
|
2015-09-09 22:06:49 +00:00
|
|
|
|
newtype GpgCmd = GpgCmd { unGpgCmd :: String }
|
|
|
|
|
|
|
|
|
|
{- Get gpg command to use, Just what's specified or, if a specific gpg
|
|
|
|
|
- command was found at configure time, use it, or otherwise, "gpg". -}
|
|
|
|
|
mkGpgCmd :: Maybe FilePath -> GpgCmd
|
|
|
|
|
mkGpgCmd (Just c) = GpgCmd c
|
2017-12-14 16:46:57 +00:00
|
|
|
|
mkGpgCmd Nothing = GpgCmd (fromMaybe "gpg" BuildInfo.gpg)
|
2015-09-09 22:06:49 +00:00
|
|
|
|
|
|
|
|
|
boolGpgCmd :: GpgCmd -> [CommandParam] -> IO Bool
|
|
|
|
|
boolGpgCmd (GpgCmd cmd) = boolSystem cmd
|
2013-05-19 21:59:58 +00:00
|
|
|
|
|
2013-09-01 18:12:00 +00:00
|
|
|
|
-- Generate an argument list to asymetrically encrypt to the given recipients.
|
|
|
|
|
pkEncTo :: [String] -> [CommandParam]
|
|
|
|
|
pkEncTo = concatMap (\r -> [Param "--recipient", Param r])
|
|
|
|
|
|
2011-12-21 01:47:56 +00:00
|
|
|
|
stdParams :: [CommandParam] -> IO [String]
|
|
|
|
|
stdParams params = do
|
2013-08-02 16:27:32 +00:00
|
|
|
|
#ifndef mingw32_HOST_OS
|
2011-12-21 01:47:56 +00:00
|
|
|
|
-- Enable batch mode if GPG_AGENT_INFO is set, to avoid extraneous
|
2012-01-09 22:19:29 +00:00
|
|
|
|
-- gpg output about password prompts. GPG_BATCH is set by the test
|
|
|
|
|
-- suite for a similar reason.
|
2011-12-21 03:20:36 +00:00
|
|
|
|
e <- getEnv "GPG_AGENT_INFO"
|
2012-01-09 22:19:29 +00:00
|
|
|
|
b <- getEnv "GPG_BATCH"
|
|
|
|
|
let batch = if isNothing e && isNothing b
|
|
|
|
|
then []
|
2012-08-17 15:22:11 +00:00
|
|
|
|
else ["--batch", "--no-tty", "--use-agent"]
|
2011-12-21 01:47:56 +00:00
|
|
|
|
return $ batch ++ defaults ++ toCommand params
|
2013-05-10 21:29:59 +00:00
|
|
|
|
#else
|
|
|
|
|
return $ defaults ++ toCommand params
|
|
|
|
|
#endif
|
2012-12-13 04:24:19 +00:00
|
|
|
|
where
|
2013-09-07 23:08:28 +00:00
|
|
|
|
-- Be quiet, even about checking the trustdb.
|
2012-12-13 04:24:19 +00:00
|
|
|
|
defaults = ["--quiet", "--trust-model", "always"]
|
2011-12-21 01:47:56 +00:00
|
|
|
|
|
2013-09-01 18:12:00 +00:00
|
|
|
|
{- Usual options for symmetric / public-key encryption. -}
|
|
|
|
|
stdEncryptionParams :: Bool -> [CommandParam]
|
2015-06-01 17:52:23 +00:00
|
|
|
|
stdEncryptionParams symmetric = enc symmetric ++
|
|
|
|
|
[ Param "--force-mdc"
|
2013-09-07 17:06:36 +00:00
|
|
|
|
, Param "--no-textmode"
|
|
|
|
|
]
|
2013-09-01 18:12:00 +00:00
|
|
|
|
where
|
2015-06-01 17:52:23 +00:00
|
|
|
|
enc True = [ Param "--symmetric" ]
|
2013-09-01 18:12:00 +00:00
|
|
|
|
-- Force gpg to only encrypt to the specified recipients, not
|
|
|
|
|
-- configured defaults. Recipients are assumed to be specified in
|
|
|
|
|
-- elsewhere.
|
2015-06-01 17:52:23 +00:00
|
|
|
|
enc False =
|
|
|
|
|
[ Param "--encrypt"
|
|
|
|
|
, Param "--no-encrypt-to"
|
|
|
|
|
, Param "--no-default-recipient"
|
|
|
|
|
]
|
2013-09-01 18:12:00 +00:00
|
|
|
|
|
2011-12-21 01:47:56 +00:00
|
|
|
|
{- Runs gpg with some params and returns its stdout, strictly. -}
|
2015-09-09 22:06:49 +00:00
|
|
|
|
readStrict :: GpgCmd -> [CommandParam] -> IO String
|
|
|
|
|
readStrict (GpgCmd cmd) params = do
|
2011-12-21 01:47:56 +00:00
|
|
|
|
params' <- stdParams params
|
2020-06-04 19:36:34 +00:00
|
|
|
|
let p = (proc cmd params')
|
|
|
|
|
{ std_out = CreatePipe }
|
|
|
|
|
withCreateProcess p (go p)
|
|
|
|
|
where
|
|
|
|
|
go p _ (Just hout) _ pid = do
|
|
|
|
|
hSetBinaryMode hout True
|
|
|
|
|
forceSuccessProcess p pid `after` hGetContentsStrict hout
|
|
|
|
|
go _ _ _ _ _ = error "internal"
|
2011-12-21 01:47:56 +00:00
|
|
|
|
|
2011-12-21 03:20:36 +00:00
|
|
|
|
{- Runs gpg, piping an input value to it, and returning its stdout,
|
2011-12-21 01:47:56 +00:00
|
|
|
|
- strictly. -}
|
2015-09-09 22:06:49 +00:00
|
|
|
|
pipeStrict :: GpgCmd -> [CommandParam] -> String -> IO String
|
|
|
|
|
pipeStrict (GpgCmd cmd) params input = do
|
2011-12-21 01:47:56 +00:00
|
|
|
|
params' <- stdParams params
|
2020-06-04 19:36:34 +00:00
|
|
|
|
let p = (proc cmd params')
|
|
|
|
|
{ std_in = CreatePipe
|
|
|
|
|
, std_out = CreatePipe
|
|
|
|
|
}
|
|
|
|
|
withCreateProcess p (go p)
|
|
|
|
|
where
|
|
|
|
|
go p (Just to) (Just from) _ pid = do
|
2012-07-19 04:43:36 +00:00
|
|
|
|
hSetBinaryMode to True
|
|
|
|
|
hSetBinaryMode from True
|
|
|
|
|
hPutStr to input
|
|
|
|
|
hClose to
|
2020-06-04 19:36:34 +00:00
|
|
|
|
forceSuccessProcess p pid `after` hGetContentsStrict from
|
|
|
|
|
go _ _ _ _ _ = error "internal"
|
2011-12-21 01:47:56 +00:00
|
|
|
|
|
2013-09-01 18:12:00 +00:00
|
|
|
|
{- Runs gpg with some parameters. First sends it a passphrase (unless it
|
|
|
|
|
- is empty) via '--passphrase-fd'. Then runs a feeder action that is
|
|
|
|
|
- passed a handle and should write to it all the data to input to gpg.
|
|
|
|
|
- Finally, runs a reader action that is passed a handle to gpg's
|
|
|
|
|
- output.
|
2011-12-21 01:47:56 +00:00
|
|
|
|
-
|
2013-01-16 19:27:46 +00:00
|
|
|
|
- Runs gpg in batch mode; this is necessary to avoid gpg 2.x prompting for
|
|
|
|
|
- the passphrase.
|
|
|
|
|
-
|
2020-06-16 21:03:19 +00:00
|
|
|
|
- Note that the reader must fully consume gpg's input before returning. -}
|
2015-09-09 22:06:49 +00:00
|
|
|
|
feedRead :: (MonadIO m, MonadMask m) => GpgCmd -> [CommandParam] -> String -> (Handle -> IO ()) -> (Handle -> m a) -> m a
|
|
|
|
|
feedRead cmd params passphrase feeder reader = do
|
2013-08-04 16:10:00 +00:00
|
|
|
|
#ifndef mingw32_HOST_OS
|
2020-06-05 17:58:21 +00:00
|
|
|
|
let setup = liftIO $ do
|
|
|
|
|
-- pipe the passphrase into gpg on a fd
|
|
|
|
|
(frompipe, topipe) <- System.Posix.IO.createPipe
|
2013-09-05 06:09:39 +00:00
|
|
|
|
toh <- fdToHandle topipe
|
2020-06-05 17:58:21 +00:00
|
|
|
|
t <- async $ do
|
|
|
|
|
hPutStrLn toh passphrase
|
|
|
|
|
hClose toh
|
|
|
|
|
let Fd pfd = frompipe
|
|
|
|
|
let passphrasefd = [Param "--passphrase-fd", Param $ show pfd]
|
|
|
|
|
return (passphrasefd, frompipe, toh, t)
|
|
|
|
|
let cleanup (_, frompipe, toh, t) = liftIO $ do
|
|
|
|
|
closeFd frompipe
|
2013-09-05 06:09:39 +00:00
|
|
|
|
hClose toh
|
2020-06-05 17:58:21 +00:00
|
|
|
|
cancel t
|
|
|
|
|
bracket setup cleanup $ \(passphrasefd, _, _, _) ->
|
|
|
|
|
go (passphrasefd ++ params)
|
2013-05-10 21:29:59 +00:00
|
|
|
|
#else
|
2013-09-05 06:09:39 +00:00
|
|
|
|
-- store the passphrase in a temp file for gpg
|
|
|
|
|
withTmpFile "gpg" $ \tmpfile h -> do
|
2014-08-10 19:30:55 +00:00
|
|
|
|
liftIO $ hPutStr h passphrase
|
|
|
|
|
liftIO $ hClose h
|
2013-09-06 21:05:41 +00:00
|
|
|
|
let passphrasefile = [Param "--passphrase-file", File tmpfile]
|
|
|
|
|
go $ passphrasefile ++ params
|
2013-05-10 21:29:59 +00:00
|
|
|
|
#endif
|
2013-08-04 16:10:00 +00:00
|
|
|
|
where
|
2020-06-16 21:03:19 +00:00
|
|
|
|
go params' = feedRead' cmd params' feeder reader
|
2013-09-05 06:09:39 +00:00
|
|
|
|
|
|
|
|
|
{- Like feedRead, but without passphrase. -}
|
2020-06-16 21:03:19 +00:00
|
|
|
|
feedRead' :: (MonadIO m, MonadMask m) => GpgCmd -> [CommandParam] -> (Handle -> IO ()) -> (Handle -> m a) -> m a
|
|
|
|
|
feedRead' (GpgCmd cmd) params feeder reader = do
|
2014-07-29 20:22:19 +00:00
|
|
|
|
params' <- liftIO $ stdParams $ Param "--batch" : params
|
2015-09-09 22:06:49 +00:00
|
|
|
|
let p = (proc cmd params')
|
2014-07-29 20:22:19 +00:00
|
|
|
|
{ std_in = CreatePipe
|
|
|
|
|
, std_out = CreatePipe
|
|
|
|
|
, std_err = Inherit
|
|
|
|
|
}
|
2020-06-04 16:13:26 +00:00
|
|
|
|
bracket (setup p) cleanup (go p)
|
2014-07-29 20:22:19 +00:00
|
|
|
|
where
|
|
|
|
|
setup = liftIO . createProcess
|
2020-06-04 16:13:26 +00:00
|
|
|
|
cleanup = liftIO . cleanupProcess
|
|
|
|
|
|
2020-06-05 17:58:21 +00:00
|
|
|
|
go p (Just to, Just from, _, pid) =
|
|
|
|
|
let runfeeder = do
|
2014-07-29 20:22:19 +00:00
|
|
|
|
feeder to
|
|
|
|
|
hClose to
|
2020-06-05 17:58:21 +00:00
|
|
|
|
in bracketIO (async runfeeder) cancel $ const $ do
|
|
|
|
|
r <- reader from
|
|
|
|
|
liftIO $ forceSuccessProcess p pid
|
|
|
|
|
return r
|
2020-06-04 16:13:26 +00:00
|
|
|
|
go _ _ = error "internal"
|
2011-12-21 01:47:56 +00:00
|
|
|
|
|
|
|
|
|
{- Finds gpg public keys matching some string. (Could be an email address,
|
2013-04-05 19:06:16 +00:00
|
|
|
|
- a key id, or a name; See the section 'HOW TO SPECIFY A USER ID' of
|
|
|
|
|
- GnuPG's manpage.) -}
|
2015-09-09 22:06:49 +00:00
|
|
|
|
findPubKeys :: GpgCmd -> String -> IO KeyIds
|
2017-05-24 18:08:02 +00:00
|
|
|
|
findPubKeys cmd for
|
2017-05-24 18:54:54 +00:00
|
|
|
|
-- pass forced subkey through as-is rather than
|
|
|
|
|
-- looking up the master key.
|
|
|
|
|
| isForcedSubKey for = return $ KeyIds [for]
|
2017-05-24 18:08:02 +00:00
|
|
|
|
| otherwise = KeyIds . parse . lines <$> readStrict cmd params
|
2012-12-13 04:24:19 +00:00
|
|
|
|
where
|
2015-06-01 17:52:23 +00:00
|
|
|
|
params = [Param "--with-colons", Param "--list-public-keys", Param for]
|
2017-01-31 22:40:42 +00:00
|
|
|
|
parse = mapMaybe (keyIdField . splitc ':')
|
2012-12-13 04:24:19 +00:00
|
|
|
|
keyIdField ("pub":_:_:_:f:_) = Just f
|
|
|
|
|
keyIdField _ = Nothing
|
2011-12-21 03:20:36 +00:00
|
|
|
|
|
2017-05-24 18:54:54 +00:00
|
|
|
|
{- "subkey!" tells gpg to force use of a specific subkey -}
|
|
|
|
|
isForcedSubKey :: String -> Bool
|
2017-12-05 17:58:53 +00:00
|
|
|
|
isForcedSubKey s = "!" `isSuffixOf` s && all isHexDigit (drop 1 (reverse s))
|
2017-05-24 18:54:54 +00:00
|
|
|
|
|
2013-09-16 16:57:39 +00:00
|
|
|
|
type UserId = String
|
|
|
|
|
|
|
|
|
|
{- All of the user's secret keys, with their UserIds.
|
|
|
|
|
- Note that the UserId may be empty. -}
|
2015-09-09 22:06:49 +00:00
|
|
|
|
secretKeys :: GpgCmd -> IO (M.Map KeyId UserId)
|
|
|
|
|
secretKeys cmd = catchDefaultIO M.empty makemap
|
2013-09-16 16:57:39 +00:00
|
|
|
|
where
|
2015-09-09 22:06:49 +00:00
|
|
|
|
makemap = M.fromList . parse . lines <$> readStrict cmd params
|
2015-06-01 17:52:23 +00:00
|
|
|
|
params = [Param "--with-colons", Param "--list-secret-keys", Param "--fixed-list-mode"]
|
2017-01-31 22:40:42 +00:00
|
|
|
|
parse = extract [] Nothing . map (splitc ':')
|
2013-09-16 16:57:39 +00:00
|
|
|
|
extract c (Just keyid) (("uid":_:_:_:_:_:_:_:_:userid:_):rest) =
|
fix failing quickcheck properties
QuickCheck 2.10 found a counterexample eg "\929184" broke the property.
As far as I can tell, Git.Filename is matching how git handles encoding
of strange high unicode characters in filenames for display. Git does
not display high unicode characters, and instead displays the C-style
escaped form of each byte. This is ambiguous, but since git is not
unicode aware, it doesn't need to roundtrip parse it.
So, making Git.FileName's roundtrip test only chars < 256 seems fine.
Utility.Format.format uses encode_c, in order to mimic git, so that's
ok.
Utility.Format.gen uses decode_c, but only so that stuff like "\n"
in the format string is handled. If the format string contains C-style
octal escapes, they will be converted to ascii characters, and not
combined into unicode characters, but that should not be a problem.
If the user wants unicode characters, they can include them in the
format string, without escaping them.
Finally, decode_c is used by Utility.Gpg.secretKeys, because gpg
--with-colons hex-escapes some characters in particular ':' and '\\'.
gpg passes unicode through, so this use of decode_c is not a problem.
This commit was sponsored by Henrik Riomar on Patreon.
2017-06-17 20:17:09 +00:00
|
|
|
|
-- If the userid contains a ":" or a few other special
|
|
|
|
|
-- characters, gpg will hex-escape it. Use decode_c to
|
|
|
|
|
-- undo.
|
2013-09-16 16:57:39 +00:00
|
|
|
|
extract ((keyid, decode_c userid):c) Nothing rest
|
2016-09-14 17:30:50 +00:00
|
|
|
|
extract c (Just keyid) rest@(("sec":_):_) =
|
2013-09-16 16:57:39 +00:00
|
|
|
|
extract ((keyid, ""):c) Nothing rest
|
2016-09-14 17:30:50 +00:00
|
|
|
|
extract c (Just keyid) (_:rest) =
|
|
|
|
|
extract c (Just keyid) rest
|
2013-09-16 16:57:39 +00:00
|
|
|
|
extract c _ [] = c
|
|
|
|
|
extract c _ (("sec":_:_:_:keyid:_):rest) =
|
|
|
|
|
extract c (Just keyid) rest
|
|
|
|
|
extract c k (_:rest) =
|
|
|
|
|
extract c k rest
|
|
|
|
|
|
2013-09-16 17:22:43 +00:00
|
|
|
|
type Passphrase = String
|
|
|
|
|
type Size = Int
|
|
|
|
|
data KeyType = Algo Int | DSA | RSA
|
|
|
|
|
|
2013-09-17 19:36:15 +00:00
|
|
|
|
{- The maximum key size that gpg currently offers in its UI when
|
|
|
|
|
- making keys. -}
|
|
|
|
|
maxRecommendedKeySize :: Size
|
|
|
|
|
maxRecommendedKeySize = 4096
|
|
|
|
|
|
2013-09-16 17:22:43 +00:00
|
|
|
|
{- Generates a secret key using the experimental batch mode.
|
|
|
|
|
- The key is added to the secret key ring.
|
|
|
|
|
- Can take a very long time, depending on system entropy levels.
|
|
|
|
|
-}
|
2015-09-09 22:06:49 +00:00
|
|
|
|
genSecretKey :: GpgCmd -> KeyType -> Passphrase -> UserId -> Size -> IO ()
|
|
|
|
|
genSecretKey (GpgCmd cmd) keytype passphrase userid keysize =
|
2020-06-04 19:36:34 +00:00
|
|
|
|
let p = (proc cmd params)
|
|
|
|
|
{ std_in = CreatePipe }
|
|
|
|
|
in withCreateProcess p (go p)
|
2013-09-16 17:22:43 +00:00
|
|
|
|
where
|
|
|
|
|
params = ["--batch", "--gen-key"]
|
2020-06-04 19:36:34 +00:00
|
|
|
|
|
|
|
|
|
go p (Just h) _ _ pid = do
|
2013-09-17 19:36:15 +00:00
|
|
|
|
hPutStr h $ unlines $ catMaybes
|
|
|
|
|
[ Just $ "Key-Type: " ++
|
2013-09-16 17:22:43 +00:00
|
|
|
|
case keytype of
|
|
|
|
|
DSA -> "DSA"
|
|
|
|
|
RSA -> "RSA"
|
|
|
|
|
Algo n -> show n
|
2013-09-17 19:36:15 +00:00
|
|
|
|
, Just $ "Key-Length: " ++ show keysize
|
|
|
|
|
, Just $ "Name-Real: " ++ userid
|
2014-04-26 23:25:05 +00:00
|
|
|
|
, Just "Expire-Date: 0"
|
2013-09-17 19:36:15 +00:00
|
|
|
|
, if null passphrase
|
|
|
|
|
then Nothing
|
|
|
|
|
else Just $ "Passphrase: " ++ passphrase
|
2013-09-16 17:22:43 +00:00
|
|
|
|
]
|
|
|
|
|
hClose h
|
2020-06-04 19:36:34 +00:00
|
|
|
|
forceSuccessProcess p pid
|
|
|
|
|
go _ _ _ _ _ = error "internal"
|
2013-09-16 17:22:43 +00:00
|
|
|
|
|
2012-04-29 18:02:18 +00:00
|
|
|
|
{- Creates a block of high-quality random data suitable to use as a cipher.
|
|
|
|
|
- It is armored, to avoid newlines, since gpg only reads ciphers up to the
|
2013-03-04 00:39:01 +00:00
|
|
|
|
- first newline. -}
|
2015-09-09 22:06:49 +00:00
|
|
|
|
genRandom :: GpgCmd -> Bool -> Size -> IO String
|
|
|
|
|
genRandom cmd highQuality size = checksize <$> readStrict cmd params
|
2012-12-13 04:24:19 +00:00
|
|
|
|
where
|
2015-06-01 17:52:23 +00:00
|
|
|
|
params =
|
|
|
|
|
[ Param "--gen-random"
|
|
|
|
|
, Param "--armor"
|
|
|
|
|
, Param $ show randomquality
|
|
|
|
|
, Param $ show size
|
|
|
|
|
]
|
2013-03-04 00:39:01 +00:00
|
|
|
|
|
2013-04-05 19:06:16 +00:00
|
|
|
|
-- See http://www.gnupg.org/documentation/manuals/gcrypt/Quality-of-random-numbers.html
|
|
|
|
|
-- for the meaning of random quality levels.
|
|
|
|
|
-- The highest available is 2, which is the default for OpenPGP
|
|
|
|
|
-- key generation; Note that it uses the blocking PRNG /dev/random
|
|
|
|
|
-- on the Linux kernel, hence the running time may take a while.
|
|
|
|
|
randomquality :: Int
|
|
|
|
|
randomquality = if highQuality then 2 else 1
|
2011-12-21 03:20:36 +00:00
|
|
|
|
|
2014-10-09 18:53:13 +00:00
|
|
|
|
{- The size is the number of bytes of entropy desired; the data is
|
2013-03-04 00:39:01 +00:00
|
|
|
|
- base64 encoded, so needs 8 bits to represent every 6 bytes of
|
|
|
|
|
- entropy. -}
|
|
|
|
|
expectedlength = size * 8 `div` 6
|
|
|
|
|
|
|
|
|
|
checksize s = let len = length s in
|
|
|
|
|
if len >= expectedlength
|
|
|
|
|
then s
|
|
|
|
|
else shortread len
|
|
|
|
|
|
2016-11-16 01:29:54 +00:00
|
|
|
|
shortread got = giveup $ unwords
|
2015-06-01 17:52:23 +00:00
|
|
|
|
[ "Not enough bytes returned from gpg", show params
|
2013-03-04 00:39:01 +00:00
|
|
|
|
, "(got", show got, "; expected", show expectedlength, ")"
|
|
|
|
|
]
|
|
|
|
|
|
2011-12-21 03:20:36 +00:00
|
|
|
|
{- A test key. This is provided pre-generated since generating a new gpg
|
|
|
|
|
- key is too much work (requires too much entropy) for a test suite to
|
|
|
|
|
- do.
|
|
|
|
|
-
|
|
|
|
|
- This key was generated with no exipiration date, and a small keysize.
|
|
|
|
|
- It has an empty passphrase. -}
|
|
|
|
|
testKeyId :: String
|
|
|
|
|
testKeyId = "129D6E0AC537B9C7"
|
2019-11-21 19:38:06 +00:00
|
|
|
|
|
2011-12-21 03:20:36 +00:00
|
|
|
|
testKey :: String
|
|
|
|
|
testKey = keyBlock True
|
|
|
|
|
[ "mI0ETvFAZgEEAKnqwWgZqznMhi1RQExem2H8t3OyKDxaNN3rBN8T6LWGGqAYV4wT"
|
|
|
|
|
, "r8In5tfsnz64bKpE1Qi68JURFwYmthgUL9N48tbODU8t3xzijdjLOSaTyqkH1ik6"
|
|
|
|
|
, "EyulfKN63xLne9i4F9XqNwpiZzukXYbNfHkDA2yb0M6g4UFKLY/fNzGXABEBAAG0"
|
|
|
|
|
, "W2luc2VjdXJlIHRlc3Qga2V5ICh0aGlzIGlzIGEgdGVzdCBrZXksIGRvIG5vdCB1"
|
|
|
|
|
, "c2UgZm9yIGFjdHVhbCBlbmNyeXB0aW9uKSA8dGVzdEBleGFtcGxlLmNvbT6IuAQT"
|
|
|
|
|
, "AQgAIgUCTvFAZgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQEp1uCsU3"
|
|
|
|
|
, "uceQ9wP/YMd1f0+/eLLcwGXNBvGqyVhUOfAKknO1bMzGbqTsq9g60qegy/cldqee"
|
|
|
|
|
, "xVxNfy0VN//JeMfgdcb8+RgJYLoaMrTy9CcsUcFPxtwN9tcLmsM0V2/fNmmFBO9t"
|
|
|
|
|
, "v75iH+zeFbNg0/FbPkHiN6Mjw7P2gXYKQXgTvQZBWaphk8oQlBm4jQRO8UBmAQQA"
|
|
|
|
|
, "vdi50M/WRCkOLt2RsUve8V8brMWYTJBJTTWoHUeRr82v4NCdX7OE1BsoVK8cy/1Q"
|
|
|
|
|
, "Y+gLOH9PqinuGGNWRmPV2Ju/RYn5H7sdewXA8E80xWhc4phHRMJ8Jjhg/GVPamkJ"
|
|
|
|
|
, "8B5zeKF0jcLFl7cuVdOyQakhoeDWJd0CyfW837nmPtMAEQEAAYifBBgBCAAJBQJO"
|
|
|
|
|
, "8UBmAhsMAAoJEBKdbgrFN7nHclAEAKBShuP/toH03atDUQTbGE34CA4yEC9BVghi"
|
|
|
|
|
, "7kviOZlOz2s8xAfp/8AYsrECx1kgbXcA7JD902eNyp7NzXsdJX0zJwHqiuZW0XlD"
|
|
|
|
|
, "T8ZJu4qrYRYgl/790WPESZ+ValvHD/fqkR38RF4tfxvyoMhhp0roGmJY33GASIG/"
|
|
|
|
|
, "+gQkDF9/"
|
|
|
|
|
, "=1k11"
|
|
|
|
|
]
|
2019-11-21 19:38:06 +00:00
|
|
|
|
|
2011-12-21 03:20:36 +00:00
|
|
|
|
testSecretKey :: String
|
|
|
|
|
testSecretKey = keyBlock False
|
|
|
|
|
[ "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"
|
|
|
|
|
, "=LDsg"
|
|
|
|
|
]
|
2019-11-21 19:38:06 +00:00
|
|
|
|
|
2011-12-21 03:20:36 +00:00
|
|
|
|
keyBlock :: Bool -> [String] -> String
|
|
|
|
|
keyBlock public ls = unlines
|
|
|
|
|
[ "-----BEGIN PGP "++t++" KEY BLOCK-----"
|
|
|
|
|
, "Version: GnuPG v1.4.11 (GNU/Linux)"
|
|
|
|
|
, ""
|
|
|
|
|
, unlines ls
|
|
|
|
|
, "-----END PGP "++t++" KEY BLOCK-----"
|
|
|
|
|
]
|
2012-12-13 04:24:19 +00:00
|
|
|
|
where
|
|
|
|
|
t
|
|
|
|
|
| public = "PUBLIC"
|
|
|
|
|
| otherwise = "PRIVATE"
|
2011-12-21 03:20:36 +00:00
|
|
|
|
|
2013-05-10 21:57:21 +00:00
|
|
|
|
#ifndef mingw32_HOST_OS
|
2011-12-21 03:20:36 +00:00
|
|
|
|
{- Runs an action using gpg in a test harness, in which gpg does
|
2019-01-21 18:00:21 +00:00
|
|
|
|
- not use ~/.gpg/, but sets up the test key in a subdirectory of
|
2020-04-28 19:47:23 +00:00
|
|
|
|
- the passed directory and uses it.
|
|
|
|
|
-
|
|
|
|
|
- If the test harness is not able to be set up (eg, because gpg is not
|
|
|
|
|
- installed or because there is some problem importing the test key,
|
|
|
|
|
- perhaps related to the agent socket), the action is not run, and Nothing
|
|
|
|
|
- is returned.
|
|
|
|
|
-}
|
|
|
|
|
testHarness :: FilePath -> GpgCmd -> IO a -> IO (Maybe a)
|
|
|
|
|
testHarness tmpdir cmd a = ifM (inPath (unGpgCmd cmd))
|
|
|
|
|
( bracket (eitherToMaybe <$> tryNonAsync setup) cleanup go
|
|
|
|
|
, return Nothing
|
|
|
|
|
)
|
2012-12-13 04:24:19 +00:00
|
|
|
|
where
|
2020-08-28 18:28:42 +00:00
|
|
|
|
var = "GNUPGHOME"
|
2011-12-21 03:20:36 +00:00
|
|
|
|
|
2018-12-19 15:56:39 +00:00
|
|
|
|
setup = do
|
2017-05-16 03:32:17 +00:00
|
|
|
|
orig <- getEnv var
|
2018-12-19 15:56:39 +00:00
|
|
|
|
subdir <- makenewdir (1 :: Integer)
|
|
|
|
|
-- gpg is picky about permissions on its home dir
|
|
|
|
|
liftIO $ void $ tryIO $ modifyFileMode subdir $
|
|
|
|
|
removeModes $ otherGroupModes
|
|
|
|
|
setEnv var subdir True
|
2014-01-07 17:20:07 +00:00
|
|
|
|
-- For some reason, recent gpg needs a trustdb to be set up.
|
2015-09-09 22:06:49 +00:00
|
|
|
|
_ <- pipeStrict cmd [Param "--trust-model", Param "auto", Param "--update-trustdb"] []
|
|
|
|
|
_ <- pipeStrict cmd [Param "--import", Param "-q"] $ unlines
|
2012-12-13 04:24:19 +00:00
|
|
|
|
[testSecretKey, testKey]
|
2017-05-16 03:32:17 +00:00
|
|
|
|
return orig
|
2011-12-21 03:20:36 +00:00
|
|
|
|
|
2020-08-28 18:28:42 +00:00
|
|
|
|
cleanup (Just (Just v)) = stopgpgagent >> setEnv var v True
|
|
|
|
|
cleanup (Just Nothing) = stopgpgagent >> unsetEnv var
|
|
|
|
|
cleanup Nothing = stopgpgagent
|
|
|
|
|
|
|
|
|
|
-- Recent versions of gpg automatically start gpg-agent, or perhaps
|
|
|
|
|
-- other daemons. Stop them when done. This only affects
|
|
|
|
|
-- daemons started for the GNUPGHOME that was used.
|
|
|
|
|
-- Older gpg may not support this, so ignore failure.
|
2020-09-08 16:21:24 +00:00
|
|
|
|
stopgpgagent = whenM (inPath "gpgconf") $
|
|
|
|
|
void $ boolSystem "gpgconf" [Param "--kill", Param "all"]
|
2020-04-28 19:47:23 +00:00
|
|
|
|
|
|
|
|
|
go (Just _) = Just <$> a
|
|
|
|
|
go Nothing = return Nothing
|
2018-12-19 15:56:39 +00:00
|
|
|
|
|
|
|
|
|
makenewdir n = do
|
|
|
|
|
let subdir = tmpdir </> show n
|
|
|
|
|
catchIOErrorType AlreadyExists (const $ makenewdir $ n + 1) $ do
|
|
|
|
|
createDirectory subdir
|
|
|
|
|
return subdir
|
2011-12-21 03:20:36 +00:00
|
|
|
|
|
|
|
|
|
{- Tests the test harness. -}
|
2018-12-19 15:56:39 +00:00
|
|
|
|
testTestHarness :: FilePath -> GpgCmd -> IO Bool
|
2020-04-28 19:47:23 +00:00
|
|
|
|
testTestHarness tmpdir cmd =
|
|
|
|
|
testHarness tmpdir cmd (findPubKeys cmd testKeyId) >>= \case
|
|
|
|
|
Nothing -> do
|
|
|
|
|
hPutStrLn stderr "unable to test gpg, setting up the test harness did not succeed"
|
|
|
|
|
return True
|
|
|
|
|
Just keys -> return $ KeyIds [testKeyId] == keys
|
2013-09-01 18:12:00 +00:00
|
|
|
|
|
2015-09-09 22:06:49 +00:00
|
|
|
|
checkEncryptionFile :: GpgCmd -> FilePath -> Maybe KeyIds -> IO Bool
|
|
|
|
|
checkEncryptionFile cmd filename keys =
|
|
|
|
|
checkGpgPackets cmd keys =<< readStrict cmd params
|
2013-09-01 18:12:00 +00:00
|
|
|
|
where
|
2015-06-01 17:52:23 +00:00
|
|
|
|
params = [Param "--list-packets", Param "--list-only", File filename]
|
2013-09-01 18:12:00 +00:00
|
|
|
|
|
2015-09-09 22:06:49 +00:00
|
|
|
|
checkEncryptionStream :: GpgCmd -> String -> Maybe KeyIds -> IO Bool
|
|
|
|
|
checkEncryptionStream cmd stream keys =
|
|
|
|
|
checkGpgPackets cmd keys =<< pipeStrict cmd params stream
|
2013-09-01 18:12:00 +00:00
|
|
|
|
where
|
2015-06-01 17:52:23 +00:00
|
|
|
|
params = [Param "--list-packets", Param "--list-only"]
|
2013-09-01 18:12:00 +00:00
|
|
|
|
|
|
|
|
|
{- Parses an OpenPGP packet list, and checks whether data is
|
|
|
|
|
- symmetrically encrypted (keys is Nothing), or encrypted to some
|
|
|
|
|
- public key(s).
|
|
|
|
|
- /!\ The key needs to be in the keyring! -}
|
2015-09-09 22:06:49 +00:00
|
|
|
|
checkGpgPackets :: GpgCmd -> Maybe KeyIds -> String -> IO Bool
|
|
|
|
|
checkGpgPackets cmd keys str = do
|
2013-09-01 18:12:00 +00:00
|
|
|
|
let (asym,sym) = partition (pubkeyEncPacket `isPrefixOf`) $
|
|
|
|
|
filter (\l' -> pubkeyEncPacket `isPrefixOf` l' ||
|
|
|
|
|
symkeyEncPacket `isPrefixOf` l') $
|
|
|
|
|
takeWhile (/= ":encrypted data packet:") $
|
|
|
|
|
lines str
|
|
|
|
|
case (keys,asym,sym) of
|
|
|
|
|
(Nothing, [], [_]) -> return True
|
|
|
|
|
(Just (KeyIds ks), ls, []) -> do
|
|
|
|
|
-- Find the master key associated with the
|
|
|
|
|
-- encryption subkey.
|
2015-09-09 22:06:49 +00:00
|
|
|
|
ks' <- concat <$> mapM (keyIds <$$> findPubKeys cmd)
|
2013-09-01 18:12:00 +00:00
|
|
|
|
[ k | k:"keyid":_ <- map (reverse . words) ls ]
|
|
|
|
|
return $ sort (nub ks) == sort (nub ks')
|
|
|
|
|
_ -> return False
|
|
|
|
|
where
|
|
|
|
|
pubkeyEncPacket = ":pubkey enc packet: "
|
|
|
|
|
symkeyEncPacket = ":symkey enc packet: "
|
|
|
|
|
#endif
|