Files
simplexmq/src/Simplex/FileTransfer/Protocol.hs
T
Evgeny Poberezkin ad4b5b6b71 parameterize transport handle with transport peer to include server certificate (#1100)
* parameterize transport handle with transport peer to include server certificate

* include server certificate into THandle

* load server chain and sign key

* fix key type

* fix for 8.10

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
Co-authored-by: IC Rainbow <aenor.realm@gmail.com>
2024-04-15 13:47:48 +01:00

350 lines
11 KiB
Haskell

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
module Simplex.FileTransfer.Protocol where
import qualified Data.Aeson.TH as J
import Data.Bifunctor (first)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Kind (Type)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe (isNothing)
import Data.Type.Equality
import Data.Word (Word32)
import Simplex.FileTransfer.Transport (XFTPErrorType (..), XFTPVersion, xftpClientHandshakeStub)
import Simplex.Messaging.Client (authTransmission)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers
import Simplex.Messaging.Protocol
( BasicAuth,
CommandError (..),
Protocol (..),
ProtocolEncoding (..),
ProtocolErrorType (..),
ProtocolMsgTag (..),
ProtocolType (..),
RcvPublicAuthKey,
RcvPublicDhKey,
RecipientId,
SenderId,
SentRawTransmission,
SignedTransmission,
SndPublicAuthKey,
Transmission,
TransmissionForAuth (..),
encodeTransmission,
encodeTransmissionForAuth,
messageTagP,
tDecodeParseValidate,
tEncodeBatch1,
tParse,
)
import Simplex.Messaging.Transport (THandleParams (..), TransportError (..), TransportPeer (..))
import Simplex.Messaging.Util ((<$?>))
xftpBlockSize :: Int
xftpBlockSize = 16384
-- | File protocol clients
data FileParty = FRecipient | FSender
deriving (Eq, Show)
data SFileParty :: FileParty -> Type where
SFRecipient :: SFileParty FRecipient
SFSender :: SFileParty FSender
instance TestEquality SFileParty where
testEquality SFRecipient SFRecipient = Just Refl
testEquality SFSender SFSender = Just Refl
testEquality _ _ = Nothing
deriving instance Eq (SFileParty p)
deriving instance Show (SFileParty p)
data AFileParty = forall p. FilePartyI p => AFP (SFileParty p)
toFileParty :: SFileParty p -> FileParty
toFileParty = \case
SFRecipient -> FRecipient
SFSender -> FSender
aFileParty :: FileParty -> AFileParty
aFileParty = \case
FRecipient -> AFP SFRecipient
FSender -> AFP SFSender
class FilePartyI (p :: FileParty) where sFileParty :: SFileParty p
instance FilePartyI FRecipient where sFileParty = SFRecipient
instance FilePartyI FSender where sFileParty = SFSender
data FileCommandTag (p :: FileParty) where
FNEW_ :: FileCommandTag FSender
FADD_ :: FileCommandTag FSender
FPUT_ :: FileCommandTag FSender
FDEL_ :: FileCommandTag FSender
FGET_ :: FileCommandTag FRecipient
FACK_ :: FileCommandTag FRecipient
PING_ :: FileCommandTag FRecipient
deriving instance Show (FileCommandTag p)
data FileCmdTag = forall p. FilePartyI p => FCT (SFileParty p) (FileCommandTag p)
instance FilePartyI p => Encoding (FileCommandTag p) where
smpEncode = \case
FNEW_ -> "FNEW"
FADD_ -> "FADD"
FPUT_ -> "FPUT"
FDEL_ -> "FDEL"
FGET_ -> "FGET"
FACK_ -> "FACK"
PING_ -> "PING"
smpP = messageTagP
instance Encoding FileCmdTag where
smpEncode (FCT _ t) = smpEncode t
smpP = messageTagP
instance ProtocolMsgTag FileCmdTag where
decodeTag = \case
"FNEW" -> Just $ FCT SFSender FNEW_
"FADD" -> Just $ FCT SFSender FADD_
"FPUT" -> Just $ FCT SFSender FPUT_
"FDEL" -> Just $ FCT SFSender FDEL_
"FGET" -> Just $ FCT SFRecipient FGET_
"FACK" -> Just $ FCT SFRecipient FACK_
"PING" -> Just $ FCT SFRecipient PING_
_ -> Nothing
instance FilePartyI p => ProtocolMsgTag (FileCommandTag p) where
decodeTag s = decodeTag s >>= (\(FCT _ t) -> checkParty' t)
instance Protocol XFTPVersion XFTPErrorType FileResponse where
type ProtoCommand FileResponse = FileCmd
type ProtoType FileResponse = 'PXFTP
protocolClientHandshake = xftpClientHandshakeStub
protocolPing = FileCmd SFRecipient PING
protocolError = \case
FRErr e -> Just e
_ -> Nothing
data FileCommand (p :: FileParty) where
FNEW :: FileInfo -> NonEmpty RcvPublicAuthKey -> Maybe BasicAuth -> FileCommand FSender
FADD :: NonEmpty RcvPublicAuthKey -> FileCommand FSender
FPUT :: FileCommand FSender
FDEL :: FileCommand FSender
FGET :: RcvPublicDhKey -> FileCommand FRecipient
FACK :: FileCommand FRecipient
PING :: FileCommand FRecipient
deriving instance Show (FileCommand p)
data FileCmd = forall p. FilePartyI p => FileCmd (SFileParty p) (FileCommand p)
deriving instance Show FileCmd
data FileInfo = FileInfo
{ sndKey :: SndPublicAuthKey,
size :: Word32,
digest :: ByteString
}
deriving (Show)
type XFTPFileId = ByteString
instance FilePartyI p => ProtocolEncoding XFTPVersion XFTPErrorType (FileCommand p) where
type Tag (FileCommand p) = FileCommandTag p
encodeProtocol _v = \case
FNEW file rKeys auth_ -> e (FNEW_, ' ', file, rKeys, auth_)
FADD rKeys -> e (FADD_, ' ', rKeys)
FPUT -> e FPUT_
FDEL -> e FDEL_
FGET rKey -> e (FGET_, ' ', rKey)
FACK -> e FACK_
PING -> e PING_
where
e :: Encoding a => a -> ByteString
e = smpEncode
protocolP v tag = (\(FileCmd _ c) -> checkParty c) <$?> protocolP v (FCT (sFileParty @p) tag)
fromProtocolError = fromProtocolError @XFTPVersion @XFTPErrorType @FileResponse
{-# INLINE fromProtocolError #-}
checkCredentials (auth, _, fileId, _) cmd = case cmd of
-- FNEW must not have signature and chunk ID
FNEW {}
| isNothing auth -> Left $ CMD NO_AUTH
| not (B.null fileId) -> Left $ CMD HAS_AUTH
| otherwise -> Right cmd
PING
| isNothing auth && B.null fileId -> Right cmd
| otherwise -> Left $ CMD HAS_AUTH
-- other client commands must have both signature and queue ID
_
| isNothing auth || B.null fileId -> Left $ CMD NO_AUTH
| otherwise -> Right cmd
instance ProtocolEncoding XFTPVersion XFTPErrorType FileCmd where
type Tag FileCmd = FileCmdTag
encodeProtocol _v (FileCmd _ c) = encodeProtocol _v c
protocolP _v = \case
FCT SFSender tag ->
FileCmd SFSender <$> case tag of
FNEW_ -> FNEW <$> _smpP <*> smpP <*> smpP
FADD_ -> FADD <$> _smpP
FPUT_ -> pure FPUT
FDEL_ -> pure FDEL
FCT SFRecipient tag ->
FileCmd SFRecipient <$> case tag of
FGET_ -> FGET <$> _smpP
FACK_ -> pure FACK
PING_ -> pure PING
fromProtocolError = fromProtocolError @XFTPVersion @XFTPErrorType @FileResponse
{-# INLINE fromProtocolError #-}
checkCredentials t (FileCmd p c) = FileCmd p <$> checkCredentials t c
instance Encoding FileInfo where
smpEncode FileInfo {sndKey, size, digest} = smpEncode (sndKey, size, digest)
smpP = FileInfo <$> smpP <*> smpP <*> smpP
instance StrEncoding FileInfo where
strEncode FileInfo {sndKey, size, digest} = strEncode (sndKey, size, digest)
strP = FileInfo <$> strP_ <*> strP_ <*> strP
data FileResponseTag
= FRSndIds_
| FRRcvIds_
| FRFile_
| FROk_
| FRErr_
| FRPong_
deriving (Show)
instance Encoding FileResponseTag where
smpEncode = \case
FRSndIds_ -> "SIDS"
FRRcvIds_ -> "RIDS"
FRFile_ -> "FILE"
FROk_ -> "OK"
FRErr_ -> "ERR"
FRPong_ -> "PONG"
smpP = messageTagP
instance ProtocolMsgTag FileResponseTag where
decodeTag = \case
"SIDS" -> Just FRSndIds_
"RIDS" -> Just FRRcvIds_
"FILE" -> Just FRFile_
"OK" -> Just FROk_
"ERR" -> Just FRErr_
"PONG" -> Just FRPong_
_ -> Nothing
data FileResponse
= FRSndIds SenderId (NonEmpty RecipientId)
| FRRcvIds (NonEmpty RecipientId)
| FRFile RcvPublicDhKey C.CbNonce
| FROk
| FRErr XFTPErrorType
| FRPong
deriving (Show)
instance ProtocolEncoding XFTPVersion XFTPErrorType FileResponse where
type Tag FileResponse = FileResponseTag
encodeProtocol _v = \case
FRSndIds fId rIds -> e (FRSndIds_, ' ', fId, rIds)
FRRcvIds rIds -> e (FRRcvIds_, ' ', rIds)
FRFile rDhKey nonce -> e (FRFile_, ' ', rDhKey, nonce)
FROk -> e FROk_
FRErr err -> e (FRErr_, ' ', err)
FRPong -> e FRPong_
where
e :: Encoding a => a -> ByteString
e = smpEncode
protocolP _v = \case
FRSndIds_ -> FRSndIds <$> _smpP <*> smpP
FRRcvIds_ -> FRRcvIds <$> _smpP
FRFile_ -> FRFile <$> _smpP <*> smpP
FROk_ -> pure FROk
FRErr_ -> FRErr <$> _smpP
FRPong_ -> pure FRPong
fromProtocolError = \case
PECmdSyntax -> CMD SYNTAX
PECmdUnknown -> CMD UNKNOWN
PESession -> SESSION
PEBlock -> BLOCK
{-# INLINE fromProtocolError #-}
checkCredentials (_, _, entId, _) cmd = case cmd of
FRSndIds {} -> noEntity
-- ERR response does not always have entity ID
FRErr _ -> Right cmd
-- PONG response must not have queue ID
FRPong -> noEntity
-- other server responses must have entity ID
_
| B.null entId -> Left $ CMD NO_ENTITY
| otherwise -> Right cmd
where
noEntity
| B.null entId = Right cmd
| otherwise = Left $ CMD HAS_AUTH
checkParty :: forall t p p'. (FilePartyI p, FilePartyI p') => t p' -> Either String (t p)
checkParty c = case testEquality (sFileParty @p) (sFileParty @p') of
Just Refl -> Right c
Nothing -> Left "incorrect XFTP party"
checkParty' :: forall t p p'. (FilePartyI p, FilePartyI p') => t p' -> Maybe (t p)
checkParty' c = case testEquality (sFileParty @p) (sFileParty @p') of
Just Refl -> Just c
_ -> Nothing
xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion 'TClient -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString
xftpEncodeAuthTransmission thParams@THandleParams {thAuth} pKey (corrId, fId, msg) = do
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, fId, msg)
xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) corrId tForAuth
xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> Transmission c -> Either TransportError ByteString
xftpEncodeTransmission thParams (corrId, fId, msg) = do
let t = encodeTransmission thParams (corrId, fId, msg)
xftpEncodeBatch1 (Nothing, t)
-- this function uses batch syntax but puts only one transmission in the batch
xftpEncodeBatch1 :: SentRawTransmission -> Either TransportError ByteString
xftpEncodeBatch1 t = first (const TELargeMsg) $ C.pad (tEncodeBatch1 t) xftpBlockSize
xftpDecodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> ByteString -> Either XFTPErrorType (SignedTransmission e c)
xftpDecodeTransmission thParams t = do
t' <- first (const BLOCK) $ C.unPad t
case tParse thParams t' of
t'' :| [] -> Right $ tDecodeParseValidate thParams t''
_ -> Left BLOCK
$(J.deriveJSON (enumJSON $ dropPrefix "F") ''FileParty)