packages feed

tahoe-chk-0.2.0.0: test/Vectors.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Vectors where

import Control.Exception (IOException, try)
import Data.Aeson (
    FromJSON (..),
    ToJSON (..),
    Value (..),
    withText,
    (.:),
 )
import Data.Aeson.Types (
    parseFail,
    withObject,
 )
import qualified Data.Binary as Binary
import qualified Data.ByteString as B
import Data.ByteString.Base32 (decodeBase32Unpadded)
import qualified Data.ByteString.Base64 as Base64
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Word (Word32)
import Data.Yaml (ParseException, decodeEither')
import GHC.Generics (
    Generic,
 )
import Tahoe.CHK.Capability (Reader, pReader)
import Tahoe.CHK.Types (
    Parameters (..),
 )
import qualified Text.Megaparsec as M

newtype JSONByteString = JSONByteString B.ByteString deriving newtype (Ord, Eq)

instance Show JSONByteString where
    show (JSONByteString bs) = T.unpack . T.decodeLatin1 . Base64.encode $ bs

instance FromJSON JSONByteString where
    parseJSON =
        withText
            "base64 encoded bytestring"
            ( \t ->
                case Base64.decode . T.encodeUtf8 $ t of
                    Left err -> parseFail $ "parsing base64-encoded byte string failed" <> show err
                    Right stuff -> pure $ JSONByteString stuff
            )

instance ToJSON JSONByteString where
    toJSON (JSONByteString bs) = String . T.decodeLatin1 . Base64.encode $ bs

data SSKFormat
    = SDMF
        { sskPrivateKey :: T.Text
        }
    | MDMF
        { sskPrivateKey :: T.Text
        }
    deriving (Show, Ord, Eq)

data Format = CHK | SSK SSKFormat deriving (Show, Ord, Eq)

instance FromJSON Format where
    parseJSON = withObject "format" $ \o -> do
        kind <- o .: "kind"
        case kind of
            "chk" -> pure CHK
            "ssk" -> SSK <$> o .: "params"
            invalid -> parseFail $ "Unsupported format: " <> T.unpack invalid

instance FromJSON SSKFormat where
    parseJSON = withObject "ssk-format" $ \o -> do
        format <- o .: "format"
        key <- o .: "key"
        sskFormat <- case format of
            "sdmf" -> pure SDMF
            "mdmf" -> pure MDMF
            invalid -> parseFail $ "Unsupported SSK format: " <> T.unpack invalid
        pure $ sskFormat key

data Sample = Sample
    { sampleTemplate :: JSONByteString
    , sampleLength :: Int
    }
    deriving (Show, Ord, Eq)

instance FromJSON Sample where
    parseJSON = withObject "sample" $ \o ->
        Sample <$> o .: "seed" <*> o .: "length"

data VectorSpec = VectorSpec
    { version :: T.Text
    , vector :: [TestCase]
    }
    deriving (Generic, Show, Ord, Eq, FromJSON)

data TestCase = TestCase
    { convergence :: JSONByteString
    , format :: Format
    , sample :: Sample
    , zfec :: Parameters
    , expected :: T.Text
    }
    deriving (Generic, Show, Ord, Eq, FromJSON)

instance FromJSON Parameters where
    parseJSON = withObject "parameters" $ \o ->
        Parameters <$> o .: "segmentSize" <*> o .: "total" <*> pure 1 <*> o .: "required"

data LoadError = IOError IOException | ParseError ParseException deriving (Show)

-- | Load the test vectors from the yaml file.
loadTestVectorData :: IO (Either LoadError VectorSpec)
loadTestVectorData = go "test_vectors.yaml"
  where
    go :: String -> IO (Either LoadError VectorSpec)
    go path = do
        bs <- read' path
        pure $
            case bs of
                Left le -> Left . IOError $ le
                Right bs' -> parse bs'

    read' :: String -> IO (Either IOError B.ByteString)
    read' = try . B.readFile

    parse :: B.ByteString -> Either LoadError VectorSpec
    parse = either (Left . ParseError) pure . decodeEither'

data WellKnown = WellKnown
    { wellKnownPlaintext :: BL.ByteString
    , wellKnownConvergenceSecret :: B.ByteString
    , wellKnownParameters :: Parameters
    , wellKnownShares :: [BL.ByteString]
    , wellKnownCapability :: Reader
    }

{- | Load one "well-known" (that is: hard-coded and generated by Tahoe-LAFS)
 cases of CHK shares.
-}
loadWellKnownCase :: Parameters -> T.Text -> IO WellKnown
loadWellKnownCase wellKnownParameters@Parameters{paramRequiredShares, paramTotalShares} capText =
    do
        wellKnownShares <- mapM (fmap extractShareData . BL.readFile . sharePath) [0 .. paramTotalShares - 1]
        pure WellKnown{..}
  where
    -- Must be at least 56 bytes or we cannot get shares for comparison out of
    -- Tahoe-LAFS (instead, it emits a LIT cap).
    wellKnownPlaintext =
        "abcdefghijklmnopqrstuvwxyz\
        \ZYXWVUTSRQPONMLKJIJHGRFCBA\
        \1357"

    -- Hard-code the particular convergence secret used to generated the
    -- expected value.
    Right wellKnownConvergenceSecret = decodeBase32Unpadded "lcngfrvgaksfwrelc6ae5kucb3zufssoe6cj74rozcqibnl6uy2a"

    Right wellKnownCapability = M.parse pReader "" capText

    -- Find the share data in the source tree.
    sharePath n = concat ["test/", show paramRequiredShares, "of", show paramTotalShares, "." <> show n]

    -- Strip the share "container" bytes that is wrapped around the share data
    -- "proper" in our test data files.  These are the server-side bookkeeping
    -- bytes written by the Tahoe-LAFS storage server which generated our test
    -- data.
    extractShareData :: BL.ByteString -> BL.ByteString
    extractShareData container = shareData
      where
        shareData = BL.take (fromIntegral shareDataLength) . BL.drop 0x0c $ container
        shareDataLength = Binary.decode . BL.take 4 . BL.drop 4 $ container :: Word32