diff options
Diffstat (limited to 'Remote/GCrypt.hs')
-rw-r--r-- | Remote/GCrypt.hs | 402 |
1 files changed, 402 insertions, 0 deletions
diff --git a/Remote/GCrypt.hs b/Remote/GCrypt.hs new file mode 100644 index 000000000..e1b6811c7 --- /dev/null +++ b/Remote/GCrypt.hs @@ -0,0 +1,402 @@ +{- git remotes encrypted using git-remote-gcrypt + - + - Copyright 2013 Joey Hess <joey@kitenet.net> + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module Remote.GCrypt ( + remote, + gen, + getGCryptUUID, + coreGCryptId, + setupRepo +) where + +import qualified Data.Map as M +import qualified Data.ByteString.Lazy as L +import Control.Exception.Extensible + +import Common.Annex +import Types.Remote +import Types.GitConfig +import Types.Crypto +import qualified Git +import qualified Git.Command +import qualified Git.Config +import qualified Git.GCrypt +import qualified Git.Construct +import qualified Git.Types as Git () +import qualified Annex.Branch +import qualified Annex.Content +import Config +import Config.Cost +import Remote.Helper.Git +import Remote.Helper.Encryptable +import Remote.Helper.Special +import Remote.Helper.Messages +import qualified Remote.Helper.Ssh as Ssh +import Utility.Metered +import Crypto +import Annex.UUID +import Annex.Ssh +import qualified Remote.Rsync +import Utility.Rsync +import Utility.Tmp +import Logs.Remote +import Logs.Transfer +import Utility.Gpg +import Annex.Content + +remote :: RemoteType +remote = RemoteType { + typename = "gcrypt", + -- Remote.Git takes care of enumerating gcrypt remotes too, + -- and will call our gen on them. + enumerate = return [], + generate = gen, + setup = gCryptSetup +} + +gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex (Maybe Remote) +gen gcryptr u c gc = do + g <- gitRepo + -- get underlying git repo with real path, not gcrypt path + r <- liftIO $ Git.GCrypt.encryptedRemote g gcryptr + let r' = r { Git.remoteName = Git.remoteName gcryptr } + -- doublecheck that cache matches underlying repo's gcrypt-id + -- (which might not be set), only for local repos + (mgcryptid, r'') <- getGCryptId True r' + case (mgcryptid, Git.GCrypt.remoteRepoId g (Git.remoteName gcryptr)) of + (Just gcryptid, Just cachedgcryptid) + | gcryptid /= cachedgcryptid -> resetup gcryptid r'' + _ -> gen' r'' u c gc + where + -- A different drive may have been mounted, making a different + -- gcrypt remote available. So need to set the cached + -- gcrypt-id and annex-uuid of the remote to match the remote + -- that is now available. Also need to set the gcrypt particiants + -- correctly. + resetup gcryptid r = do + let u' = genUUIDInNameSpace gCryptNameSpace gcryptid + v <- M.lookup u' <$> readRemoteLog + case (Git.remoteName gcryptr, v) of + (Just remotename, Just c') -> do + setGcryptEncryption c' remotename + setConfig (remoteConfig gcryptr "uuid") (fromUUID u') + setConfig (ConfigKey $ Git.GCrypt.remoteConfigKey "gcrypt-id" remotename) gcryptid + gen' r u' c' gc + _ -> do + warning $ "not using unknown gcrypt repository pointed to by remote " ++ Git.repoDescribe r + return Nothing + +gen' :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex (Maybe Remote) +gen' r u c gc = do + cst <- remoteCost gc $ + if repoCheap r then nearlyCheapRemoteCost else expensiveRemoteCost + (rsynctransport, rsyncurl) <- rsyncTransportToObjects r + let rsyncopts = Remote.Rsync.genRsyncOpts c gc rsynctransport rsyncurl + let this = Remote + { uuid = u + , cost = cst + , name = Git.repoDescribe r + , storeKey = \_ _ _ -> noCrypto + , retrieveKeyFile = \_ _ _ _ -> noCrypto + , retrieveKeyFileCheap = \_ _ -> return False + , removeKey = remove this rsyncopts + , hasKey = checkPresent this rsyncopts + , hasKeyCheap = repoCheap r + , whereisKey = Nothing + , remoteFsck = Nothing + , repairRepo = Nothing + , config = c + , localpath = localpathCalc r + , repo = r + , gitconfig = gc { remoteGitConfig = Just $ extractGitConfig r } + , readonly = Git.repoIsHttp r + , globallyAvailable = globallyAvailableCalc r + , remotetype = remote + } + return $ Just $ encryptableRemote c + (store this rsyncopts) + (retrieve this rsyncopts) + this + +rsyncTransportToObjects :: Git.Repo -> Annex ([CommandParam], String) +rsyncTransportToObjects r = do + (rsynctransport, rsyncurl, _) <- rsyncTransport r + return (rsynctransport, rsyncurl ++ "/annex/objects") + +rsyncTransport :: Git.Repo -> Annex ([CommandParam], String, AccessMethod) +rsyncTransport r + | "ssh://" `isPrefixOf` loc = sshtransport $ break (== '/') $ drop (length "ssh://") loc + | "//:" `isInfixOf` loc = othertransport + | ":" `isInfixOf` loc = sshtransport $ separate (== ':') loc + | otherwise = othertransport + where + loc = Git.repoLocation r + sshtransport (host, path) = do + let rsyncpath = if "/~/" `isPrefixOf` path + then drop 3 path + else path + opts <- sshCachingOptions (host, Nothing) [] + return (rsyncShell $ Param "ssh" : opts, host ++ ":" ++ rsyncpath, AccessShell) + othertransport = return ([], loc, AccessDirect) + +noCrypto :: Annex a +noCrypto = error "cannot use gcrypt remote without encryption enabled" + +unsupportedUrl :: Annex a +unsupportedUrl = error "using non-ssh remote repo url with gcrypt is not supported" + +gCryptSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID) +gCryptSetup mu c = go $ M.lookup "gitrepo" c + where + remotename = fromJust (M.lookup "name" c) + go Nothing = error "Specify gitrepo=" + go (Just gitrepo) = do + c' <- encryptionSetup c + inRepo $ Git.Command.run + [ Params "remote add" + , Param remotename + , Param $ Git.GCrypt.urlPrefix ++ gitrepo + ] + + setGcryptEncryption c' remotename + + {- Run a git fetch and a push to the git repo in order to get + - its gcrypt-id set up, so that later git annex commands + - will use the remote as a gcrypt remote. The fetch is + - needed if the repo already exists; the push is needed + - if the repo has not yet been initialized by gcrypt. -} + void $ inRepo $ Git.Command.runBool + [ Param "fetch" + , Param remotename + ] + void $ inRepo $ Git.Command.runBool + [ Param "push" + , Param remotename + , Param $ show Annex.Branch.fullname + ] + g <- inRepo Git.Config.reRead + case Git.GCrypt.remoteRepoId g (Just remotename) of + Nothing -> error "unable to determine gcrypt-id of remote" + Just gcryptid -> do + let u = genUUIDInNameSpace gCryptNameSpace gcryptid + if Just u == mu || isNothing mu + then do + method <- setupRepo gcryptid =<< inRepo (Git.Construct.fromRemoteLocation gitrepo) + gitConfigSpecialRemote u c' "gcrypt" (fromAccessMethod method) + return (c', u) + else error $ "uuid mismatch " ++ show (u, mu, gcryptid) + +{- Sets up the gcrypt repository. The repository is either a local + - repo, or it is accessed via rsync directly, or it is accessed over ssh + - and git-annex-shell is available to manage it. + - + - The GCryptID is recorded in the repository's git config for later use. + - Also, if the git config has receive.denyNonFastForwards set, disable + - it; gcrypt relies on being able to fast-forward branches. + -} +setupRepo :: Git.GCrypt.GCryptId -> Git.Repo -> Annex AccessMethod +setupRepo gcryptid r + | Git.repoIsUrl r = do + (_, _, accessmethod) <- rsyncTransport r + case accessmethod of + AccessDirect -> rsyncsetup + AccessShell -> ifM gitannexshellsetup + ( return AccessShell + , rsyncsetup + ) + | Git.repoIsLocalUnknown r = localsetup =<< liftIO (Git.Config.read r) + | otherwise = localsetup r + where + localsetup r' = do + let setconfig k v = liftIO $ Git.Command.run [Param "config", Param k, Param v] r' + setconfig coreGCryptId gcryptid + setconfig denyNonFastForwards (Git.Config.boolConfig False) + return AccessDirect + + {- As well as modifying the remote's git config, + - create the objectDir on the remote, + - which is needed for direct rsync of objects to work. + -} + rsyncsetup = Remote.Rsync.withRsyncScratchDir $ \tmp -> do + liftIO $ createDirectoryIfMissing True $ tmp </> objectDir + (rsynctransport, rsyncurl, _) <- rsyncTransport r + let tmpconfig = tmp </> "config" + void $ liftIO $ rsync $ rsynctransport ++ + [ Param $ rsyncurl ++ "/config" + , Param tmpconfig + ] + liftIO $ do + void $ Git.Config.changeFile tmpconfig coreGCryptId gcryptid + void $ Git.Config.changeFile tmpconfig denyNonFastForwards (Git.Config.boolConfig False) + ok <- liftIO $ rsync $ rsynctransport ++ + [ Params "--recursive" + , Param $ tmp ++ "/" + , Param rsyncurl + ] + unless ok $ + error "Failed to connect to remote to set it up." + return AccessDirect + + {- Ask git-annex-shell to configure the repository as a gcrypt + - repository. May fail if it is too old. -} + gitannexshellsetup = Ssh.onRemote r (boolSystem, False) + "gcryptsetup" [ Param gcryptid ] [] + + denyNonFastForwards = "receive.denyNonFastForwards" + +shellOrRsync :: Remote -> Annex a -> Annex a -> Annex a +shellOrRsync r ashell arsync = case method of + AccessShell -> ashell + _ -> arsync + where + method = toAccessMethod $ fromMaybe "" $ + remoteAnnexGCrypt $ gitconfig r + +{- Configure gcrypt to use the same list of keyids that + - were passed to initremote as its participants. + - Also, configure it to use a signing key that is in the list of + - participants, which gcrypt requires is the case, and may not be + - depending on system configuration. + - + - (For shared encryption, gcrypt's default behavior is used.) -} +setGcryptEncryption :: RemoteConfig -> String -> Annex () +setGcryptEncryption c remotename = do + let participants = ConfigKey $ Git.GCrypt.remoteParticipantConfigKey remotename + case extractCipher c of + Nothing -> noCrypto + Just (EncryptedCipher _ _ (KeyIds { keyIds = ks})) -> do + setConfig participants (unwords ks) + let signingkey = ConfigKey $ Git.GCrypt.remoteSigningKey remotename + skeys <- M.keys <$> liftIO secretKeys + case filter (`elem` ks) skeys of + [] -> noop + (k:_) -> setConfig signingkey k + Just (SharedCipher _) -> + unsetConfig participants + +store :: Remote -> Remote.Rsync.RsyncOpts -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool +store r rsyncopts (cipher, enck) k p + | not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $ + metered (Just p) k $ \meterupdate -> spoolencrypted $ \h -> do + let dest = gCryptLocation r enck + createDirectoryIfMissing True $ parentDir dest + readBytes (meteredWriteFile meterupdate dest) h + return True + | Git.repoIsSsh (repo r) = shellOrRsync r storeshell storersync + | otherwise = unsupportedUrl + where + gpgopts = getGpgEncParams r + storersync = Remote.Rsync.storeEncrypted rsyncopts gpgopts (cipher, enck) k p + storeshell = withTmp enck $ \tmp -> + ifM (spoolencrypted $ readBytes $ \b -> catchBoolIO $ L.writeFile tmp b >> return True) + ( Ssh.rsyncHelper (Just p) + =<< Ssh.rsyncParamsRemote False r Upload enck tmp Nothing + , return False + ) + spoolencrypted a = Annex.Content.sendAnnex k noop $ \src -> + liftIO $ catchBoolIO $ + encrypt gpgopts cipher (feedFile src) a + +retrieve :: Remote -> Remote.Rsync.RsyncOpts -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool +retrieve r rsyncopts (cipher, enck) k d p + | not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $ do + retrievewith $ L.readFile src + return True + | Git.repoIsSsh (repo r) = shellOrRsync r retrieveshell retrieversync + | otherwise = unsupportedUrl + where + src = gCryptLocation r enck + retrievewith a = metered (Just p) k $ \meterupdate -> liftIO $ + a >>= \b -> + decrypt cipher (feedBytes b) + (readBytes $ meteredWriteFile meterupdate d) + retrieversync = Remote.Rsync.retrieveEncrypted rsyncopts (cipher, enck) k d p + retrieveshell = withTmp enck $ \tmp -> + ifM (Ssh.rsyncHelper (Just p) =<< Ssh.rsyncParamsRemote False r Download enck tmp Nothing) + ( liftIO $ catchBoolIO $ do + decrypt cipher (feedFile tmp) $ + readBytes $ L.writeFile d + return True + , return False + ) + +remove :: Remote -> Remote.Rsync.RsyncOpts -> Key -> Annex Bool +remove r rsyncopts k + | not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $ do + liftIO $ removeDirectoryRecursive $ parentDir $ gCryptLocation r k + return True + | Git.repoIsSsh (repo r) = shellOrRsync r removeshell removersync + | otherwise = unsupportedUrl + where + removersync = Remote.Rsync.remove rsyncopts k + removeshell = Ssh.dropKey (repo r) k + +checkPresent :: Remote -> Remote.Rsync.RsyncOpts -> Key -> Annex (Either String Bool) +checkPresent r rsyncopts k + | not $ Git.repoIsUrl (repo r) = + guardUsable (repo r) (cantCheck $ repo r) $ + liftIO $ catchDefaultIO (cantCheck $ repo r) $ + Right <$> doesFileExist (gCryptLocation r k) + | Git.repoIsSsh (repo r) = shellOrRsync r checkshell checkrsync + | otherwise = unsupportedUrl + where + checkrsync = Remote.Rsync.checkPresent (repo r) rsyncopts k + checkshell = Ssh.inAnnex (repo r) k + +{- Annexed objects are hashed using lower-case directories for max + - portability. -} +gCryptLocation :: Remote -> Key -> FilePath +gCryptLocation r key = Git.repoLocation (repo r) </> objectDir </> keyPath key hashDirLower + +data AccessMethod = AccessDirect | AccessShell + +fromAccessMethod :: AccessMethod -> String +fromAccessMethod AccessShell = "shell" +fromAccessMethod AccessDirect = "true" + +toAccessMethod :: String -> AccessMethod +toAccessMethod "shell" = AccessShell +toAccessMethod _ = AccessDirect + +getGCryptUUID :: Bool -> Git.Repo -> Annex (Maybe UUID) +getGCryptUUID fast r = (genUUIDInNameSpace gCryptNameSpace <$>) . fst + <$> getGCryptId fast r + +coreGCryptId :: String +coreGCryptId = "core.gcrypt-id" + +{- gcrypt repos set up by git-annex as special remotes have a + - core.gcrypt-id setting in their config, which can be mapped back to + - the remote's UUID. + - + - In fast mode, only checks local repos. To check a remote repo, + - tries git-annex-shell and direct rsync of the git config file. + - + - (Also returns a version of input repo with its config read.) -} +getGCryptId :: Bool -> Git.Repo -> Annex (Maybe Git.GCrypt.GCryptId, Git.Repo) +getGCryptId fast r + | Git.repoIsLocal r || Git.repoIsLocalUnknown r = extract <$> + liftIO (catchMaybeIO $ Git.Config.read r) + | not fast = extract . liftM fst <$> getM (eitherToMaybe <$>) + [ Ssh.onRemote r (Git.Config.fromPipe r, Left undefined) "configlist" [] [] + , getConfigViaRsync r + ] + | otherwise = return (Nothing, r) + where + extract Nothing = (Nothing, r) + extract (Just r') = (Git.Config.getMaybe coreGCryptId r', r') + +getConfigViaRsync :: Git.Repo -> Annex (Either SomeException (Git.Repo, String)) +getConfigViaRsync r = do + (rsynctransport, rsyncurl, _) <- rsyncTransport r + liftIO $ do + withTmpFile "tmpconfig" $ \tmpconfig _ -> do + void $ rsync $ rsynctransport ++ + [ Param $ rsyncurl ++ "/config" + , Param tmpconfig + ] + Git.Config.fromFile r tmpconfig |