{- Stateless OpenPGP interface - - Copyright 2011-2024 Joey Hess - - 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 presence 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"