git-annex/Utility/StatelessOpenPGP.hs
Joey Hess 7e69063a29
support annex.shared-sop-command for encryption=shared
This works well, and it interoperates with gpg in my testing (although some
SOP commands might choose to use a profile that does not so caveat emptor).

Note that for creating the Cipher, gpg --gen-random is still used. SOP
does not have an eqivilant, and as long as the user has gpg around,
which seems likely, it doesn't matter that it uses gpg here, it's not being
used for encryption. That seemed better than implementing a second way
to get high quality entropy, at least for now.

The need for the sop command to run in an empty directory has each call
to encrypt and decrypt creating a new temporary directory. That is some
unncessary overhead, though probably swamped by the overhead of running
the sop command. This could be improved in the future by passing an
already empty directory to them, or a sufficiently empty directory
(.git/annex/tmp would probably suffice).

Sponsored-by: Brett Eisenberg on Patreon
2024-01-12 13:31:18 -04:00

202 lines
5.9 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 Control.Concurrent.Async
import Control.Monad.IO.Class
import qualified Data.ByteString as B
{- 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
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
}
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"