git-annex/Utility/StatelessOpenPGP.hs
2024-03-07 19:19:45 -04:00

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"