/ Crypto.hs
Crypto.hs
1 {- git-annex crypto 2 - 3 - Currently using gpg by default, or optionally stateless OpenPGP. 4 - 5 - Copyright 2011-2024 Joey Hess <id@joeyh.name> 6 - 7 - Licensed under the GNU AGPL version 3 or higher. 8 -} 9 10 {-# LANGUAGE FlexibleInstances #-} 11 {-# LANGUAGE OverloadedStrings #-} 12 {-# LANGUAGE Rank2Types #-} 13 14 module Crypto ( 15 EncryptionMethod(..), 16 Cipher, 17 KeyIds(..), 18 EncKey, 19 StorableCipher(..), 20 genEncryptedCipher, 21 genSharedCipher, 22 genSharedPubKeyCipher, 23 updateCipherKeyIds, 24 decryptCipher, 25 decryptCipher', 26 encryptKey, 27 isEncKey, 28 feedBytes, 29 readBytes, 30 readBytesStrictly, 31 encrypt, 32 decrypt, 33 LensEncParams(..), 34 35 prop_HmacSha1WithCipher_sane 36 ) where 37 38 import qualified Data.ByteString as S 39 import qualified Data.ByteString.Lazy as L 40 import Control.Monad.IO.Class 41 import qualified Data.ByteString.Short as S (toShort) 42 43 import Annex.Common 44 import qualified Utility.Gpg as Gpg 45 import qualified Utility.StatelessOpenPGP as SOP 46 import Types.Crypto 47 import Types.Remote 48 import Types.Key 49 import Annex.SpecialRemote.Config 50 import Utility.Tmp.Dir 51 52 {- The number of bytes of entropy used to generate a Cipher. 53 - 54 - Since a Cipher is base-64 encoded, the actual size of a Cipher 55 - is larger than this. 512 bytes of date base-64 encodes to 684 56 - characters. 57 -} 58 cipherSize :: Int 59 cipherSize = 512 60 61 {- The beginning of a Cipher is used for MAC'ing; the remainder is used 62 - as the symmetric encryption passphrase. 63 - 64 - Due to the base-64 encoding of the Cipher, the beginning 265 characters 65 - represent at best 192 bytes of entropy. However that's more than enough 66 - for both the default MAC algorithm, namely HMAC-SHA1, and the "strongest" 67 - currently supported, namely HMAC-SHA512, which respectively need 68 - (ideally) 64 and 128 bytes of entropy. 69 - 70 - The remaining characters (320 bytes of entropy) is enough for 71 - the symmetric encryption passphrase; unlike weaker public key crypto, 72 - that does not need to be too large. 73 -} 74 cipherBeginning :: Int 75 cipherBeginning = 256 76 77 cipherPassphrase :: Cipher -> S.ByteString 78 cipherPassphrase (Cipher c) = S.drop cipherBeginning c 79 cipherPassphrase (MacOnlyCipher _) = giveup "MAC-only cipher" 80 81 cipherMac :: Cipher -> S.ByteString 82 cipherMac (Cipher c) = S.take cipherBeginning c 83 cipherMac (MacOnlyCipher c) = c 84 85 {- Creates a new Cipher, encrypted to the specified key id. -} 86 genEncryptedCipher :: LensEncParams c => Gpg.GpgCmd -> c -> Gpg.KeyId -> EncryptedCipherVariant -> Bool -> IO StorableCipher 87 genEncryptedCipher cmd c keyid variant highQuality = do 88 ks <- Gpg.findPubKeys cmd keyid 89 random <- Gpg.genRandom cmd highQuality size 90 encryptCipher cmd c (mkCipher random) variant ks 91 where 92 (mkCipher, size) = case variant of 93 Hybrid -> (Cipher, cipherSize) -- used for MAC + symmetric 94 PubKey -> (MacOnlyCipher, cipherBeginning) -- only used for MAC 95 96 {- Creates a new, shared Cipher. -} 97 genSharedCipher :: Gpg.GpgCmd -> Bool -> IO StorableCipher 98 genSharedCipher cmd highQuality = 99 SharedCipher <$> Gpg.genRandom cmd highQuality cipherSize 100 101 {- Creates a new, shared Cipher, and looks up the gpg public key that will 102 - be used for encrypting content. -} 103 genSharedPubKeyCipher :: Gpg.GpgCmd -> Gpg.KeyId -> Bool -> IO StorableCipher 104 genSharedPubKeyCipher cmd keyid highQuality = do 105 ks <- Gpg.findPubKeys cmd keyid 106 random <- Gpg.genRandom cmd highQuality cipherSize 107 return $ SharedPubKeyCipher random ks 108 109 {- Updates an existing Cipher, making changes to its keyids. 110 - 111 - When the Cipher is encrypted, re-encrypts it. -} 112 updateCipherKeyIds :: LensEncParams encparams => Gpg.GpgCmd -> encparams -> [(Bool, Gpg.KeyId)] -> StorableCipher -> IO StorableCipher 113 updateCipherKeyIds _ _ _ SharedCipher{} = giveup "Cannot update shared cipher" 114 updateCipherKeyIds _ _ [] c = return c 115 updateCipherKeyIds cmd encparams changes encipher@(EncryptedCipher _ variant ks) = do 116 ks' <- updateCipherKeyIds' cmd changes ks 117 cipher <- decryptCipher cmd encparams encipher 118 encryptCipher cmd encparams cipher variant ks' 119 updateCipherKeyIds cmd _ changes (SharedPubKeyCipher cipher ks) = 120 SharedPubKeyCipher cipher <$> updateCipherKeyIds' cmd changes ks 121 122 updateCipherKeyIds' :: Gpg.GpgCmd -> [(Bool, Gpg.KeyId)] -> KeyIds -> IO KeyIds 123 updateCipherKeyIds' cmd changes (KeyIds ks) = do 124 dropkeys <- listKeyIds [ k | (False, k) <- changes ] 125 forM_ dropkeys $ \k -> unless (k `elem` ks) $ 126 giveup $ "Key " ++ k ++ " was not present; cannot remove." 127 addkeys <- listKeyIds [ k | (True, k) <- changes ] 128 let ks' = (addkeys ++ ks) \\ dropkeys 129 when (null ks') $ 130 giveup "Cannot remove the last key." 131 return $ KeyIds ks' 132 where 133 listKeyIds = concat <$$> mapM (keyIds <$$> Gpg.findPubKeys cmd) 134 135 {- Encrypts a Cipher to the specified KeyIds. -} 136 encryptCipher :: LensEncParams c => Gpg.GpgCmd -> c -> Cipher -> EncryptedCipherVariant -> KeyIds -> IO StorableCipher 137 encryptCipher cmd c cip variant (KeyIds ks) = do 138 -- gpg complains about duplicate recipient keyids 139 let ks' = nub $ sort ks 140 let params = concat 141 [ getGpgEncParamsBase c 142 , Gpg.pkEncTo ks' 143 , Gpg.stdEncryptionParams False 144 ] 145 encipher <- Gpg.pipeStrict cmd params cipher 146 return $ EncryptedCipher encipher variant (KeyIds ks') 147 where 148 cipher = case cip of 149 Cipher x -> x 150 MacOnlyCipher x -> x 151 152 {- Decrypting an EncryptedCipher is expensive; the Cipher should be cached. -} 153 decryptCipher :: LensEncParams c => Gpg.GpgCmd -> c -> StorableCipher -> IO Cipher 154 decryptCipher cmd c cip = decryptCipher' cmd Nothing c cip 155 156 decryptCipher' :: LensEncParams c => Gpg.GpgCmd -> Maybe [(String, String)] -> c -> StorableCipher -> IO Cipher 157 decryptCipher' _ _ _ (SharedCipher t) = return $ Cipher t 158 decryptCipher' _ _ _ (SharedPubKeyCipher t _) = return $ MacOnlyCipher t 159 decryptCipher' cmd environ c (EncryptedCipher t variant _) = 160 mkCipher <$> Gpg.pipeStrict' cmd params environ t 161 where 162 mkCipher = case variant of 163 Hybrid -> Cipher 164 PubKey -> MacOnlyCipher 165 params = Param "--decrypt" : getGpgDecParams c 166 167 type EncKey = Key -> Key 168 169 {- Generates an encrypted form of a Key. The encryption does not need to be 170 - reversible, nor does it need to be the same type of encryption used 171 - on content. It does need to be repeatable. -} 172 encryptKey :: Mac -> Cipher -> EncKey 173 encryptKey mac c k = mkKey $ \d -> d 174 { keyName = S.toShort $ encodeBS $ macWithCipher mac c (serializeKey' k) 175 , keyVariety = OtherKey $ 176 encryptedBackendNamePrefix <> encodeBS (showMac mac) 177 } 178 179 encryptedBackendNamePrefix :: S.ByteString 180 encryptedBackendNamePrefix = "GPG" 181 182 isEncKey :: Key -> Bool 183 isEncKey k = case fromKey keyVariety k of 184 OtherKey s -> encryptedBackendNamePrefix `S.isPrefixOf` s 185 _ -> False 186 187 type Feeder = Handle -> IO () 188 type Reader m a = Handle -> m a 189 190 feedBytes :: L.ByteString -> Feeder 191 feedBytes = flip L.hPut 192 193 readBytes :: (MonadIO m) => (L.ByteString -> m a) -> Reader m a 194 readBytes a h = liftIO (L.hGetContents h) >>= a 195 196 readBytesStrictly :: (MonadIO m) => (S.ByteString -> m a) -> Reader m a 197 readBytesStrictly a h = liftIO (S.hGetContents h) >>= a 198 199 {- Runs a Feeder action, that generates content that is symmetrically 200 - encrypted with the Cipher (unless it is empty, in which case 201 - public-key encryption is used), and then read by the Reader action. 202 - 203 - Note that the Reader must fully consume all input before returning. 204 -} 205 encrypt :: (MonadIO m, MonadMask m, LensEncParams c) => Gpg.GpgCmd -> c -> Cipher -> Feeder -> Reader m a -> m a 206 encrypt gpgcmd c cipher feeder reader = case cipher of 207 Cipher{} -> 208 let passphrase = cipherPassphrase cipher 209 in case statelessOpenPGPCommand c of 210 Just sopcmd -> withTmpDir (literalOsPath "sop") $ \d -> 211 SOP.encryptSymmetric sopcmd passphrase 212 (SOP.EmptyDirectory d) 213 (statelessOpenPGPProfile c) 214 (SOP.Armoring False) 215 feeder reader 216 Nothing -> Gpg.feedRead gpgcmd (params ++ Gpg.stdEncryptionParams True) passphrase feeder reader 217 MacOnlyCipher{} -> Gpg.feedRead' gpgcmd (params ++ Gpg.stdEncryptionParams False) feeder reader 218 where 219 params = getGpgEncParams c 220 221 {- Runs a Feeder action, that generates content that is decrypted with the 222 - Cipher (or using a private key if the Cipher is empty), and read by the 223 - Reader action. 224 - 225 - Note that the Reader must fully consume all input before returning. 226 - -} 227 decrypt :: (MonadIO m, MonadMask m, LensEncParams c) => Gpg.GpgCmd -> c -> Cipher -> Feeder -> Reader m a -> m a 228 decrypt cmd c cipher feeder reader = case cipher of 229 Cipher{} -> 230 let passphrase = cipherPassphrase cipher 231 in case statelessOpenPGPCommand c of 232 Just sopcmd -> withTmpDir (literalOsPath "sop") $ \d -> 233 SOP.decryptSymmetric sopcmd passphrase 234 (SOP.EmptyDirectory d) 235 feeder reader 236 Nothing -> Gpg.feedRead cmd params passphrase feeder reader 237 MacOnlyCipher{} -> Gpg.feedRead' cmd params feeder reader 238 where 239 params = Param "--decrypt" : getGpgDecParams c 240 241 macWithCipher :: Mac -> Cipher -> S.ByteString -> String 242 macWithCipher mac c = macWithCipher' mac (cipherMac c) 243 macWithCipher' :: Mac -> S.ByteString -> S.ByteString -> String 244 macWithCipher' mac c s = calcMac show mac c s 245 246 {- Ensure that macWithCipher' returns the same thing forevermore. -} 247 prop_HmacSha1WithCipher_sane :: Bool 248 prop_HmacSha1WithCipher_sane = known_good == macWithCipher' HmacSha1 "foo" "bar" 249 where 250 known_good = "46b4ec586117154dacd49d664e5d63fdc88efb51" 251 252 class LensEncParams a where 253 {- Base gpg parameters for encrypting. Does not include specification 254 - of recipient keys. -} 255 getGpgEncParamsBase :: a -> [CommandParam] 256 {- Gpg parameters for encrypting. When the remote is configured to use 257 - public-key encryption, includes specification of recipient keys. -} 258 getGpgEncParams :: a -> [CommandParam] 259 {- Gpg parameters for decrypting. -} 260 getGpgDecParams :: a -> [CommandParam] 261 {- Set when stateless OpenPGP should be used rather than gpg. 262 - It is currently only used for SharedEncryption and not the other 263 - schemes which use public keys. -} 264 statelessOpenPGPCommand :: a -> Maybe SOP.SOPCmd 265 {- When using stateless OpenPGP, this may be set to a profile 266 - which should be used instead of the default. -} 267 statelessOpenPGPProfile :: a -> Maybe SOP.SOPProfile 268 269 {- Extract the GnuPG options from a pair of a Remote Config and a Remote 270 - Git Config. -} 271 instance LensEncParams (ParsedRemoteConfig, RemoteGitConfig) where 272 getGpgEncParamsBase (_c,gc) = map Param (remoteAnnexGnupgOptions gc) 273 getGpgEncParams (c,gc) = getGpgEncParamsBase (c,gc) ++ 274 {- When the remote is configured to use public-key encryption, 275 - look up the recipient keys and add them to the option list. -} 276 case getRemoteConfigValue encryptionField c of 277 Just PubKeyEncryption -> 278 Gpg.pkEncTo $ maybe [] (splitc ',') $ 279 getRemoteConfigValue cipherkeysField c 280 Just SharedPubKeyEncryption -> 281 Gpg.pkEncTo $ maybe [] (splitc ',') $ 282 getRemoteConfigValue pubkeysField c 283 _ -> [] 284 getGpgDecParams (_c,gc) = map Param (remoteAnnexGnupgDecryptOptions gc) 285 statelessOpenPGPCommand (c,gc) = case remoteAnnexSharedSOPCommand gc of 286 Nothing -> Nothing 287 Just sopcmd -> 288 {- So far stateless OpenPGP is only supported 289 - for SharedEncryption, not other encryption 290 - methods that involve public keys. -} 291 case getRemoteConfigValue encryptionField c of 292 Just SharedEncryption -> Just sopcmd 293 _ -> Nothing 294 statelessOpenPGPProfile (_c,gc) = remoteAnnexSharedSOPProfile gc 295 296 {- Extract the GnuPG options from a Remote. -} 297 instance LensEncParams (RemoteA a) where 298 getGpgEncParamsBase r = getGpgEncParamsBase (config r, gitconfig r) 299 getGpgEncParams r = getGpgEncParams (config r, gitconfig r) 300 getGpgDecParams r = getGpgDecParams (config r, gitconfig r) 301 statelessOpenPGPCommand r = statelessOpenPGPCommand (config r, gitconfig r) 302 statelessOpenPGPProfile r = statelessOpenPGPProfile (config r, gitconfig r)