From c111f44bcb4ccaaee1c6de044a1adc484f1f519b Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Fri, 19 May 2023 13:56:49 -0400
Subject: [PATCH] Convert types safely or with the ability to report errors

---
 src/Tahoe/SDMF/Internal/Encoding.hs | 56 ++++++++++++++++++++++------
 src/Tahoe/SDMF/Internal/Share.hs    | 58 ++++++++++++++++++++++-------
 2 files changed, 89 insertions(+), 25 deletions(-)

diff --git a/src/Tahoe/SDMF/Internal/Encoding.hs b/src/Tahoe/SDMF/Internal/Encoding.hs
index e9f66dc..db9ef5d 100644
--- a/src/Tahoe/SDMF/Internal/Encoding.hs
+++ b/src/Tahoe/SDMF/Internal/Encoding.hs
@@ -5,22 +5,25 @@
 -}
 module Tahoe.SDMF.Internal.Encoding where
 
+import Control.Monad (when)
 import Control.Monad.IO.Class (MonadIO (liftIO))
 import Crypto.Cipher.Types (BlockCipher (blockSize), IV, makeIV)
 import Crypto.Random (MonadRandom (getRandomBytes))
 import Data.Bifunctor (Bifunctor (bimap))
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as LB
+import Data.Int (Int64)
 import qualified Data.Text as T
 import Data.Word (Word16, Word64, Word8)
 import Tahoe.CHK (padCiphertext, zfec, zunfec)
 import Tahoe.CHK.Merkle (MerkleTree (MerkleLeaf))
 import Tahoe.SDMF.Internal.Capability (Reader (..), Writer (..), deriveReader)
+import Tahoe.SDMF.Internal.Converting (from, tryInto)
 import qualified Tahoe.SDMF.Internal.Keys as Keys
 import Tahoe.SDMF.Internal.Share (HashChain (HashChain), Share (..))
 
+-- | Randomly generate a new IV suitable for use with some BlockCipher.
 randomIV :: forall c m. (BlockCipher c, MonadRandom m) => m (Maybe (IV c))
--- XXX Secure enough random source?
 randomIV = (makeIV :: B.ByteString -> Maybe (IV c)) <$> getRandomBytes (blockSize (undefined :: c))
 
 {- | Given a pre-determined key pair and sequence number, encode some
@@ -32,19 +35,38 @@ randomIV = (makeIV :: B.ByteString -> Maybe (IV c)) <$> getRandomBytes (blockSiz
 -}
 encode :: (MonadFail m, MonadIO m, MonadRandom m) => Keys.KeyPair -> Word64 -> Word16 -> Word16 -> LB.ByteString -> m ([Share], Writer)
 encode keypair shareSequenceNumber required total ciphertext = do
-    blocks <- liftIO $ fmap LB.fromStrict <$> zfec (fromIntegral required) (fromIntegral total) paddedCiphertext
+    -- Make sure the encoding parameters fit into a Word8
+    requiredAsWord8 <- tryInto @Word8 ("must have 0 < required < 255 but required == " <> show required) required
+    totalAsWord8 <- tryInto @Word8 ("must have 0 < total < 256 but total == " <> show total) total
+
+    -- And that they make sense together.
+    when (required >= total) (fail $ "must have required < total but required == " <> show required <> ", total == " <> show total)
+
+    -- They look okay, we can proceed.
+    blocks <- liftIO $ fmap LB.fromStrict <$> zfec (from required) (from total) paddedCiphertext
 
     (Just iv) <- randomIV
 
-    -- XXX fromIntegral is going from Word16 to Word8, not safe
+    -- We know the length won't be negative (doesn't make sense) and we
+    -- know all positive values fit into a Word64 so we can do this
+    -- conversion safely.  But if it needs to fail for some reason, it
+    -- can do so safely.
+    dataLength <- tryInto @Word64 "must have 0 <= data length" (LB.length ciphertext)
+
+    -- All segments are the same so we can figure the size from any one
+    -- block.  This conversion might fail because of Int64 vs Word64 but
+    -- only for truly, truly tremendous share data.
+    shareSegmentSize <- tryInto @Word64 "must have segment size < 2^63" (LB.length (head blocks))
+
     let makeShare' =
             flip $
                 makeShare
                     shareSequenceNumber
                     (Keys.SDMF_IV iv)
-                    (fromIntegral required)
-                    (fromIntegral total)
-                    (fromIntegral $ LB.length ciphertext)
+                    requiredAsWord8
+                    totalAsWord8
+                    dataLength
+                    shareSegmentSize
                     (Keys.toVerificationKey keypair)
 
     let makeShare'' = makeShare' <$> blocks
@@ -64,14 +86,14 @@ makeShare ::
     Word8 ->
     Word8 ->
     Word64 ->
+    Word64 ->
     Keys.Verification ->
     B.ByteString ->
     LB.ByteString ->
     Share
-makeShare shareSequenceNumber shareIV shareRequiredShares shareTotalShares shareDataLength shareVerificationKey shareEncryptedPrivateKey shareData = Share{..}
+makeShare shareSequenceNumber shareIV shareRequiredShares shareTotalShares shareDataLength shareSegmentSize shareVerificationKey shareEncryptedPrivateKey shareData = Share{..}
   where
     shareRootHash = B.replicate 32 0
-    shareSegmentSize = fromIntegral $ LB.length shareData -- XXX Partial
     shareSignature = B.replicate 32 0 -- XXX Actually compute sig, and is it 32 bytes?
     shareHashChain = HashChain []
     shareBlockHashTree = MerkleLeaf (B.replicate 32 0) -- XXX Real hash here, plus length check
@@ -84,12 +106,22 @@ makeShare shareSequenceNumber shareIV shareRequiredShares shareTotalShares share
 decode :: (MonadFail m, MonadIO m) => Reader -> [(Word16, Share)] -> m LB.ByteString
 decode _ [] = fail "Cannot decode with no shares"
 decode _ s@((_, Share{shareRequiredShares, shareTotalShares, shareDataLength}) : shares)
-    | length s < fromIntegral shareRequiredShares = fail $ "got " <> show (length shares) <> " shares, required " <> show shareRequiredShares
+    -- Make sure we have enough shares.
+    | length s < requiredAsInt =
+        fail $ "got " <> show (length shares) <> " shares, required " <> show shareRequiredShares
     | otherwise = do
-        ciphertext <- liftIO $ zunfec (fromIntegral shareRequiredShares) (fromIntegral shareTotalShares) (take (fromIntegral shareRequiredShares) blocks)
-        pure . LB.take (fromIntegral shareDataLength) . LB.fromStrict $ ciphertext
+        -- Make sure this implementation can handle the amount of data involved.
+        -- Since we use lazy ByteString we're limited to 2^63-1 bytes rather than
+        -- 2^64-1 bytes so there are some SDMF shares we can't interpret right
+        -- now.
+        shareDataLength' <- tryInto @Int64 ("share data length " <> show shareDataLength <> " is beyond maximum supported by this implementation " <> show (maxBound :: Int64)) shareDataLength
+        ciphertext <- liftIO $ zunfec requiredAsInt totalAsInt (take requiredAsInt blocks)
+        pure . LB.take shareDataLength' . LB.fromStrict $ ciphertext
   where
-    blocks = bimap fromIntegral (LB.toStrict . shareData) <$> s
+    blocks = bimap (from @Word16) (LB.toStrict . shareData) <$> s
+
+    requiredAsInt = from shareRequiredShares
+    totalAsInt = from shareTotalShares
 
 -- | Compute an SDMF write capability for a given keypair.
 capabilityForKeyPair :: Keys.KeyPair -> Either T.Text Writer
diff --git a/src/Tahoe/SDMF/Internal/Share.hs b/src/Tahoe/SDMF/Internal/Share.hs
index d370bbc..e5d3f4d 100644
--- a/src/Tahoe/SDMF/Internal/Share.hs
+++ b/src/Tahoe/SDMF/Internal/Share.hs
@@ -13,9 +13,10 @@ import Data.Binary.Put (putByteString, putLazyByteString, putWord16be, putWord32
 import qualified Data.ByteArray as ByteArray
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as LB
-import Data.Word (Word16, Word64, Word8)
+import Data.Word (Word16, Word32, Word64, Word8)
 import Data.X509 (PrivKey (PrivKeyRSA), PubKey (PubKeyRSA))
 import Tahoe.CHK.Merkle (MerkleTree, leafHashes)
+import Tahoe.SDMF.Internal.Converting (From (from), TryFrom (tryFrom), into, tryInto)
 import qualified Tahoe.SDMF.Internal.Keys as Keys
 
 hashSize :: Int
@@ -113,13 +114,43 @@ instance Binary Share where
         verificationKeyBytes = Keys.verificationKeyToBytes shareVerificationKey
         blockHashTreeBytes = B.concat . leafHashes $ shareBlockHashTree
 
-        -- TODO Compute these from all the putting.
-        signatureOffset = fromIntegral $ 1 + 8 + hashSize + 16 + 18 + 32 + B.length verificationKeyBytes
-        hashChainOffset = signatureOffset + fromIntegral (B.length shareSignature)
-        blockHashTreeOffset = hashChainOffset + fromIntegral (length (hashChain shareHashChain) * (hashSize + 2))
-        shareDataOffset = blockHashTreeOffset + fromIntegral (B.length blockHashTreeBytes)
-        encryptedPrivateKeyOffset = fromIntegral shareDataOffset + fromIntegral (LB.length shareData)
-        eofOffset = encryptedPrivateKeyOffset + fromIntegral (B.length shareEncryptedPrivateKey)
+        -- Some conversions could fail because we can't be completely sure of
+        -- the size of the data we're working with.  Put has no good failure
+        -- mechanism though.  Try to provide the best failure behavior we can
+        -- here.
+        signatureOffset =
+            case tryInto @Word32 "" $ 1 + 8 + hashSize + 16 + 18 + 32 + B.length verificationKeyBytes of
+                Nothing -> error "Binary.put Share could not represent signature offset"
+                Just x -> x
+
+        hashChainOffset =
+            signatureOffset
+                + case tryInto @Word32 "" (B.length shareSignature) of
+                    Nothing -> error "Binary.put Share could not represent hash chain offset"
+                    Just x -> x
+        blockHashTreeOffset =
+            hashChainOffset
+                + case tryInto @Word32 "" (length (hashChain shareHashChain) * (hashSize + 2)) of
+                    Nothing -> error "Binary.put Share could not represent block hash tree offset"
+                    Just x -> x
+        shareDataOffset =
+            blockHashTreeOffset
+                + case tryInto @Word32 "" (B.length blockHashTreeBytes) of
+                    Nothing -> error "Binary.put Share could not represent share data offset"
+                    Just x -> x
+
+        -- Then there are a couple 64 bit offsets, represented as Word64s, for
+        -- positions that follow the share data.
+        encryptedPrivateKeyOffset =
+            into @Word64 shareDataOffset
+                + case tryInto @Word64 "" (LB.length shareData) of
+                    Nothing -> error "Binary.put Share could not represent share data length"
+                    Just x -> x
+        eofOffset =
+            encryptedPrivateKeyOffset
+                + case tryInto @Word64 "" (B.length shareEncryptedPrivateKey) of
+                    Nothing -> error "Binary.put Share could not represent encrypted private key length"
+                    Just x -> x
 
     get = do
         version <- getWord8
@@ -148,12 +179,13 @@ instance Binary Share where
         shareVerificationOffset <- bytesRead
 
         -- Read in the values between all those offsets.
-        shareVerificationKey <- Keys.Verification <$> isolate (fromIntegral signatureOffset - fromIntegral shareVerificationOffset) getSubjectPublicKeyInfo
-        shareSignature <- getByteString (fromIntegral hashChainOffset - fromIntegral signatureOffset)
-        shareHashChain <- isolate (fromIntegral blockHashTreeOffset - fromIntegral hashChainOffset) get
-        shareBlockHashTree <- isolate (fromIntegral shareDataOffset - fromIntegral blockHashTreeOffset) get
+        shareVerificationKey <- Keys.Verification <$> isolate (from signatureOffset - from shareVerificationOffset) getSubjectPublicKeyInfo
+        shareSignature <- getByteString (from $ hashChainOffset - signatureOffset)
+        shareHashChain <- isolate (from $ blockHashTreeOffset - hashChainOffset) get
+        shareBlockHashTree <- isolate (from $ shareDataOffset - blockHashTreeOffset) get
         shareData <- getLazyByteString (fromIntegral encryptedPrivateKeyOffset - fromIntegral shareDataOffset)
-        shareEncryptedPrivateKey <- getByteString (fromIntegral eofOffset - fromIntegral encryptedPrivateKeyOffset)
+        keyBytesLength <- tryInto @Int "Binary.get Share cannot represent private key length" (eofOffset - encryptedPrivateKeyOffset)
+        shareEncryptedPrivateKey <- getByteString keyBytesLength
 
         empty <- isEmpty
         unless empty (fail "Expected end of input but there are more bytes")
-- 
GitLab