Added new encryption=sharedpubkey mode for special remotes.

This is useful for makking a special remote that anyone with a clone of the
repo and your public keys can upload files to, but only you can decrypt the
files stored in it.
This commit is contained in:
Joey Hess 2016-05-10 16:50:31 -04:00
parent 2d00523609
commit e219289c83
Failed to extract signature
7 changed files with 114 additions and 63 deletions

View file

@ -18,8 +18,8 @@ module Crypto (
StorableCipher(..),
genEncryptedCipher,
genSharedCipher,
updateEncryptedCipher,
describeCipher,
genSharedPubKeyCipher,
updateCipherKeyIds,
decryptCipher,
encryptKey,
isEncKey,
@ -74,7 +74,7 @@ cipherMac (Cipher c) = take cipherBeginning c
cipherMac (MacOnlyCipher c) = c
{- Creates a new Cipher, encrypted to the specified key id. -}
genEncryptedCipher :: Gpg.GpgCmd -> String -> EncryptedCipherVariant -> Bool -> IO StorableCipher
genEncryptedCipher :: Gpg.GpgCmd -> Gpg.KeyId -> EncryptedCipherVariant -> Bool -> IO StorableCipher
genEncryptedCipher cmd keyid variant highQuality = do
ks <- Gpg.findPubKeys cmd keyid
random <- Gpg.genRandom cmd highQuality size
@ -89,35 +89,40 @@ genSharedCipher :: Gpg.GpgCmd -> Bool -> IO StorableCipher
genSharedCipher cmd highQuality =
SharedCipher <$> Gpg.genRandom cmd highQuality cipherSize
{- Updates an existing Cipher, re-encrypting it to add or remove keyids,
- depending on whether the first component is True or False. -}
updateEncryptedCipher :: Gpg.GpgCmd -> [(Bool, String)] -> StorableCipher -> IO StorableCipher
updateEncryptedCipher _ _ SharedCipher{} = error "Cannot update shared cipher"
updateEncryptedCipher _ [] encipher = return encipher
updateEncryptedCipher cmd newkeys encipher@(EncryptedCipher _ variant (KeyIds ks)) = do
dropKeys <- listKeyIds [ k | (False, k) <- newkeys ]
forM_ dropKeys $ \k -> unless (k `elem` ks) $
{- Creates a new, shared Cipher, and looks up the gpg public key that will
- be used for encrypting content. -}
genSharedPubKeyCipher :: Gpg.GpgCmd -> Gpg.KeyId -> Bool -> IO StorableCipher
genSharedPubKeyCipher cmd keyid highQuality = do
ks <- Gpg.findPubKeys cmd keyid
random <- Gpg.genRandom cmd highQuality cipherSize
return $ SharedPubKeyCipher random ks
{- Updates an existing Cipher, making changes to its keyids.
-
- When the Cipher is encrypted, re-encrypts it. -}
updateCipherKeyIds :: Gpg.GpgCmd -> [(Bool, Gpg.KeyId)] -> StorableCipher -> IO StorableCipher
updateCipherKeyIds _ _ SharedCipher{} = error "Cannot update shared cipher"
updateCipherKeyIds _ [] c = return c
updateCipherKeyIds cmd changes encipher@(EncryptedCipher _ variant ks) = do
ks' <- updateCipherKeyIds' cmd changes ks
cipher <- decryptCipher cmd encipher
encryptCipher cmd cipher variant ks'
updateCipherKeyIds cmd changes (SharedPubKeyCipher cipher ks) =
SharedPubKeyCipher cipher <$> updateCipherKeyIds' cmd changes ks
updateCipherKeyIds' :: Gpg.GpgCmd -> [(Bool, Gpg.KeyId)] -> KeyIds -> IO KeyIds
updateCipherKeyIds' cmd changes (KeyIds ks) = do
dropkeys <- listKeyIds [ k | (False, k) <- changes ]
forM_ dropkeys $ \k -> unless (k `elem` ks) $
error $ "Key " ++ k ++ " was not present; cannot remove."
addKeys <- listKeyIds [ k | (True, k) <- newkeys ]
let ks' = (addKeys ++ ks) \\ dropKeys
addkeys <- listKeyIds [ k | (True, k) <- changes ]
let ks' = (addkeys ++ ks) \\ dropkeys
when (null ks') $
error "Cannot remove the last key."
cipher <- decryptCipher cmd encipher
encryptCipher cmd cipher variant $ KeyIds ks'
return $ KeyIds ks'
where
listKeyIds = concat <$$> mapM (keyIds <$$> Gpg.findPubKeys cmd)
describeCipher :: StorableCipher -> String
describeCipher (SharedCipher _) = "shared cipher"
describeCipher (EncryptedCipher _ variant (KeyIds ks)) =
scheme ++ " with gpg " ++ keys ks ++ " " ++ unwords ks
where
scheme = case variant of
Hybrid -> "hybrid cipher"
PubKey -> "pubkey crypto"
keys [_] = "key"
keys _ = "keys"
{- Encrypts a Cipher to the specified KeyIds. -}
encryptCipher :: Gpg.GpgCmd -> Cipher -> EncryptedCipherVariant -> KeyIds -> IO StorableCipher
encryptCipher cmd c variant (KeyIds ks) = do
@ -134,6 +139,7 @@ encryptCipher cmd c variant (KeyIds ks) = do
{- Decrypting an EncryptedCipher is expensive; the Cipher should be cached. -}
decryptCipher :: Gpg.GpgCmd -> StorableCipher -> IO Cipher
decryptCipher _ (SharedCipher t) = return $ Cipher t
decryptCipher _ (SharedPubKeyCipher t _) = return $ MacOnlyCipher t
decryptCipher cmd (EncryptedCipher t variant _) =
mkCipher <$> Gpg.pipeStrict cmd [ Param "--decrypt" ] t
where
@ -223,6 +229,7 @@ instance LensGpgEncParams RemoteConfig where
- look up the recipient keys and add them to the option list. -}
getGpgEncParams c = case M.lookup "encryption" c of
Just "pubkey" -> Gpg.pkEncTo $ maybe [] (split ",") $ M.lookup "cipherkeys" c
Just "sharedpubkey" -> Gpg.pkEncTo $ maybe [] (split ",") $ M.lookup "pubkeys" c
_ -> []
getGpgDecParams _ = []

View file

@ -297,9 +297,9 @@ shellOrRsync r ashell arsync
setGcryptEncryption :: RemoteConfig -> String -> Annex ()
setGcryptEncryption c remotename = do
let participants = remoteconfig Git.GCrypt.remoteParticipantConfigKey
case extractCipher c of
case cipherKeyIds =<< extractCipher c of
Nothing -> noCrypto
Just (EncryptedCipher _ _ (KeyIds { keyIds = ks})) -> do
Just (KeyIds { keyIds = ks}) -> do
setConfig participants (unwords ks)
let signingkey = ConfigKey $ Git.GCrypt.remoteSigningKey remotename
cmd <- gpgCmd <$> Annex.getGitConfig
@ -307,8 +307,6 @@ setGcryptEncryption c remotename = do
case filter (`elem` ks) skeys of
[] -> noop
(k:_) -> setConfig signingkey k
Just (SharedCipher _) ->
unsetConfig participants
setConfig (remoteconfig Git.GCrypt.remotePublishParticipantConfigKey)
(Git.Config.boolConfig True)
where

View file

@ -14,7 +14,6 @@ module Remote.Helper.Encryptable (
remoteCipher',
embedCreds,
cipherKey,
storeCipher,
extractCipher,
describeEncryption,
) where
@ -58,20 +57,18 @@ encryptionSetup c = do
encryption = M.lookup "encryption" c
-- Generate a new cipher, depending on the chosen encryption scheme
genCipher cmd = case encryption of
_ | M.member "cipher" c || M.member "cipherkeys" c -> cannotchange
_ | M.member "cipher" c || M.member "cipherkeys" c || M.member "pubkeys" c -> cannotchange
Just "none" -> return (c, NoEncryption)
Just "shared" -> use "encryption setup" . genSharedCipher cmd
=<< highRandomQuality
Just "shared" -> encsetup $ genSharedCipher cmd
-- hybrid encryption is the default when a keyid is
-- specified but no encryption
_ | maybe (M.member "keyid" c) (== "hybrid") encryption ->
use "encryption setup" . genEncryptedCipher cmd key Hybrid
=<< highRandomQuality
Just "pubkey" -> use "encryption setup" . genEncryptedCipher cmd key PubKey
=<< highRandomQuality
encsetup $ genEncryptedCipher cmd key Hybrid
Just "pubkey" -> encsetup $ genEncryptedCipher cmd key PubKey
Just "sharedpubkey" -> encsetup $ genSharedPubKeyCipher cmd key
_ -> error $ "Specify " ++ intercalate " or "
(map ("encryption=" ++)
["none","shared","hybrid","pubkey"])
["none","shared","hybrid","pubkey", "sharedpubkey"])
++ "."
key = fromMaybe (error "Specifiy keyid=...") $ M.lookup "keyid" c
newkeys = maybe [] (\k -> [(True,k)]) (M.lookup "keyid+" c) ++
@ -82,13 +79,16 @@ encryptionSetup c = do
SharedCipher _ | maybe True (== "shared") encryption -> return (c', EncryptionIsSetup)
EncryptedCipher _ variant _
| maybe True (== if variant == Hybrid then "hybrid" else "pubkey") encryption ->
use "encryption update" $ updateEncryptedCipher cmd newkeys v
use "encryption update" $ updateCipherKeyIds cmd newkeys v
SharedPubKeyCipher _ _ ->
use "encryption update" $ updateCipherKeyIds cmd newkeys v
_ -> cannotchange
encsetup a = use "encryption setup" . a =<< highRandomQuality
use m a = do
showNote m
cipher <- liftIO a
showNote $ describeCipher cipher
return (storeCipher c' cipher, EncryptionIsSetup)
mapM_ showNote (describeCipher cipher)
return (storeCipher cipher c', EncryptionIsSetup)
highRandomQuality =
(&&) (maybe True ( /= "false") $ M.lookup "highRandomQuality" c)
<$> fmap not (Annex.getState Annex.fast)
@ -123,8 +123,8 @@ remoteCipher' c = go $ extractCipher c
- 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, in which case the creds will be encrypted
- using it. Not when a shared cipher is used.
- When gpg encryption is used and the creds are encrypted using it.
- Not when a shared cipher is used.
-}
embedCreds :: RemoteConfig -> Bool
embedCreds c
@ -141,22 +141,26 @@ cipherKey c = fmap make <$> remoteCipher c
mac = fromMaybe defaultMac $ M.lookup "mac" c >>= readMac
{- Stores an StorableCipher in a remote's configuration. -}
storeCipher :: RemoteConfig -> StorableCipher -> RemoteConfig
storeCipher c (SharedCipher t) = M.insert "cipher" (toB64bs t) c
storeCipher c (EncryptedCipher t _ ks) =
M.insert "cipher" (toB64bs t) $ M.insert "cipherkeys" (showkeys ks) c
storeCipher :: StorableCipher -> RemoteConfig -> RemoteConfig
storeCipher cip = case cip of
(SharedCipher t) -> addcipher t
(EncryptedCipher t _ ks) -> addcipher t . storekeys ks "cipherkeys"
(SharedPubKeyCipher t ks) -> addcipher t . storekeys ks "pubkeys"
where
showkeys (KeyIds l) = intercalate "," l
addcipher t = M.insert "cipher" (toB64bs t)
storekeys (KeyIds l) n = M.insert n (intercalate "," l)
{- Extracts an StorableCipher from a remote's configuration. -}
extractCipher :: RemoteConfig -> Maybe StorableCipher
extractCipher c = case (M.lookup "cipher" c,
M.lookup "cipherkeys" c,
M.lookup "cipherkeys" c <|> M.lookup "pubkeys" c,
M.lookup "encryption" c) of
(Just t, Just ks, encryption) | maybe True (== "hybrid") encryption ->
Just $ EncryptedCipher (fromB64bs t) Hybrid (readkeys ks)
(Just t, Just ks, Just "pubkey") ->
Just $ EncryptedCipher (fromB64bs t) PubKey (readkeys ks)
(Just t, Just ks, Just "sharedpubkey") ->
Just $ SharedPubKeyCipher (fromB64bs t) (readkeys ks)
(Just t, Nothing, encryption) | maybe True (== "shared") encryption ->
Just $ SharedCipher (fromB64bs t)
_ -> Nothing
@ -166,14 +170,25 @@ extractCipher c = case (M.lookup "cipher" c,
describeEncryption :: RemoteConfig -> String
describeEncryption c = case extractCipher c of
Nothing -> "not encrypted"
(Just (SharedCipher _)) -> "encrypted (encryption key stored in git repository)"
(Just (EncryptedCipher _ v (KeyIds { keyIds = ks }))) -> unwords $ catMaybes
[ Just "encrypted (to gpg keys:"
, Just (unwords ks ++ ")")
Just cip -> "encrypted " ++ unwords (map paren (describeCipher cip))
where
paren s = "(" ++ s ++ ")"
describeCipher :: StorableCipher -> [String]
describeCipher c = case c of
(SharedCipher _) -> ["encryption key stored in git repository"]
(EncryptedCipher _ v ks) -> catMaybes
[ Just $ showkeys ks
, case v of
PubKey -> Nothing
Hybrid -> Just "(hybrid mode)"
Hybrid -> Just "hybrid mode"
]
(SharedPubKeyCipher _ ks) ->
[ showkeys ks
, "shared cipher"
]
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

View file

@ -10,6 +10,7 @@ module Types.Crypto (
StorableCipher(..),
EncryptedCipherVariant(..),
KeyIds(..),
cipherKeyIds,
Mac(..),
readMac,
showMac,
@ -23,12 +24,19 @@ import Utility.Gpg (KeyIds(..))
-- XXX ideally, this would be a locked memory region
data Cipher = Cipher String | MacOnlyCipher String
data StorableCipher = EncryptedCipher String EncryptedCipherVariant KeyIds
| SharedCipher String
data StorableCipher
= EncryptedCipher String EncryptedCipherVariant KeyIds
| SharedCipher String
| SharedPubKeyCipher String KeyIds
deriving (Ord, Eq)
data EncryptedCipherVariant = Hybrid | PubKey
deriving (Ord, Eq)
cipherKeyIds :: StorableCipher -> Maybe KeyIds
cipherKeyIds (EncryptedCipher _ _ ks) = Just ks
cipherKeyIds (SharedPubKeyCipher _ ks) = Just ks
cipherKeyIds (SharedCipher _) = Nothing
defaultMac :: Mac
defaultMac = HmacSha1

4
debian/changelog vendored
View file

@ -28,6 +28,10 @@ git-annex (6.20160420) UNRELEASED; urgency=medium
* In the unusual configuration where annex.crippledfilesystem=true but
core.symlinks=true, store object contents in mixed case hash
directories so that symlinks will point to them.
* Added new encryption=sharedpubkey mode for special remotes.
This is useful for makking a special remote that anyone with a clone
of the repo and your public keys can upload files to, but only you can
decrypt the files stored in it.
-- Joey Hess <id@joeyh.name> Thu, 28 Apr 2016 13:17:04 -0400

View file

@ -23,8 +23,9 @@ to disable encryption. To use encryption, you run
* `git annex initremote newremote type=... encryption=hybrid keyid=KEYID ...`
* `git annex initremote newremote type=... encryption=shared`
* `git annex initremote newremote type=... encryption=pubkey keyid=KEYID ...`
* `git annex initremote newremote type=... encryption=sharedpubkey keyid=KEYID ...``
## hybrid encryption keys
## hybrid encryption keys (encryption=hybrid)
The [[hybrid_key_design|design/encryption]] allows additional
encryption keys to be added on to a special remote later. Due to this
@ -53,7 +54,7 @@ probably to replace a revoked key:
See also [[encryption_design|design/encryption]] for other security
risks associated with encryption.
## shared encryption key
## shared encryption key (encryption=shared)
Alternatively, you can configure git-annex to use a shared cipher to
encrypt data stored in a remote. This shared cipher is stored,
@ -66,7 +67,7 @@ The advantage is you don't need to set up gpg keys. The disadvantage is
that this is **insecure** unless you trust every clone of the git
repository with access to the encrypted data stored in the special remote.
## regular public key encryption
## regular public key encryption (encryption=pubkey)
This alternative simply encrypts the files in the special remotes to one or
more public keys. It might be considered more secure due to its simplicity
@ -88,9 +89,25 @@ key has to be kept around to be able to decrypt those files.
that the key has been compromised, it is **insecure** to leave files
encrypted using that old key, and the user should re-encrypt everything.)
(Because filenames are MAC'ed, a cipher still needs to be
generated (and encrypted to the given key IDs). It is only used for MHAC
encryption of filenames.)
(A cipher still needs to be generated (and is encrypted to the given key IDs).
It is only used for HMAC encryption of filenames.)
## regular public key encryption with shared filename encryption (encryption=sharedpubkey)
This is a variation on encryption=pubkey which lets anyone who
has access to the gpg public keys store files in the special remote.
But, only owners of the corresponding private keys can retrieve the files
from the special remote.
git annex initremote newremote type=... [encryption=hybrid] keyid=KEYID ...
This might be useful if you want to let others drop off files for you in a
special remote, so that only you can access them.
The filenames used on the special remote are encrypted using HMAC,
which prevents the special remote from seeing the filenames. But, anyone
who can clone the git repository can access the HMAC cipher; it's stored
**unencrypted** in the git repository.
## MAC algorithm

View file

@ -12,3 +12,5 @@ remotes (S3). In that case, I don't care much about hiding file names, but
would appreciate the increased security of not having the secret key on the
backup server. It would only be needed if I wanted to verify or restore
backups.
> Added "encryption=sharedpubkey" [[done]] --[[Joey]]