tahoe-chk-0.1.0.2: src/Tahoe/CHK/Upload.hs
{-# LANGUAGE ScopedTypeVariables #-}
module Tahoe.CHK.Upload (
UploadResult (uploadResultReadCap, uploadResultExistingShares, uploadResultShareMap),
Uploadable (..),
Parameters (Parameters),
defaultParameters,
filesystemUploadable,
filesystemUploadableWithConvergence,
filesystemUploadableRandomConvergence,
memoryUploadableWithConvergence,
getConvergentKey,
upload,
store,
prettyFormatSharemap,
adjustSegmentSize,
encryptAndEncode,
) where
import Control.Monad.Conc.Class (
modifyIORefCAS,
)
import Data.Maybe (
fromJust,
)
import Data.List (
intersperse,
)
import Data.IORef (
newIORef,
)
import qualified Data.Binary as Binary
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.Text (
Text,
intercalate,
pack,
)
import qualified Data.Text as Text
import qualified Data.Set as Set
import qualified Data.Map.Strict as Map
import Crypto.Classes (
buildKey,
buildKeyIO,
)
import qualified Tahoe.CHK.Capability as Cap
import System.IO (
IOMode (ReadMode),
hFileSize,
hSetBinaryMode,
openBinaryFile,
openFile,
)
import Crypto.Cipher.AES128 (
AESKey128,
)
import Tahoe.CHK.Crypto (
convergenceEncryptionHashLazy,
storageIndexHash,
)
import Tahoe.CHK.Server (
ShareMap,
StorageServer (..),
)
import Tahoe.CHK.Types (
Parameters (Parameters),
ShareNum,
Size,
StorageIndex,
)
import Tahoe.Util (nextMultipleOf)
import Data.Tuple.Extra (thd3)
import Tahoe.CHK (
encode,
)
import Tahoe.CHK.Encrypt (encrypt)
-- Some data that can be uploaded.
data Uploadable = Uploadable
{ uploadableKey :: AESKey128
, uploadableSize :: Size
, uploadableParameters :: Parameters
, uploadableReadCleartext :: Integer -> IO B.ByteString
}
-- The outcome of an attempt to upload an immutable.
data UploadResult = UploadResult
{ uploadResultReadCap :: Cap.Reader
, uploadResultExistingShares :: Integer
, uploadResultShareMap :: ShareMap
}
deriving (Show)
-- Find shares that already exist on servers.
locateAllShareholders :: StorageIndex -> [StorageServer] -> IO ShareMap
locateAllShareholders storageIndex servers =
Map.unionsWith Set.union <$> mapM getBuckets servers
where
getBuckets :: StorageServer -> IO ShareMap
getBuckets s = do
buckets <- storageServerGetBuckets s storageIndex
return $ Map.fromSet (const $ Set.singleton s) buckets
planSharePlacement :: Parameters -> StorageIndex -> ShareMap -> [StorageServer] -> ShareMap
planSharePlacement (Parameters _ total _ _) _storageIndex _currentShares servers =
Map.fromList
[ (shareNum, Set.singleton server)
| (shareNum, server) <- zip [0 .. (fromIntegral total - 1)] (cycle servers)
]
-- Upload some immutable share data to some buckets on some servers.
--
-- XXX TODO This writes the raw share data to a file with no server-side
-- bookkeeping. This may not be intrinsically bad but it makes the share
-- files incompatible with the Tahoe-LAFS storage server on-disk state. This
-- may not be intrinsically bad either but interop testing would be much
-- easier if the on-disk state were compatible.
uploadImmutableShares ::
StorageIndex ->
[(ShareNum, StorageServer, BL.ByteString)] ->
IO ()
uploadImmutableShares storageIndex uploads =
uploadChunks 0
where
-- How much data to upload to each server per request.
chunkSize = 1024 * 1024
-- Upload the chunk of each share at `offset` to the corresponding server
-- and then proceed to the chunks at the next offset.
uploadChunks offset = do
res <- uploadChunk chunkSize storageIndex offset
if res
then uploadChunks (offset + chunkSize)
else pure ()
uploadChunk ::
Integer ->
StorageIndex ->
Integer ->
IO Bool
uploadChunk size storageIndex' offset =
if any ("" /=) chunks
then mapM_ (uploadOneChunk offset storageIndex') uploads >> pure True
else pure False
where
chunks = map (BL.take (fromIntegral size) . BL.drop (fromIntegral offset) . thd3) uploads
uploadOneChunk offset storageIndex' (shareNum, server, shareData) =
storageServerWrite server storageIndex' shareNum offset (BL.toStrict shareData)
{- | Encrypt and encode some application data to some ZFEC shares and upload
them to some servers.
-}
store ::
-- | The servers to consider using.
[StorageServer] ->
-- | The application data to operate on.
Uploadable ->
-- | The result of the attempt.
IO UploadResult
store servers uploadable@(Uploadable key _ params _) =
encryptAndEncode uploadable >>= upload servers key params
{- | Given some cleartext and some encoding parameters: encrypt and encode some
shares that can later be used to reconstruct the cleartext.
-}
encryptAndEncode ::
-- | The application data to encrypt and encode.
Uploadable ->
-- | An action to get an action that can be repeatedly evaluated to get
-- share data. As long as there is more share data, it evaluates to Left.
-- When shares are done, it evaluates to Right.
IO ([BL.ByteString], Cap.Reader)
encryptAndEncode (Uploadable readKey _ params read') = do
plaintext <- readAll read'
let ciphertext = encrypt readKey plaintext
(shares, cap) <- encode readKey params ciphertext
pure (map Binary.encode shares, cap)
where
readAll :: (Integer -> IO B.ByteString) -> IO BL.ByteString
readAll f = do
bs <- BL.fromStrict <$> f (1024 * 32)
if bs == ""
then pure ""
else (bs <>) <$> readAll f
{- | Given some cleartext, some encoding parameters, and some servers:
encrypt, encode, and upload some shares that can later be used to
reconstruct the cleartext.
This replaces allmydata.immutable.upload.Uploader.upload.
-}
upload ::
-- | The servers to consider uploading shares to.
[StorageServer] ->
-- | The encryption key (to derive the storage index).
AESKey128 ->
-- | The encoding parameters (XXX only for happy, right?)
Parameters ->
-- | The share data to upload.
([BL.ByteString], Cap.Reader) ->
-- | Describe the outcome of the upload.
IO UploadResult
upload servers key params encoded = do
-- Decide where to put it
existingShares <- locateAllShareholders storageIndex servers
let targets = targetServers params existingShares
-- Go
let (streams, cap) = encoded
uploadImmutableShares storageIndex (uploads targets streams)
return $
UploadResult
{ uploadResultReadCap = cap
, uploadResultExistingShares = fromIntegral $ length existingShares
, uploadResultShareMap = targets
}
where
storageIndex :: StorageIndex
storageIndex = storageIndexHash key
targetServers :: Parameters -> ShareMap -> ShareMap
targetServers parameters existingShares = planSharePlacement parameters storageIndex existingShares servers
-- Adapt a stream of share data to a stream of share data annotated with
-- server placement decision.
uploads ::
ShareMap ->
[BL.ByteString] ->
[(ShareNum, StorageServer, BL.ByteString)]
uploads goal shareDatav = Map.foldrWithKey (makeUpload shareDatav) [] goal
where
makeUpload ::
[BL.ByteString] ->
ShareNum ->
Set.Set StorageServer ->
[(ShareNum, StorageServer, BL.ByteString)] ->
[(ShareNum, StorageServer, BL.ByteString)]
makeUpload shareDatav' num servers' soFar =
[ ( num
, server
, shareDatav' !! fromIntegral num
)
| server <- Set.elems servers'
]
++ soFar
defaultParameters :: Parameters
defaultParameters = Parameters (128 * 1024) 10 7 3
-- The adjustment implemented in
-- allmydata.immutable.upload.BaseUploadable.get_all_encoding_parameters
adjustSegmentSize :: Parameters -> Size -> Parameters
adjustSegmentSize (Parameters segmentSize total happy required) dataSize =
Parameters (effectiveSegmentSize dataSize) total happy required
where
effectiveSegmentSize =
-- For small files, shrink the segment size to avoid wasting space.
-- Also make the shrunk value a multiple of required shares or the
-- encoding doesn't work.
nextMultipleOf required . min segmentSize
-- Create an uploadable with the given key.
filesystemUploadable :: AESKey128 -> FilePath -> Parameters -> IO Uploadable
filesystemUploadable key path params = do
fhandle <- openBinaryFile path ReadMode
fsize <- hFileSize fhandle
return $
Uploadable
{ uploadableKey = key
, uploadableSize = fsize
, uploadableParameters = adjustSegmentSize params fsize
, -- TODO Consider replacing this with a lazy bytestring or a list of bytestrings
uploadableReadCleartext = B.hGet fhandle . fromIntegral
}
filesystemUploadableWithConvergence :: B.ByteString -> FilePath -> Parameters -> IO Uploadable
filesystemUploadableWithConvergence secret uploadablePath params = do
-- Allow getConvergentKey to use lazy ByteString to read and hash the
-- uploadable by letting the handle remain open past the end of this
-- function. lazy ByteString will close the handle.
uploadableHandle <- openFile uploadablePath ReadMode
hSetBinaryMode uploadableHandle True
-- Annoyingly, adjust the segment size here so that the convergence secret
-- is computed based on the adjusted value. This is what Tahoe-LAFS does so
-- it is necessary to arrive at the same converged key. Whether this part
-- of the construction is actually important aside from interop, I don't
-- know.
size <- hFileSize uploadableHandle
content <- BL.hGetContents uploadableHandle
memoryUploadableWithConvergence secret size content params
-- TODO Consider lazy bytestring here instead
memoryUploadableWithConvergence :: B.ByteString -> Integer -> BL.ByteString -> Parameters -> IO Uploadable
memoryUploadableWithConvergence secret size content params =
let key = getConvergentKey secret (adjustSegmentSize params size) content
in memoryUploadable key size content params
memoryUploadable :: AESKey128 -> Integer -> BL.ByteString -> Parameters -> IO Uploadable
memoryUploadable key size content params =
let makeReader :: BL.ByteString -> IO (Integer -> IO BL.ByteString)
makeReader allContent =
let cas len content' = (BL.drop len content', BL.take len content')
in do
contentRef <- newIORef allContent
return $ (modifyIORefCAS contentRef . cas) . fromIntegral
in do
reader <- makeReader content
return $
Uploadable
{ uploadableKey = key
, uploadableSize = size
, uploadableParameters = adjustSegmentSize params size
, -- TODO Consider replacing this with a lazy bytestring or a list of bytestrings
uploadableReadCleartext = (BL.toStrict <$>) . reader
}
-- allmydata.immutable.upload.FileHandle._get_encryption_key_convergent
getConvergentKey :: B.ByteString -> Parameters -> BL.ByteString -> AESKey128
getConvergentKey secret params content =
fromJust . buildKey $ convergenceEncryptionHashLazy secret params content
-- Create an uploadable with a random key.
filesystemUploadableRandomConvergence :: FilePath -> Parameters -> IO Uploadable
filesystemUploadableRandomConvergence path params = do
key <- buildKeyIO :: IO AESKey128
filesystemUploadable key path params
prettyFormatSharemap :: ShareMap -> Text
prettyFormatSharemap sharemap =
intercalate
"\n"
[ Text.concat ["\t", showElem elem']
| elem' <- Map.toList sharemap
]
where
showElem :: (ShareNum, Set.Set StorageServer) -> Text
showElem (shareNum, servers) =
Text.concat $
[ pack $ show shareNum
, ":"
]
++ intersperse ", " (map storageServerID (Set.toList servers))