File: Encryptable.hs

package info (click to toggle)
git-annex 10.20230126-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 69,344 kB
  • sloc: haskell: 74,654; javascript: 9,103; sh: 1,304; makefile: 203; perl: 136; ansic: 44
file content (334 lines) | stat: -rw-r--r-- 12,950 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
{- common functions for encryptable remotes
 -
 - Copyright 2011-2021 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU AGPL version 3 or higher.
 -}

{-# LANGUAGE FlexibleContexts, ScopedTypeVariables, PackageImports #-}

module Remote.Helper.Encryptable (
	EncryptionIsSetup,
	encryptionSetup,
	noEncryptionUsed,
	encryptionAlreadySetup,
	encryptionConfigParsers,
	parseEncryptionConfig,
	remoteCipher,
	remoteCipher',
	embedCreds,
	cipherKey,
	extractCipher,
	isEncrypted,
	encryptionIsEnabled,
	describeEncryption,
	encryptionField,
	highRandomQualityField
) where

import qualified Data.Map as M
import qualified Data.Set as S
import qualified "sandi" Codec.Binary.Base64 as B64
import qualified Data.ByteString as B
import Control.Concurrent.STM

import Annex.Common
import Types.Remote
import Crypto
import Types.Crypto
import Types.ProposedAccepted
import qualified Annex
import Annex.SpecialRemote.Config

-- Used to ensure that encryption has been set up before trying to
-- eg, store creds in the remote config that would need to use the
-- encryption setup.
data EncryptionIsSetup = EncryptionIsSetup | NoEncryption

-- Remotes that don't use encryption can use this instead of
-- encryptionSetup.
noEncryptionUsed :: EncryptionIsSetup
noEncryptionUsed = NoEncryption

-- Using this avoids the type-safe check, so you'd better be sure
-- of what you're doing.
encryptionAlreadySetup :: EncryptionIsSetup
encryptionAlreadySetup = EncryptionIsSetup

encryptionConfigParsers :: [RemoteConfigFieldParser]
encryptionConfigParsers =
	[ encryptionFieldParser
	, optionalStringParser cipherField HiddenField
	, optionalStringParser cipherkeysField HiddenField
	, optionalStringParser pubkeysField HiddenField
	, yesNoParser embedCredsField Nothing
		(FieldDesc "embed credentials into git repository")
	, macFieldParser
	, optionalStringParser (Accepted "keyid")
		(FieldDesc "gpg key id")
	, optionalStringParser (Accepted "keyid+")
		(FieldDesc "add additional gpg key")
	, optionalStringParser (Accepted "keyid-")
		(FieldDesc "remove gpg key")
	, highRandomQualityFieldParser
	]

encryptionConfigs :: S.Set RemoteConfigField
encryptionConfigs = S.fromList (map parserForField encryptionConfigParsers)

-- Parse only encryption fields, ignoring all others.
parseEncryptionConfig :: RemoteConfig -> Either String ParsedRemoteConfig
parseEncryptionConfig c = parseRemoteConfig
	(M.restrictKeys c encryptionConfigs)
	(RemoteConfigParser encryptionConfigParsers Nothing)

encryptionFieldParser :: RemoteConfigFieldParser
encryptionFieldParser = RemoteConfigFieldParser
	{ parserForField = encryptionField
	, valueParser = \v c -> Just . RemoteConfigValue
		<$> parseEncryptionMethod (fmap fromProposedAccepted v) c
	, fieldDesc = FieldDesc "how to encrypt data stored in the special remote"
	, valueDesc = Just $ ValueDesc $
		intercalate " or " (M.keys encryptionMethods)
	}

encryptionMethods :: M.Map String EncryptionMethod
encryptionMethods = M.fromList
	[ ("none", NoneEncryption)
	, ("shared", SharedEncryption)
	, ("hybrid", HybridEncryption)
	, ("pubkey", PubKeyEncryption)
	, ("sharedpubkey", SharedPubKeyEncryption)
	]

parseEncryptionMethod :: Maybe String -> RemoteConfig -> Either String EncryptionMethod
parseEncryptionMethod (Just s) _ = case M.lookup s encryptionMethods of
	Just em -> Right em
	Nothing -> Left badEncryptionMethod
-- Hybrid encryption is the default when a keyid is specified without
-- an encryption field, or when there's a cipher already but no encryption
-- field.
parseEncryptionMethod Nothing c
	| M.member (Accepted "keyid") c || M.member cipherField c = Right HybridEncryption
	| otherwise = Left badEncryptionMethod

badEncryptionMethod :: String
badEncryptionMethod = "Specify " ++ intercalate " or "
	(map ((fromProposedAccepted encryptionField ++ "=") ++)
		(M.keys encryptionMethods))
	++ "."

highRandomQualityField :: RemoteConfigField
highRandomQualityField = Accepted "highRandomQuality"

highRandomQualityFieldParser :: RemoteConfigFieldParser
highRandomQualityFieldParser = RemoteConfigFieldParser
	{ parserForField = highRandomQualityField
	, valueParser = \v _c -> Just . RemoteConfigValue
		<$> parseHighRandomQuality (fmap fromProposedAccepted v)
	, fieldDesc = HiddenField
	, valueDesc = Nothing
	}
 
parseHighRandomQuality :: Maybe String -> Either String Bool
parseHighRandomQuality Nothing = Right True
parseHighRandomQuality (Just "false") = Right False
parseHighRandomQuality (Just "true") = Right True
parseHighRandomQuality _ = Left "expected highRandomQuality=true/false"
	
macFieldParser :: RemoteConfigFieldParser
macFieldParser = RemoteConfigFieldParser
	{ parserForField = macField
	, valueParser = \v _c -> Just . RemoteConfigValue <$> parseMac v
	, fieldDesc = FieldDesc "how to encrypt filenames used on the remote"
	, valueDesc = Just $ ValueDesc $
		intercalate " or " (M.keys macMap)
	}

parseMac :: Maybe (ProposedAccepted String) -> Either String Mac
parseMac Nothing = Right defaultMac
parseMac (Just (Accepted s)) = Right $ fromMaybe defaultMac (readMac s)
parseMac (Just (Proposed s)) = case readMac s of
	Just mac -> Right mac
	Nothing -> Left "bad mac value"

{- Encryption setup for a remote. The user must specify whether to use
 - an encryption key, or not encrypt. An encrypted cipher is created, or is
 - updated to be accessible to an additional encryption key. Or the user
 - could opt to use a shared cipher, which is stored unencrypted. -}
encryptionSetup :: RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, EncryptionIsSetup)
encryptionSetup c gc = do
	pc <- either giveup return $ parseEncryptionConfig c
	cmd <- gpgCmd <$> Annex.getGitConfig
	maybe (genCipher pc cmd) (updateCipher pc cmd) (extractCipher pc)
  where
	-- The type of encryption
	encryption = parseEncryptionMethod (fromProposedAccepted <$> M.lookup encryptionField c) c
	-- Generate a new cipher, depending on the chosen encryption scheme
	genCipher pc cmd = case encryption of
		Right NoneEncryption -> return (c, NoEncryption)
		Right SharedEncryption -> encsetup $ genSharedCipher cmd
		Right HybridEncryption -> encsetup $ genEncryptedCipher cmd (pc, gc) key Hybrid
		Right PubKeyEncryption -> encsetup $ genEncryptedCipher cmd (pc, gc) key PubKey
		Right SharedPubKeyEncryption -> encsetup $ genSharedPubKeyCipher cmd key
		Left err -> giveup err
	key = maybe (giveup "Specify keyid=...") fromProposedAccepted $
		M.lookup (Accepted "keyid") c
	newkeys = maybe [] (\k -> [(True,fromProposedAccepted k)]) (M.lookup (Accepted "keyid+") c) ++
		maybe [] (\k -> [(False,fromProposedAccepted k)]) (M.lookup (Accepted "keyid-") c)
	cannotchange = giveup "Cannot set encryption type of existing remotes."
	-- Update an existing cipher if possible.
	updateCipher pc cmd v = case v of
		SharedCipher _ | encryption == Right SharedEncryption ->
			return (c', EncryptionIsSetup)
		EncryptedCipher _ variant _ | sameasencryption variant ->
			use "encryption update" $ updateCipherKeyIds cmd (pc, gc) newkeys v
		SharedPubKeyCipher _ _ ->
			use "encryption update" $ updateCipherKeyIds cmd (pc, gc) newkeys v
		_ -> cannotchange
	sameasencryption variant = case encryption of
		Right HybridEncryption -> variant == Hybrid
		Right PubKeyEncryption -> variant == PubKey
		Right _ -> False
		Left _ -> True
	encsetup a = use "encryption setup" . a =<< highRandomQuality
	use m a = do
		showNote m
		cipher <- liftIO a
		showNote (describeCipher cipher)
		return (storeCipher cipher c', EncryptionIsSetup)
	highRandomQuality = ifM (Annex.getRead Annex.fast)
		( return False
		, case parseHighRandomQuality (fromProposedAccepted <$> M.lookup highRandomQualityField c) of
			Left err -> giveup err
			Right v -> return v
		)
	c' = foldr M.delete c
		-- Remove configs that are only used in here to generate
		-- the encryption keys, and should not be stored in
		-- remote.log.
		-- Older versions used to remove 'encryption' as well, since
		-- it was redundant; we now need to keep it for
		-- public-key encryption, hence we leave it on newer
		-- remotes (while being backward-compatible).
		(map Accepted ["keyid", "keyid+", "keyid-", "highRandomQuality"])

remoteCipher :: ParsedRemoteConfig -> RemoteGitConfig -> Annex (Maybe Cipher)
remoteCipher c gc = fmap fst <$> remoteCipher' c gc

{- Gets encryption Cipher. The decrypted Ciphers are cached in the Annex
 - state. -}
remoteCipher' :: ParsedRemoteConfig -> RemoteGitConfig -> Annex (Maybe (Cipher, StorableCipher))
remoteCipher' c gc = case extractCipher c of
	Nothing -> return Nothing
	Just encipher -> do
		cachev <- Annex.getRead Annex.ciphers
		cachedciper <- liftIO $ atomically $ 
			M.lookup encipher <$> readTMVar cachev
		case cachedciper of
			Just cipher -> return $ Just (cipher, encipher)
			-- Not cached; decrypt it, making sure
			-- to only decrypt one at a time. Avoids
			-- prompting for decrypting the same thing twice
			-- when this is run concurrently.
			Nothing -> bracketOnError
				(liftIO $ atomically $ takeTMVar cachev)
				(liftIO . atomically . putTMVar cachev)
				(go cachev encipher)
  where
	go cachev encipher cache = do
		cmd <- gpgCmd <$> Annex.getGitConfig
		cipher <- liftIO $ decryptCipher cmd (c, gc) encipher
		liftIO $ atomically $ putTMVar cachev $
			M.insert encipher cipher cache
		return $ Just (cipher, encipher)

{- Checks if the remote's config allows storing creds in the remote's config.
 - 
 - embedcreds=yes allows this, and embedcreds=no prevents it.
 -
 - If not set, the default is to only store creds when it's surely safe:
 - When gpg encryption is used and the creds are encrypted using it.
 - Not when a shared cipher is used.
 -}
embedCreds :: ParsedRemoteConfig -> Bool
embedCreds c = case getRemoteConfigValue embedCredsField c of
	Just v -> v
	Nothing -> case (getRemoteConfigValue cipherkeysField c, getRemoteConfigValue cipherField c) of
		(Just (_ :: String), Just (_ :: String)) -> True
		_ -> False

{- Gets encryption Cipher, and key encryptor. -}
cipherKey :: ParsedRemoteConfig -> RemoteGitConfig -> Annex (Maybe (Cipher, EncKey))
cipherKey c gc = fmap make <$> remoteCipher c gc
  where
	make ciphertext = (ciphertext, encryptKey mac ciphertext)
	mac = fromMaybe defaultMac $ getRemoteConfigValue macField c

{- Stores an StorableCipher in a remote's configuration. -}
storeCipher :: StorableCipher -> RemoteConfig -> RemoteConfig
storeCipher cip = case cip of
	(SharedCipher t) -> addcipher t
	(EncryptedCipher t _ ks) -> addcipher t . storekeys ks cipherkeysField
	(SharedPubKeyCipher t ks) -> addcipher t . storekeys ks pubkeysField
  where
	addcipher t = M.insert cipherField (Accepted (toB64bs t))
	storekeys (KeyIds l) n = M.insert n (Accepted (intercalate "," l))

{- Extracts an StorableCipher from a remote's configuration. -}
extractCipher :: ParsedRemoteConfig -> Maybe StorableCipher
extractCipher c = case (getRemoteConfigValue cipherField c,
			(getRemoteConfigValue cipherkeysField c <|> getRemoteConfigValue pubkeysField c),
			getRemoteConfigValue encryptionField c) of
	(Just t, Just ks, Just HybridEncryption) ->
		Just $ EncryptedCipher (fromB64bs t) Hybrid (readkeys ks)
	(Just t, Just ks, Just PubKeyEncryption) ->
		Just $ EncryptedCipher (fromB64bs t) PubKey (readkeys ks)
	(Just t, Just ks, Just SharedPubKeyEncryption) ->
		Just $ SharedPubKeyCipher (fromB64bs t) (readkeys ks)
	(Just t, Nothing, Just SharedEncryption) ->
		Just $ SharedCipher (fromB64bs t)
	_ -> Nothing
  where
	readkeys = KeyIds . splitc ','

isEncrypted :: ParsedRemoteConfig -> Bool
isEncrypted = isJust . extractCipher

-- Check if encryption is enabled. This can be done before encryption
-- is fully set up yet, so the cipher might not be present yet.
encryptionIsEnabled :: ParsedRemoteConfig -> Bool
encryptionIsEnabled c = case getRemoteConfigValue encryptionField c of
	Nothing -> False
	Just NoneEncryption -> False
	Just _ -> True

describeEncryption :: ParsedRemoteConfig -> String
describeEncryption c = case extractCipher c of
	Nothing -> "none"
	Just cip -> nameCipher cip ++ " (" ++ describeCipher cip ++ ")"

nameCipher :: StorableCipher -> String
nameCipher (SharedCipher _) = "shared"
nameCipher (EncryptedCipher _ PubKey _) = "pubkey"
nameCipher (EncryptedCipher _ Hybrid _) = "hybrid"
nameCipher (SharedPubKeyCipher _ _) = "sharedpubkey"

describeCipher :: StorableCipher -> String
describeCipher c = case c of
	(SharedCipher _) -> "encryption key stored in git repository"
	(EncryptedCipher _ _ ks) -> showkeys ks
	(SharedPubKeyCipher _ ks) -> showkeys ks
  where
	showkeys (KeyIds { keyIds = ks }) = "to gpg keys: " ++ unwords ks

{- Not using Utility.Base64 because these "Strings" are really
 - bags of bytes and that would convert to unicode and not round-trip
 - cleanly. -}
toB64bs :: String -> String
toB64bs = w82s . B.unpack . B64.encode . B.pack . s2w8

fromB64bs :: String -> String
fromB64bs s = either (const bad) (w82s . B.unpack) (B64.decode $ B.pack $ s2w8 s)
  where
	bad = error "bad base64 encoded data"