diff --git a/README.md b/README.md index 173dc50acc465539b4bcfc940c923d30227c690b..c6c90a641e67bb4d1f31bb563ae01d7eaa4c9851 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,11 @@ However, its APIs are intended to be easy to integrate with such an implementati ### What is the current state? -* All implementation tasks are pending. +* SDMF write, read, and verify capabilities can be parsed and serialized. +* SDMF shares can be deserialized, decoded, and decrypted. +* Plaintext can be encrypted, encoded into shares, and the shares serialized to bytes. + * Not all fields of the shares contain correctly initialized values. + * Enough fields are correctly populated to recover the original plaintext. ## Why does it exist? diff --git a/encode-ssk/Main.hs b/encode-ssk/Main.hs new file mode 100644 index 0000000000000000000000000000000000000000..cc50fb611105491eef02c21ce3c8d22e89af94e7 --- /dev/null +++ b/encode-ssk/Main.hs @@ -0,0 +1,29 @@ +module Main where + +import qualified Crypto.PubKey.RSA as RSA +import Data.Binary (encode) +import Data.ByteString.Base32 (encodeBase32Unpadded) +import qualified Data.ByteString.Lazy as LB +import qualified Data.Text as T +import qualified Data.Text.IO as T +import System.IO (stdin) +import qualified Tahoe.SDMF as SDMF +import qualified Tahoe.SDMF.Keys as SDMF.Keys + +main :: IO () +main = do + plaintext <- LB.hGetContents stdin + keypair <- SDMF.Keys.KeyPair . snd <$> RSA.generate (2048 `div` 8) e + Just iv <- SDMF.randomIV + + let ciphertext = SDMF.encrypt keypair iv plaintext + (shares, writeCap) <- SDMF.encode keypair iv 1 3 5 ciphertext + let shareBytes = encode <$> shares + + let si = SDMF.Keys.unStorageIndex . SDMF.verifierStorageIndex . SDMF.readerVerifier . SDMF.writerReader $ writeCap + + mapM_ (uncurry (writeShare si)) (zip [0 :: Int ..] shareBytes) + T.putStrLn (SDMF.dangerRealShow (SDMF.SDMFWriter writeCap)) + where + e = 0x10001 + writeShare si shnum = LB.writeFile $ (T.unpack . T.toLower . encodeBase32Unpadded $ si) <> "." <> show shnum diff --git a/flake.nix b/flake.nix index f5feba570df0b1b4aec8cd7a77f11c664b788aca..cc2531248b496c8e322a472788860b38f0d8d648 100644 --- a/flake.nix +++ b/flake.nix @@ -58,7 +58,7 @@ text = '' cabal update hackage.haskell.org - cabal build + cabal build all cabal run tests ''; } diff --git a/src/Tahoe/SDMF.hs b/src/Tahoe/SDMF.hs index 3f2cce62d7c113a0a8ee6d806c1fde9cdfca97fb..3367f1189837e66a839c12000d07b5d79be7a18b 100644 --- a/src/Tahoe/SDMF.hs +++ b/src/Tahoe/SDMF.hs @@ -24,6 +24,7 @@ import Tahoe.SDMF.Internal.Encoding ( import Tahoe.SDMF.Internal.Encrypting ( decrypt, encrypt, + randomIV, ) import Tahoe.SDMF.Internal.Share ( Share (..), diff --git a/src/Tahoe/SDMF/Internal/Capability.hs b/src/Tahoe/SDMF/Internal/Capability.hs index bc2f24312917598ba54b4a99e11ee04d4286319c..ba5acbf0698e6e9247aa60d0c52d992fcb6104c2 100644 --- a/src/Tahoe/SDMF/Internal/Capability.hs +++ b/src/Tahoe/SDMF/Internal/Capability.hs @@ -23,7 +23,6 @@ import Tahoe.SDMF.Internal.Keys ( deriveReadKey, deriveStorageIndex, readKeyBytes, - shorten, showBase32, ) import Text.Megaparsec ( diff --git a/src/Tahoe/SDMF/Internal/Encoding.hs b/src/Tahoe/SDMF/Internal/Encoding.hs index f95b90d4d43406f36be94e6c10ac97a431bb7365..b34b91c56e0028f61b4bc5d2200993ccde4eb5c9 100644 --- a/src/Tahoe/SDMF/Internal/Encoding.hs +++ b/src/Tahoe/SDMF/Internal/Encoding.hs @@ -7,9 +7,8 @@ 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.Hash (digestFromByteString) -import Crypto.Random (MonadRandom (getRandomBytes)) +import Crypto.Random (MonadRandom) import Data.Bifunctor (Bifunctor (bimap)) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as LB @@ -23,10 +22,6 @@ 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)) -randomIV = (makeIV :: B.ByteString -> Maybe (IV c)) <$> getRandomBytes (blockSize (undefined :: c)) - {- | Given a pre-determined key pair and sequence number, encode some ciphertext into a collection of SDMF shares. @@ -34,8 +29,8 @@ randomIV = (makeIV :: B.ByteString -> Maybe (IV c)) <$> getRandomBytes (blockSiz Thus they cannot be re-used for "different" data. Any shares created with a given key pair are part of the same logical data object. -} -encode :: (MonadFail m, MonadIO m, MonadRandom m) => Keys.KeyPair -> Word64 -> Word16 -> Word16 -> LB.ByteString -> m ([Share], Writer) -encode keypair shareSequenceNumber required total ciphertext = do +encode :: (MonadFail m, MonadIO m, MonadRandom m) => Keys.KeyPair -> Keys.SDMF_IV -> Word64 -> Word16 -> Word16 -> LB.ByteString -> m ([Share], Writer) +encode keypair iv shareSequenceNumber required total ciphertext = do -- 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 @@ -46,8 +41,6 @@ encode keypair shareSequenceNumber required total ciphertext = do -- They look okay, we can proceed. blocks <- liftIO $ fmap LB.fromStrict <$> zfec (from required) (from total) paddedCiphertext - (Just iv) <- randomIV - -- 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 @@ -63,7 +56,7 @@ encode keypair shareSequenceNumber required total ciphertext = do flip $ makeShare shareSequenceNumber - (Keys.SDMF_IV iv) + iv requiredAsWord8 totalAsWord8 dataLength diff --git a/src/Tahoe/SDMF/Internal/Encrypting.hs b/src/Tahoe/SDMF/Internal/Encrypting.hs index b3b1db8155d1335b82029aacc6a36515e202df01..93bd5ed75eaf6003b487505e7e6402f79ce60e67 100644 --- a/src/Tahoe/SDMF/Internal/Encrypting.hs +++ b/src/Tahoe/SDMF/Internal/Encrypting.hs @@ -1,18 +1,44 @@ -- | Implement the encryption scheme used by SDMF. module Tahoe.SDMF.Internal.Encrypting where -import Crypto.Cipher.Types (ctrCombine, nullIV) +import Crypto.Cipher.AES (AES128) +import Crypto.Cipher.Types (BlockCipher (blockSize), ctrCombine, makeIV, nullIV) +import Crypto.Random (MonadRandom (getRandomBytes)) +import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as LB import qualified Tahoe.SDMF.Internal.Keys as Keys +-- | Randomly generate a new IV suitable for use with the block cipher used by SDMF. +randomIV :: MonadRandom m => m (Maybe Keys.SDMF_IV) +randomIV = (fmap Keys.SDMF_IV . makeIV :: B.ByteString -> Maybe Keys.SDMF_IV) <$> getRandomBytes (blockSize (undefined :: AES128)) + {- | Encrypt plaintext bytes according to the scheme used for SDMF share construction. -} -encrypt :: Keys.Data -> LB.ByteString -> LB.ByteString -encrypt Keys.Data{unData} = LB.fromStrict . ctrCombine unData nullIV . LB.toStrict +encrypt :: Keys.KeyPair -> Keys.SDMF_IV -> LB.ByteString -> LB.ByteString +encrypt keypair iv = encryptWithDataKey dataKey + where + signatureKey = Keys.toSignatureKey keypair + (Just writeKey) = Keys.deriveWriteKey signatureKey + (Just readKey) = Keys.deriveReadKey writeKey + (Just dataKey) = Keys.deriveDataKey iv readKey {- | Decrypt ciphertext bytes according to the scheme used for SDMF share construction. -} -decrypt :: Keys.Data -> LB.ByteString -> LB.ByteString -decrypt = encrypt +decrypt :: Keys.Read -> Keys.SDMF_IV -> LB.ByteString -> LB.ByteString +decrypt readKey iv = decryptWithDataKey dataKey + where + (Just dataKey) = Keys.deriveDataKey iv readKey + +{- | Encrypt plaintext bytes according to the scheme used for SDMF share + construction using a pre-computed data encryption key. +-} +encryptWithDataKey :: Keys.Data -> LB.ByteString -> LB.ByteString +encryptWithDataKey Keys.Data{unData} = LB.fromStrict . ctrCombine unData nullIV . LB.toStrict + +{- | Decrypt ciphertext bytes according to the scheme used for SDMF share + construction using a pre-computed data encryption key. +-} +decryptWithDataKey :: Keys.Data -> LB.ByteString -> LB.ByteString +decryptWithDataKey = encryptWithDataKey diff --git a/tahoe-ssk.cabal b/tahoe-ssk.cabal index 68fbb5a1214c4eb5f111df540624ce86a0ad69f0..bd04c33880d49b8ad7b4a3eb16633a001d330818 100644 --- a/tahoe-ssk.cabal +++ b/tahoe-ssk.cabal @@ -164,3 +164,19 @@ executable make-keypairs , cryptonite , tahoe-ssk , x509 + +executable encode-ssk + import: + warnings + , language + + main-is: Main.hs + hs-source-dirs: encode-ssk + build-depends: + , base + , base32 + , binary + , bytestring + , cryptonite + , tahoe-ssk + , text diff --git a/test/Spec.hs b/test/Spec.hs index 0b3c1648c4318bd6506d2a27db06733d511bbf6e..17d43b89b6f9c4a6b1013e57da86cc539b6f3ab6 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -22,7 +22,7 @@ import Data.ByteString.Base32 (decodeBase32Unpadded, encodeBase32Unpadded) import qualified Data.ByteString.Lazy as LB import Data.Either (rights) import qualified Data.Text as T -import Generators (capabilities, encodingParameters, genRSAKeys, shareHashChains, shares) +import Generators (capabilities, encodingParameters, genRSAKeys, ivLength, shareHashChains, shares) import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range import System.IO (hSetEncoding, stderr, stdout, utf8) @@ -175,11 +175,13 @@ tests = , testProperty "Ciphertext round-trips through encode . decode" $ property $ do keypair <- forAll genRSAKeys + ivBytes <- forAll $ Gen.bytes (Range.singleton ivLength) + let Just iv = Keys.SDMF_IV <$> makeIV ivBytes ciphertext <- forAll $ LB.fromStrict <$> Gen.bytes (Range.exponential 1 1024) sequenceNumber <- forAll $ Gen.integral Range.exponentialBounded (required, total) <- forAll encodingParameters - (shares', Tahoe.SDMF.Writer{Tahoe.SDMF.writerReader}) <- liftIO $ Tahoe.SDMF.encode keypair sequenceNumber required total ciphertext + (shares', Tahoe.SDMF.Writer{Tahoe.SDMF.writerReader}) <- liftIO $ Tahoe.SDMF.encode keypair iv sequenceNumber required total ciphertext annotateShow shares' @@ -190,12 +192,11 @@ tests = do keypair <- forAll genRSAKeys (Just iv) <- fmap Keys.SDMF_IV <$> (makeIV <$> forAll (Gen.bytes (Range.singleton 16))) - let (Just dataKey) = do + let (Just readKey) = do writeKey <- Keys.deriveWriteKey (Keys.toSignatureKey keypair) - readKey <- Keys.deriveReadKey writeKey - Keys.deriveDataKey iv readKey + Keys.deriveReadKey writeKey plaintext <- forAll $ LB.fromStrict <$> Gen.bytes (Range.exponential 1 1024) - tripping plaintext (Tahoe.SDMF.encrypt dataKey) (Just . Tahoe.SDMF.decrypt dataKey) + tripping plaintext (Tahoe.SDMF.encrypt keypair iv) (Just . Tahoe.SDMF.decrypt readKey iv) , testCase "Recover plaintext from a known-correct slot" $ do s0 <- liftIO $ Binary.decode <$> (LB.readFile "test/data/3of10.0" >>= readShareFromBucket) s6 <- liftIO $ Binary.decode <$> (LB.readFile "test/data/3of10.6" >>= readShareFromBucket) @@ -213,7 +214,7 @@ tests = expectedPlaintext = "abcdefghijklmnopqrstuvwxyzZYXWVUTSRQPONMLKJIJHGRFCBA1357" (Just dataKey) = Keys.deriveDataKey (Tahoe.SDMF.shareIV s0) readerReadKey - recoveredPlaintext = Tahoe.SDMF.decrypt dataKey ciphertext + recoveredPlaintext = Tahoe.SDMF.decrypt readerReadKey (Tahoe.SDMF.shareIV s0) ciphertext assertEqual "read key: expected /= derived" expectedReadKey readerReadKey assertEqual "data key: expected /= derived" expectedDataKey dataKey