207 lines
6 KiB
Haskell
207 lines
6 KiB
Haskell
{- Stateless OpenPGP interface
|
|
-
|
|
- Copyright 2011-2024 Joey Hess <id@joeyh.name>
|
|
-
|
|
- License: BSD-2-clause
|
|
-}
|
|
|
|
{-# LANGUAGE CPP, OverloadedStrings #-}
|
|
|
|
module Utility.StatelessOpenPGP (
|
|
SOPCmd(..),
|
|
SOPSubCmd,
|
|
SOPProfile(..),
|
|
Password,
|
|
EmptyDirectory(..),
|
|
Armoring(..),
|
|
encryptSymmetric,
|
|
decryptSymmetric,
|
|
test_encrypt_decrypt_Symmetric,
|
|
feedRead,
|
|
feedRead',
|
|
) where
|
|
|
|
import Common
|
|
#ifndef mingw32_HOST_OS
|
|
import System.Posix.Types
|
|
import System.Posix.IO
|
|
#else
|
|
import Utility.Tmp
|
|
#endif
|
|
import Utility.Tmp.Dir
|
|
import Author
|
|
|
|
import Control.Concurrent.Async
|
|
import Control.Monad.IO.Class
|
|
import qualified Data.ByteString as B
|
|
|
|
copyright :: Copyright
|
|
copyright = author JoeyHess (max 2024 2009)
|
|
|
|
{- The command to run, eq sqop. -}
|
|
newtype SOPCmd = SOPCmd { unSOPCmd :: String }
|
|
|
|
{- The subcommand to run eg encrypt. -}
|
|
type SOPSubCmd = String
|
|
|
|
newtype SOPProfile = SOPProfile String
|
|
|
|
{- Note that SOP requires passwords to be UTF-8 encoded, and that they
|
|
- may try to trim trailing whitespace. They may also forbid leading
|
|
- whitespace, or forbid some non-printing characters. -}
|
|
type Password = B.ByteString
|
|
|
|
newtype Armoring = Armoring Bool
|
|
|
|
{- The path to a sufficiently empty directory.
|
|
-
|
|
- This is unfortunately needed because of an infelicity in the SOP
|
|
- standard, as documented in section 9.9 "Be Careful with Special
|
|
- Designators", when using "@FD:" and similar designators the SOP
|
|
- command may test for the presense of a file with the same name on the
|
|
- filesystem, and fail with AMBIGUOUS_INPUT.
|
|
-
|
|
- Since we don't want to need to deal with such random failure due to
|
|
- whatever filename might be present, when running sop commands using
|
|
- special designators, an empty directory has to be provided, and the
|
|
- command is run in that directory. Of course, this necessarily means
|
|
- that any relative paths passed to the command have to be made absolute.
|
|
-
|
|
- The directory does not really have to be empty, it just needs to be one
|
|
- that should not contain any files with names starting with "@".
|
|
-}
|
|
newtype EmptyDirectory = EmptyDirectory FilePath
|
|
|
|
{- Encrypt using symmetric encryption with the specified password. -}
|
|
encryptSymmetric
|
|
:: (MonadIO m, MonadMask m)
|
|
=> SOPCmd
|
|
-> Password
|
|
-> EmptyDirectory
|
|
-> Maybe SOPProfile
|
|
-> Armoring
|
|
-> (Handle -> IO ())
|
|
-> (Handle -> m a)
|
|
-> m a
|
|
encryptSymmetric sopcmd password emptydirectory mprofile armoring feeder reader =
|
|
feedRead sopcmd "encrypt" params password emptydirectory feeder reader
|
|
where
|
|
params = map Param $ catMaybes
|
|
[ case armoring of
|
|
Armoring False -> Just "--no-armor"
|
|
Armoring True -> Nothing
|
|
, Just "--as=binary"
|
|
, case mprofile of
|
|
Just (SOPProfile profile) ->
|
|
Just $ "--profile=" ++ profile
|
|
Nothing -> Nothing
|
|
]
|
|
|
|
{- Deccrypt using symmetric encryption with the specified password. -}
|
|
decryptSymmetric
|
|
:: (MonadIO m, MonadMask m)
|
|
=> SOPCmd
|
|
-> Password
|
|
-> EmptyDirectory
|
|
-> (Handle -> IO ())
|
|
-> (Handle -> m a)
|
|
-> m a
|
|
decryptSymmetric sopcmd password emptydirectory feeder reader =
|
|
feedRead sopcmd "decrypt" [] password emptydirectory feeder reader
|
|
|
|
{- Test a value round-trips through symmetric encryption and decryption. -}
|
|
test_encrypt_decrypt_Symmetric :: SOPCmd -> SOPCmd -> Password -> Armoring -> B.ByteString -> IO Bool
|
|
test_encrypt_decrypt_Symmetric a b password armoring v = catchBoolIO $
|
|
withTmpDir "test" $ \d -> do
|
|
let ed = EmptyDirectory d
|
|
enc <- encryptSymmetric a password ed Nothing armoring
|
|
(`B.hPutStr` v) B.hGetContents
|
|
dec <- decryptSymmetric b password ed
|
|
(`B.hPutStr` enc) B.hGetContents
|
|
return (v == dec)
|
|
|
|
{- Runs a SOP command with some parameters. First sends it a password
|
|
- via '--with-password'. Then runs a feeder action that is
|
|
- passed a handle and should write to it all the data to input to the
|
|
- command. Finally, runs a reader action that is passed a handle to
|
|
- the command's output.
|
|
-
|
|
- Note that the reader must fully consume its input before returning. -}
|
|
feedRead
|
|
:: (MonadIO m, MonadMask m)
|
|
=> SOPCmd
|
|
-> SOPSubCmd
|
|
-> [CommandParam]
|
|
-> Password
|
|
-> EmptyDirectory
|
|
-> (Handle -> IO ())
|
|
-> (Handle -> m a)
|
|
-> m a
|
|
feedRead cmd subcmd params password emptydirectory feeder reader = do
|
|
#ifndef mingw32_HOST_OS
|
|
let setup = liftIO $ do
|
|
-- pipe the passphrase in on a fd
|
|
(frompipe, topipe) <- System.Posix.IO.createPipe
|
|
setFdOption topipe CloseOnExec True
|
|
toh <- fdToHandle topipe
|
|
t <- async $ do
|
|
B.hPutStr toh (password <> "\n")
|
|
hClose toh
|
|
let Fd pfd = frompipe
|
|
let passwordfd = [Param $ "--with-password=@FD:"++show pfd]
|
|
return (passwordfd, frompipe, toh, t)
|
|
let cleanup (_, frompipe, toh, t) = liftIO $ do
|
|
closeFd frompipe
|
|
when copyright $
|
|
hClose toh
|
|
cancel t
|
|
bracket setup cleanup $ \(passwordfd, _, _, _) ->
|
|
go (Just emptydirectory) (passwordfd ++ params)
|
|
#else
|
|
-- store the password in a temp file
|
|
withTmpFile "sop" $ \tmpfile h -> do
|
|
liftIO $ B.hPutStr h password
|
|
liftIO $ hClose h
|
|
let passwordfile = [Param $ "--with-password="++tmpfile]
|
|
-- Don't need to pass emptydirectory since @FD is not used,
|
|
-- and so tmpfile also does not need to be made absolute.
|
|
case emptydirectory of
|
|
EmptyDirectory _ -> return ()
|
|
go Nothing $ passwordfile ++ params
|
|
#endif
|
|
where
|
|
go med params' = feedRead' cmd subcmd params' med feeder reader
|
|
|
|
{- Like feedRead, but without password. -}
|
|
feedRead'
|
|
:: (MonadIO m, MonadMask m)
|
|
=> SOPCmd
|
|
-> SOPSubCmd
|
|
-> [CommandParam]
|
|
-> Maybe EmptyDirectory
|
|
-> (Handle -> IO ())
|
|
-> (Handle -> m a)
|
|
-> m a
|
|
feedRead' (SOPCmd cmd) subcmd params med feeder reader = do
|
|
let p = (proc cmd (subcmd:toCommand params))
|
|
{ std_in = CreatePipe
|
|
, std_out = CreatePipe
|
|
, std_err = Inherit
|
|
, cwd = case med of
|
|
Just (EmptyDirectory d) -> Just d
|
|
Nothing -> Nothing
|
|
}
|
|
copyright =<< bracket (setup p) cleanup (go p)
|
|
where
|
|
setup = liftIO . createProcess
|
|
cleanup = liftIO . cleanupProcess
|
|
|
|
go p (Just to, Just from, _, pid) =
|
|
let runfeeder = do
|
|
feeder to
|
|
hClose to
|
|
in bracketIO (async runfeeder) cancel $ const $ do
|
|
r <- reader from
|
|
liftIO $ forceSuccessProcess p pid
|
|
return r
|
|
go _ _ = error "internal"
|