make KeyHash non-optional, verify KeyHash in SMP handshake, use StrEncoding class (#250)

* make KeyHash non-optional, StrEncoding class

* change server URI format in agent config, refactor with StrEncoding

* refactor Crypto using checkAlgorithm

* refactor parsing connection requests

* prepare to validate CA fingerprint sent in client handshake

* KeyHash check in handshake

* rename type to CliCommand

* server validates keyhash sent by the client

* validate -a option when parsing

* more of StrEncoding
This commit is contained in:
Evgeny Poberezkin
2022-01-02 22:24:43 +00:00
committed by GitHub
parent 33bb38299b
commit f3523bbba9
27 changed files with 577 additions and 591 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Transport (TLS, Transport (..))
cfg :: AgentConfig
cfg = defaultAgentConfig {smpServers = L.fromList ["localhost:5223#bU0K+bRg24xWW//lS0umO1Zdw/SXqpJNtm1/RrPLViE="]}
cfg = defaultAgentConfig {smpServers = L.fromList ["smp://bU0K-bRg24xWW__lS0umO1Zdw_SXqpJNtm1_RrPLViE=@localhost:5223"]}
logCfg :: LogConfig
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
+79 -86
View File
@@ -1,5 +1,6 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
@@ -8,24 +9,27 @@
module Main where
import Control.Monad.Except
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Composition ((.:))
import Data.Either (fromRight)
import Data.Ini (Ini, lookupValue, readIniFile)
import Data.List (dropWhileEnd)
import qualified Data.Text as T
import Data.X509.Validation (Fingerprint (..))
import Network.Socket (ServiceName)
import Options.Applicative
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Server (runSMPServer)
import Simplex.Messaging.Server.Env.STM
import Simplex.Messaging.Server.StoreLog (StoreLog, openReadStoreLog, storeLogFilePath)
import Simplex.Messaging.Transport (ATransport (..), TLS, Transport (..), encodeFingerprint, loadFingerprint, simplexMQVersion)
import Simplex.Messaging.Transport (ATransport (..), TLS, Transport (..), loadFingerprint, simplexMQVersion)
import Simplex.Messaging.Transport.WebSockets (WS)
import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist, removeDirectoryRecursive)
import System.Exit (exitFailure)
import System.FilePath (combine)
import System.IO (IOMode (..))
import System.IO (IOMode (..), hGetLine, withFile)
import System.Process (readCreateProcess, shell)
import Text.Read (readMaybe)
cfgDir :: FilePath
cfgDir = "/etc/opt/simplex"
@@ -56,58 +60,53 @@ fingerprintFile = combine cfgDir "fingerprint"
main :: IO ()
main = do
getCliOptions >>= \opts -> case optCommand opts of
Init initOptions@InitOptions {pubkeyAlgorithm} -> do
-- TODO check during parsing
checkPubkeyAlgorithm pubkeyAlgorithm
getCliCommand >>= \case
Init opts ->
doesFileExist iniFile >>= \case
True -> iniAlreadyExistsErr >> exitFailure
False -> initializeServer initOptions
Start -> do
True -> exitError $ "Error: server is already initialized (" <> iniFile <> " exists).\nRun `smp-server start`."
_ -> initializeServer opts
Start ->
doesFileExist iniFile >>= \case
False -> iniDoesNotExistErr >> exitFailure
True -> readIniFile iniFile >>= either (\e -> putStrLn e >> exitFailure) (runServer . mkIniOptions)
True -> readIniFile iniFile >>= either exitError (runServer . mkIniOptions)
_ -> exitError $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `smp-server init`."
Delete -> cleanup >> putStrLn "Deleted configuration and log files"
where
checkPubkeyAlgorithm alg
| alg == "ED448" || alg == "ED25519" = pure ()
| otherwise = putStrLn ("Unsupported public key algorithm " <> alg) >> exitFailure
iniAlreadyExistsErr = putStrLn $ "Error: server is already initialized (" <> iniFile <> " exists).\nRun `smp-server start`."
iniDoesNotExistErr = putStrLn $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `smp-server init`."
newtype CliOptions = CliOptions {optCommand :: Command}
exitError :: String -> IO ()
exitError msg = putStrLn msg >> exitFailure
data Command
data CliCommand
= Init InitOptions
| Start
| Delete
data InitOptions = InitOptions
{ enableStoreLog :: Bool,
pubkeyAlgorithm :: String
signAlgorithm :: SignAlgorithm
}
getCliOptions :: IO CliOptions
getCliOptions =
data SignAlgorithm = ED448 | ED25519
deriving (Read, Show)
getCliCommand :: IO CliCommand
getCliCommand =
customExecParser
(prefs showHelpOnEmpty)
( info
(helper <*> versionOption <*> cliOptionsP)
(helper <*> versionOption <*> cliCommandP)
(header version <> fullDesc)
)
where
versionOption = infoOption version (long "version" <> short 'v' <> help "Show version")
cliOptionsP :: Parser CliOptions
cliOptionsP =
CliOptions
<$> hsubparser
( command "init" (info initP (progDesc $ "Initialize server - creates " <> cfgDir <> " and " <> logDir <> " directories and configuration files"))
<> command "start" (info (pure Start) (progDesc $ "Start server (configuration: " <> iniFile <> ")"))
<> command "delete" (info (pure Delete) (progDesc "Delete configuration and log files"))
)
cliCommandP :: Parser CliCommand
cliCommandP =
hsubparser
( command "init" (info initP (progDesc $ "Initialize server - creates " <> cfgDir <> " and " <> logDir <> " directories and configuration files"))
<> command "start" (info (pure Start) (progDesc $ "Start server (configuration: " <> iniFile <> ")"))
<> command "delete" (info (pure Delete) (progDesc "Delete configuration and log files"))
)
where
initP :: Parser Command
initP :: Parser CliCommand
initP =
Init .: InitOptions
<$> switch
@@ -115,34 +114,35 @@ cliOptionsP =
<> short 'l'
<> help "Enable store log for SMP queues persistence"
)
<*> strOption
( long "pubkey-algorithm"
<*> option
(maybeReader readMaybe)
( long "sign-algorithm"
<> short 'a'
<> help "Public key algorithm used for certificate generation: ED25519, ED448"
<> value "ED448"
<> help "Signature algorithm used for TLS certificates: ED25519, ED448"
<> value ED448
<> showDefault
<> metavar "ALG"
)
initializeServer :: InitOptions -> IO ()
initializeServer InitOptions {enableStoreLog, pubkeyAlgorithm} = do
initializeServer InitOptions {enableStoreLog, signAlgorithm} = do
cleanup
createDirectoryIfMissing True cfgDir
createDirectoryIfMissing True logDir
createX509
saveFingerprint
fp <- saveFingerprint
createIni
putStrLn $ "Server initialized, you can modify configuration in " <> iniFile <> ".\nRun `smp-server start` to start server."
printServiceInfo
printServiceInfo fp
warnCAPrivateKeyFile
where
createX509 = do
createOpensslConf
-- CA certificate (identity/offline)
run $ "openssl genpkey -algorithm " <> pubkeyAlgorithm <> " -out " <> caKeyFile
run $ "openssl genpkey -algorithm " <> show signAlgorithm <> " -out " <> caKeyFile
run $ "openssl req -new -x509 -days 999999 -config " <> opensslCnfFile <> " -extensions v3_ca -key " <> caKeyFile <> " -out " <> caCrtFile
-- server certificate (online)
run $ "openssl genpkey -algorithm " <> pubkeyAlgorithm <> " -out " <> serverKeyFile
run $ "openssl genpkey -algorithm " <> show signAlgorithm <> " -out " <> serverKeyFile
run $ "openssl req -new -config " <> opensslCnfFile <> " -reqexts v3_req -key " <> serverKeyFile <> " -out " <> serverCsrFile
run $ "openssl x509 -req -days 999999 -copy_extensions copy -in " <> serverCsrFile <> " -CA " <> caCrtFile <> " -CAkey " <> caKeyFile <> " -out " <> serverCrtFile
where
@@ -168,8 +168,9 @@ initializeServer InitOptions {enableStoreLog, pubkeyAlgorithm} = do
\extendedKeyUsage = serverAuth\n"
saveFingerprint = do
fingerprint <- loadFingerprint caCrtFile
writeFile fingerprintFile $ (B.unpack . encodeFingerprint) fingerprint <> "\n"
Fingerprint fp <- loadFingerprint caCrtFile
withFile fingerprintFile WriteMode (`B.hPutStrLn` strEncode fp)
pure fp
createIni = do
writeFile iniFile $
@@ -207,45 +208,39 @@ mkIniOptions ini =
runServer :: IniOptions -> IO ()
runServer IniOptions {enableStoreLog, port, enableWebsockets} = do
checkSavedFingerprint
printServiceInfo
checkCAPrivateKeyFile
cfg <- setupServerConfig
fp <- checkSavedFingerprint
printServiceInfo fp
storeLog <- openStoreLog
let cfg = mkServerConfig storeLog
printServerConfig cfg
runSMPServer cfg
where
checkSavedFingerprint = do
savedFingerprint <- loadSavedFingerprint
fingerprint <- loadFingerprint caCrtFile
when (savedFingerprint /= (B.unpack . encodeFingerprint) fingerprint) $
putStrLn "Stored fingerprint is invalid." >> exitFailure
Fingerprint fp <- loadFingerprint caCrtFile
when (B.pack savedFingerprint /= strEncode fp) $
exitError "Stored fingerprint is invalid."
pure fp
checkCAPrivateKeyFile =
doesFileExist caKeyFile >>= (`when` (alert >> warnCAPrivateKeyFile))
where
alert = putStrLn $ "WARNING: " <> caKeyFile <> " is present on the server!"
mkServerConfig storeLog =
ServerConfig
{ transports = (port, transport @TLS) : [("80", transport @WS) | enableWebsockets],
tbqSize = 16,
serverTbqSize = 128,
msgQueueQuota = 256,
queueIdBytes = 24,
msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20
caCertificateFile = caCrtFile,
privateKeyFile = serverKeyFile,
certificateFile = serverCrtFile,
storeLog
}
setupServerConfig = do
storeLog <- openStoreLog
let transports = (port, transport @TLS) : [("80", transport @WS) | enableWebsockets]
pure
ServerConfig
{ tbqSize = 16,
serverTbqSize = 128,
msgQueueQuota = 256,
queueIdBytes = 24,
msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20
caCertificateFile = caCrtFile,
privateKeyFile = serverKeyFile,
certificateFile = serverCrtFile,
transports,
storeLog
}
where
openStoreLog :: IO (Maybe (StoreLog 'ReadMode))
openStoreLog
| enableStoreLog = Just <$> openReadStoreLog storeLogFile
| otherwise = pure Nothing
openStoreLog :: IO (Maybe (StoreLog 'ReadMode))
openStoreLog =
if enableStoreLog
then Just <$> openReadStoreLog storeLogFile
else pure Nothing
printServerConfig ServerConfig {storeLog, transports} = do
putStrLn $ case storeLog of
@@ -261,11 +256,10 @@ cleanup = do
where
deleteDirIfExists path = doesDirectoryExist path >>= (`when` removeDirectoryRecursive path)
printServiceInfo :: IO ()
printServiceInfo = do
printServiceInfo :: ByteString -> IO ()
printServiceInfo fpStr = do
putStrLn version
fingerprint <- loadSavedFingerprint
putStrLn $ "Fingerprint: " <> fingerprint
B.putStrLn $ "Fingerprint: " <> strEncode fpStr
version :: String
version = "SMP server v" <> simplexMQVersion
@@ -274,13 +268,12 @@ warnCAPrivateKeyFile :: IO ()
warnCAPrivateKeyFile =
putStrLn $
"----------\n\
\We highly recommend to remove CA private key file from the server and keep it securely in place of your choosing.\n\
\In case server's TLS credential is compromised you will be able to regenerate it using this key,\n\
\thus keeping server's identity and allowing clients to keep established connections. Key location:\n"
\You should store CA private key securely and delete it from the server.\n\
\If server TLS credential is compromised this key can be used to sign a new one, \
\keeping the same server identity and established connections.\n\
\CA private key location:\n"
<> caKeyFile
<> "\n----------"
loadSavedFingerprint :: IO String
loadSavedFingerprint = do
fingerprint <- readFile fingerprintFile
pure $ dropWhileEnd (== '\n') fingerprint
loadSavedFingerprint = withFile fingerprintFile ReadMode hGetLine
+1 -1
View File
@@ -1,7 +1,7 @@
CREATE TABLE servers (
host TEXT NOT NULL,
port TEXT,
key_hash BLOB,
key_hash BLOB NOT NULL,
PRIMARY KEY (host, port)
) WITHOUT ROWID;
+1
View File
@@ -43,6 +43,7 @@ library
Simplex.Messaging.Crypto
Simplex.Messaging.Crypto.Ratchet
Simplex.Messaging.Encoding
Simplex.Messaging.Encoding.String
Simplex.Messaging.Parsers
Simplex.Messaging.Protocol
Simplex.Messaging.Server
+1 -1
View File
@@ -691,7 +691,7 @@ newSndQueue_ ::
SMPQueueUri ->
ConnInfo ->
m (SndQueue, SMPConfirmation)
newSndQueue_ a (SMPQueueUri smpServer senderId rcvE2ePubDhKey) cInfo = do
newSndQueue_ a (SMPQueueUri smpServer senderId clientVersion rcvE2ePubDhKey) cInfo = do
(senderKey, sndPrivateKey) <- liftIO $ C.generateSignatureKeyPair a
(e2ePubKey, e2ePrivKey) <- liftIO C.generateKeyPair'
let sndQueue =
+1 -1
View File
@@ -255,7 +255,7 @@ newRcvQueue_ a c srv = do
sndId = Just sndId,
status = New
}
pure (rq, SMPQueueUri srv sndId e2eDhKey)
pure (rq, SMPQueueUri srv sndId SMP.smpClientVersion e2eDhKey)
subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnId -> m ()
subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connId = do
+1 -1
View File
@@ -43,7 +43,7 @@ defaultAgentConfig :: AgentConfig
defaultAgentConfig =
AgentConfig
{ tcpPort = "5224",
smpServers = undefined,
smpServers = undefined, -- TODO move it elsewhere?
cmdSignAlg = C.SignAlg C.SEd448,
connIdBytes = 12,
tbqSize = 16,
+103 -159
View File
@@ -41,6 +41,7 @@ module Simplex.Messaging.Agent.Protocol
AHeader (..),
AMessage (..),
SMPServer (..),
SrvLoc (..),
SMPQueueUri (..),
ConnectionMode (..),
SConnectionMode (..),
@@ -76,21 +77,12 @@ module Simplex.Messaging.Agent.Protocol
clientToAgentMsg,
serializeAgentMessage,
serializeMsgIntegrity,
serializeSMPQueueUri,
serializeConnMode,
serializeConnMode',
connMode,
connMode',
serializeConnReq,
serializeConnReq',
serializeAgentError,
serializeSmpErrorType,
commandP,
smpServerP,
smpQueueUriP,
connModeT,
connReqP,
connReqP',
msgIntegrityP,
agentErrorTypeP,
smpErrorTypeP,
@@ -105,7 +97,7 @@ module Simplex.Messaging.Agent.Protocol
)
where
import Control.Applicative (optional, (<|>))
import Control.Applicative ((<|>))
import Control.Monad.IO.Class
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
@@ -113,13 +105,13 @@ import Data.ByteString.Base64
import qualified Data.ByteString.Base64.URL as U
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Composition ((.:))
import Data.Functor (($>))
import Data.Int (Int64)
import Data.Kind (Type)
import Data.List (find)
import qualified Data.List.NonEmpty as L
import Data.Maybe (isJust)
import Data.String (IsString (..))
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Data.Time.ISO8601
@@ -127,10 +119,10 @@ import Data.Type.Equality
import Data.Typeable ()
import GHC.Generics (Generic)
import Generic.Random (genericArbitraryU)
import Network.HTTP.Types (parseSimpleQuery, renderSimpleQuery)
import Network.Socket (HostName, ServiceName)
import Network.HTTP.Types (SimpleQuery, parseSimpleQuery, renderSimpleQuery)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers
import Simplex.Messaging.Protocol
( ClientMessage (..),
@@ -138,11 +130,14 @@ import Simplex.Messaging.Protocol
MsgBody,
MsgId,
PrivHeader (..),
SMPServer (..),
SndPublicVerifyKey,
SrvLoc (..),
)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Transport (Transport (..), TransportError, serializeTransportError, transportErrorP)
import Simplex.Messaging.Util
import Simplex.Messaging.Version
import Test.QuickCheck (Arbitrary (..))
import Text.Read
import UnliftIO.Exception (Exception)
@@ -198,15 +193,12 @@ data ACommand (p :: AParty) where
END :: ACommand Agent
DOWN :: ACommand Agent
UP :: ACommand Agent
-- QST :: QueueDirection -> ACommand Client
-- STAT :: QueueDirection -> Maybe QueueStatus -> Maybe SubMode -> ACommand Agent
SEND :: MsgBody -> ACommand Client
MID :: AgentMsgId -> ACommand Agent
SENT :: AgentMsgId -> ACommand Agent
MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent
MSG :: MsgMeta -> MsgBody -> ACommand Agent
ACK :: AgentMsgId -> ACommand Client
-- RCVD :: AgentMsgId -> ACommand Agent
OFF :: ACommand Client
DEL :: ACommand Client
OK :: ACommand Agent
@@ -232,7 +224,7 @@ instance TestEquality SConnectionMode where
testEquality SCMContact SCMContact = Just Refl
testEquality _ _ = Nothing
data AConnectionMode = forall m. ACM (SConnectionMode m)
data AConnectionMode = forall m. ConnectionModeI m => ACM (SConnectionMode m)
instance Eq AConnectionMode where
ACM m == ACM m' = isJust $ testEquality m m'
@@ -293,16 +285,14 @@ data AHeader = AHeader
prevMsgHash :: MsgHash
}
serializeAHeader :: AHeader -> ByteString
serializeAHeader AHeader {sndMsgId, prevMsgHash} =
bshow sndMsgId <> " " <> encode prevMsgHash <> "\n"
instance StrEncoding AHeader where
strEncode AHeader {sndMsgId, prevMsgHash} =
bshow sndMsgId <> " " <> strEncode prevMsgHash <> "\n"
strP = AHeader <$> A.decimal <* A.space <*> (strP <|> pure "") <* A.endOfLine
emptyAHeader :: ByteString
emptyAHeader = "\n"
aHeaderP :: Parser AHeader
aHeaderP = AHeader <$> A.decimal <* A.space <*> (base64P <|> pure "") <* A.endOfLine
emptyAHeaderP :: Parser ()
emptyAHeaderP = A.endOfLine $> ()
@@ -326,9 +316,9 @@ agentToClientMsg = \case
AgentConfirmation senderKey cInfo ->
ClientMessage (PHConfirmation senderKey) $ emptyAHeader <> cInfo
AgentInvitation cReq cInfo ->
ClientMessage PHEmpty $ emptyAHeader <> serializeConnReq' cReq <> "\n" <> cInfo
ClientMessage PHEmpty $ emptyAHeader <> strEncode cReq <> "\n" <> cInfo
AgentMessage header aMsg ->
ClientMessage PHEmpty $ serializeAHeader header <> serializeAMessage aMsg
ClientMessage PHEmpty $ strEncode header <> strEncode aMsg
clientToAgentMsg :: ClientMessage -> Either AgentErrorType AgentMessage
clientToAgentMsg (ClientMessage header body) = parse parser (AGENT A_MESSAGE) body
@@ -336,121 +326,79 @@ clientToAgentMsg (ClientMessage header body) = parse parser (AGENT A_MESSAGE) bo
parser = case header of
PHConfirmation senderKey -> AgentConfirmation senderKey <$> (emptyAHeaderP *> A.takeByteString)
PHEmpty -> invitationP <|> messageP
invitationP = AgentInvitation <$> (emptyAHeaderP *> connReqP' <* A.endOfLine) <*> A.takeByteString
messageP = AgentMessage <$> aHeaderP <*> aMessageP
invitationP = AgentInvitation <$> (emptyAHeaderP *> strP <* A.endOfLine) <*> A.takeByteString
messageP = AgentMessage <$> strP <*> strP
aMessageP :: Parser AMessage
aMessageP =
"HELLO" $> HELLO
<|> "REPLY " *> reply
<|> "MSG " *> a_msg
where
reply = REPLY <$> connReqP'
a_msg = A_MSG <$> A.takeByteString
instance StrEncoding AMessage where
strP =
"HELLO" $> HELLO
<|> "REPLY " *> (REPLY <$> strP)
<|> "MSG " *> (A_MSG <$> A.takeByteString)
strEncode = \case
HELLO -> "HELLO"
REPLY cReq -> "REPLY " <> strEncode cReq
A_MSG body -> "MSG " <> body
-- | SMP server location parser.
smpServerP :: Parser SMPServer
smpServerP = SMPServer <$> server <*> optional port <*> kHash
where
server = B.unpack <$> A.takeWhile1 (A.notInClass ":#,; ")
port = A.char ':' *> (B.unpack <$> A.takeWhile1 A.isDigit)
kHash = Just . C.KeyHash <$> (A.char '#' *> base64P)
instance StrEncoding SMPQueueUri where
strEncode SMPQueueUri {smpServer = srv, senderId = qId, smpVersionRange = vr, dhPublicKey = k} =
strEncode srv <> "/" <> U.encode qId <> "#" <> strEncode k
strP = do
smpServer <- strP <* A.char '/'
senderId <- strP <* A.char '#'
let smpVersionRange = SMP.smpClientVersion
dhPublicKey <- strP
pure SMPQueueUri {smpServer, senderId, smpVersionRange, dhPublicKey}
serializeAMessage :: AMessage -> ByteString
serializeAMessage = \case
HELLO -> "HELLO"
REPLY cReq -> "REPLY " <> serializeConnReq' cReq
A_MSG body -> "MSG " <> body
newtype QueryStringParams = QSP SimpleQuery
-- | Serialize SMP queue information that is sent out-of-band.
serializeSMPQueueUri :: SMPQueueUri -> ByteString
serializeSMPQueueUri (SMPQueueUri srv qId dhKey) =
serializeServerUri srv <> "/" <> U.encode qId <> "#" <> C.serializePubKeyUri' dhKey
instance StrEncoding QueryStringParams where
strEncode (QSP q) = renderSimpleQuery True q
strP = QSP . parseSimpleQuery <$> A.takeTill (\c -> c == ' ' || c == '\n')
-- | SMP queue information parser.
smpQueueUriP :: Parser SMPQueueUri
smpQueueUriP =
SMPQueueUri <$> smpServerUriP <* A.char '/' <*> base64UriP <* A.char '#' <*> C.strPubKeyUriP
queryParam :: StrEncoding a => ByteString -> QueryStringParams -> Parser a
queryParam name (QSP q) =
case find ((== name) . fst) q of
Just (_, p) -> either fail pure $ parseAll strP p
_ -> fail $ "no qs param " <> B.unpack name
serializeConnReq :: AConnectionRequest -> ByteString
serializeConnReq (ACR _ cr) = serializeConnReq' cr
instance forall m. ConnectionModeI m => StrEncoding (ConnectionRequest m) where
strEncode = \case
CRInvitation crData -> serialize "invitation" crData
CRContact crData -> serialize "contact" crData
where
serialize crMode ConnReqData {crScheme, crSmpQueues, crEncryption = _} =
strEncode crScheme <> "/" <> crMode <> "#/" <> queryStr
where
queryStr = strEncode $ QSP [("smp", strEncode crSmpQueues), ("e2e", "")]
strP = do
ACR m cr <- strP
case testEquality m $ sConnectionMode @m of
Just Refl -> pure cr
_ -> fail "bad connection request mode"
serializeConnReq' :: ConnectionRequest m -> ByteString
serializeConnReq' = \case
CRInvitation crData -> serialize CMInvitation crData
CRContact crData -> serialize CMContact crData
where
serialize crMode ConnReqData {crScheme, crSmpQueues, crEncryption = _} =
sch <> "/" <> m <> "#/" <> queryStr
where
sch = case crScheme of
CRSSimplex -> "simplex:"
CRSAppServer host port -> B.pack $ "https://" <> host <> maybe "" (':' :) port
m = case crMode of
CMInvitation -> "invitation"
CMContact -> "contact"
queryStr = renderSimpleQuery True [("smp", queues), ("e2e", "")]
queues = B.intercalate "," . map serializeSMPQueueUri $ L.toList crSmpQueues
instance StrEncoding AConnectionRequest where
strEncode (ACR _ cr) = strEncode cr
strP = do
crScheme <- strP
mkConnReq <- "/" *> mkConnReqP <* "#/?"
query <- strP
crSmpQueues <- queryParam "smp" query
let crEncryption = ConnectionEncryption
pure $ mkConnReq ConnReqData {crScheme, crSmpQueues, crEncryption}
where
mkConnReqP =
"invitation" $> ACR SCMInvitation . CRInvitation
<|> "contact" $> ACR SCMContact . CRContact
connReqP' :: forall m. ConnectionModeI m => Parser (ConnectionRequest m)
connReqP' = do
ACR m cr <- connReqP
case testEquality m $ sConnectionMode @m of
Just Refl -> pure cr
_ -> fail "bad connection request mode"
instance StrEncoding ConnectionMode where
strEncode = \case
CMInvitation -> "INV"
CMContact -> "CON"
strP = "INV" $> CMInvitation <|> "CON" $> CMContact
connReqP :: Parser AConnectionRequest
connReqP = do
crScheme <- "simplex:" $> CRSSimplex <|> "https://" *> appServer
crMode <- "/" *> mode <* "#/?"
query <- parseSimpleQuery <$> A.takeTill (\c -> c == ' ' || c == '\n')
crSmpQueues <- paramP "smp" smpQueues query
let crEncryption = ConnectionEncryption
cReq = ConnReqData {crScheme, crSmpQueues, crEncryption}
pure $ case crMode of
CMInvitation -> ACR SCMInvitation $ CRInvitation cReq
CMContact -> ACR SCMContact $ CRContact cReq
where
appServer = CRSAppServer <$> host <*> optional port
host = B.unpack <$> A.takeTill (\c -> c == ':' || c == '/')
port = B.unpack <$> (A.char ':' *> A.takeTill (== '/'))
mode = "invitation" $> CMInvitation <|> "contact" $> CMContact
paramP param parser query =
let p = maybe (fail "") (pure . snd) $ find ((== param) . fst) query
in parseAll parser <$?> p
smpQueues =
maybe (fail "no SMP queues") pure . L.nonEmpty
=<< (smpQueue `A.sepBy1'` A.char ',')
smpQueue = parseAll smpQueueUriP <$?> A.takeTill (== ',')
-- | Serialize SMP server URI.
serializeServerUri :: SMPServer -> ByteString
serializeServerUri SMPServer {host, port, keyHash} = "smp://" <> kh <> B.pack host <> p
where
kh = maybe "" ((<> "@") . U.encode . C.unKeyHash) keyHash
p = B.pack $ maybe "" (':' :) port
smpServerUriP :: Parser SMPServer
smpServerUriP = do
_ <- "smp://"
keyHash <- C.KeyHash <$> (U.decode <$?> A.takeTill (== '@') <* A.char '@')
host <- B.unpack <$> A.takeWhile1 (A.notInClass ":#,;/ ")
port <- optional $ B.unpack <$> (A.char ':' *> A.takeWhile1 A.isDigit)
pure SMPServer {host, port, keyHash = Just keyHash}
serializeConnMode :: AConnectionMode -> ByteString
serializeConnMode (ACM cMode) = serializeConnMode' $ connMode cMode
serializeConnMode' :: ConnectionMode -> ByteString
serializeConnMode' = \case
CMInvitation -> "INV"
CMContact -> "CON"
connModeP' :: Parser ConnectionMode
connModeP' = "INV" $> CMInvitation <|> "CON" $> CMContact
connModeP :: Parser AConnectionMode
connModeP = connMode' <$> connModeP'
instance StrEncoding AConnectionMode where
strEncode (ACM cMode) = strEncode $ connMode cMode
strP = connMode' <$> strP
connModeT :: Text -> Maybe ConnectionMode
connModeT = \case
@@ -458,17 +406,6 @@ connModeT = \case
"CON" -> Just CMContact
_ -> Nothing
-- | SMP server location and transport key digest (hash).
data SMPServer = SMPServer
{ host :: HostName,
port :: Maybe ServiceName,
keyHash :: Maybe C.KeyHash -- TODO make non optional
}
deriving (Eq, Ord, Show)
instance IsString SMPServer where
fromString = parseString $ parseAll smpServerP
-- | SMP agent connection alias.
type ConnId = ByteString
@@ -482,6 +419,7 @@ type InvitationId = ByteString
data SMPQueueUri = SMPQueueUri
{ smpServer :: SMPServer,
senderId :: SMP.SenderId,
smpVersionRange :: VersionRange,
dhPublicKey :: C.PublicKeyX25519
}
deriving (Eq, Show)
@@ -494,7 +432,7 @@ deriving instance Eq (ConnectionRequest m)
deriving instance Show (ConnectionRequest m)
data AConnectionRequest = forall m. ACR (SConnectionMode m) (ConnectionRequest m)
data AConnectionRequest = forall m. ConnectionModeI m => ACR (SConnectionMode m) (ConnectionRequest m)
instance Eq AConnectionRequest where
ACR m cr == ACR m' cr' = case testEquality m m' of
@@ -510,17 +448,23 @@ data ConnReqData = ConnReqData
}
deriving (Eq, Show)
data ConnReqScheme = CRSSimplex | CRSAppServer HostName (Maybe ServiceName)
data ConnReqScheme = CRSSimplex | CRSAppServer SrvLoc
deriving (Eq, Show)
instance StrEncoding ConnReqScheme where
strEncode = \case
CRSSimplex -> "simplex:"
CRSAppServer srv -> "https://" <> strEncode srv
strP =
"simplex:" $> CRSSimplex
<|> "https://" *> (CRSAppServer <$> strP)
-- TODO this is a stub for double ratchet E2E encryption parameters (2 public DH keys)
data ConnectionEncryption = ConnectionEncryption
deriving (Eq, Show)
simplexChat :: ConnReqScheme
simplexChat = CRSAppServer "simplex.chat" Nothing
data QueueDirection = SND | RCV deriving (Show)
simplexChat = CRSAppServer $ SrvLoc "simplex.chat" Nothing
-- | SMP queue status.
data QueueStatus
@@ -665,20 +609,20 @@ commandP =
<|> "CON" $> ACmd SAgent CON
<|> "OK" $> ACmd SAgent OK
where
newCmd = ACmd SClient . NEW <$> connModeP
invResp = ACmd SAgent . INV <$> connReqP
joinCmd = ACmd SClient <$> (JOIN <$> connReqP <* A.space <*> A.takeByteString)
confMsg = ACmd SAgent <$> (CONF <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString)
letCmd = ACmd SClient <$> (LET <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString)
reqMsg = ACmd SAgent <$> (REQ <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString)
acptCmd = ACmd SClient <$> (ACPT <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString)
newCmd = ACmd SClient . NEW <$> strP
invResp = ACmd SAgent . INV <$> strP
joinCmd = ACmd SClient .: JOIN <$> strP_ <*> A.takeByteString
confMsg = ACmd SAgent .: CONF <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString
letCmd = ACmd SClient .: LET <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString
reqMsg = ACmd SAgent .: REQ <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString
acptCmd = ACmd SClient .: ACPT <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString
rjctCmd = ACmd SClient . RJCT <$> A.takeByteString
infoCmd = ACmd SAgent . INFO <$> A.takeByteString
sendCmd = ACmd SClient . SEND <$> A.takeByteString
msgIdResp = ACmd SAgent . MID <$> A.decimal
sentResp = ACmd SAgent . SENT <$> A.decimal
msgErrResp = ACmd SAgent <$> (MERR <$> A.decimal <* A.space <*> agentErrorTypeP)
message = ACmd SAgent <$> (MSG <$> msgMetaP <* A.space <*> A.takeByteString)
msgErrResp = ACmd SAgent .: MERR <$> A.decimal <* A.space <*> agentErrorTypeP
message = ACmd SAgent .: MSG <$> msgMetaP <* A.space <*> A.takeByteString
ackCmd = ACmd SClient . ACK <$> A.decimal
msgMetaP = do
integrity <- msgIntegrityP
@@ -705,9 +649,9 @@ parseCommand = parse commandP $ CMD SYNTAX
-- | Serialize SMP agent command.
serializeCommand :: ACommand p -> ByteString
serializeCommand = \case
NEW cMode -> "NEW " <> serializeConnMode cMode
INV cReq -> "INV " <> serializeConnReq cReq
JOIN cReq cInfo -> B.unwords ["JOIN", serializeConnReq cReq, serializeBinary cInfo]
NEW cMode -> "NEW " <> strEncode cMode
INV cReq -> "INV " <> strEncode cReq
JOIN cReq cInfo -> B.unwords ["JOIN", strEncode cReq, serializeBinary cInfo]
CONF confId cInfo -> B.unwords ["CONF", confId, serializeBinary cInfo]
LET confId cInfo -> B.unwords ["LET", confId, serializeBinary cInfo]
REQ invId cInfo -> B.unwords ["REQ", invId, serializeBinary cInfo]
+8 -7
View File
@@ -52,6 +52,7 @@ import Simplex.Messaging.Agent.Store
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration)
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (blobFieldParser)
import Simplex.Messaging.Protocol (MsgBody)
import qualified Simplex.Messaging.Protocol as SMP
@@ -545,19 +546,19 @@ instance ToField MsgIntegrity where toField = toField . serializeMsgIntegrity
instance FromField MsgIntegrity where fromField = blobFieldParser msgIntegrityP
instance ToField SMPQueueUri where toField = toField . serializeSMPQueueUri
instance ToField SMPQueueUri where toField = toField . strEncode
instance FromField SMPQueueUri where fromField = blobFieldParser smpQueueUriP
instance FromField SMPQueueUri where fromField = blobFieldParser strP
instance ToField AConnectionRequest where toField = toField . serializeConnReq
instance ToField AConnectionRequest where toField = toField . strEncode
instance FromField AConnectionRequest where fromField = blobFieldParser connReqP
instance FromField AConnectionRequest where fromField = blobFieldParser strP
instance ToField (ConnectionRequest c) where toField = toField . serializeConnReq'
instance ConnectionModeI c => ToField (ConnectionRequest c) where toField = toField . strEncode
instance (E.Typeable c, ConnectionModeI c) => FromField (ConnectionRequest c) where fromField = blobFieldParser connReqP'
instance (E.Typeable c, ConnectionModeI c) => FromField (ConnectionRequest c) where fromField = blobFieldParser strP
instance ToField ConnectionMode where toField = toField . decodeLatin1 . serializeConnMode'
instance ToField ConnectionMode where toField = toField . decodeLatin1 . strEncode
instance FromField ConnectionMode where fromField = fromTextField_ connModeT
+1 -2
View File
@@ -61,7 +61,6 @@ import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe)
import Network.Socket (ServiceName)
import Numeric.Natural
import Simplex.Messaging.Agent.Protocol (SMPServer (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol
import Simplex.Messaging.Transport (ATransport (..), THandle (..), TLS, TProxy, Transport (..), TransportError, clientHandshake, runTransportClient)
@@ -171,7 +170,7 @@ getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, smpPing} msgQ dis
client :: forall c. Transport c => TProxy c -> SMPClient -> TMVar (Either SMPClientError (THandle c)) -> c -> IO ()
client _ c thVar h =
runExceptT (clientHandshake h) >>= \case
runExceptT (clientHandshake h $ keyHash smpServer) >>= \case
Left e -> atomically . putTMVar thVar . Left $ SMPTransportError e
Right th@THandle {sessionId} -> do
atomically $ do
+106 -189
View File
@@ -52,7 +52,6 @@ module Simplex.Messaging.Crypto
DhSecret (..),
DhSecretX25519,
ADhSecret (..),
CryptoDhSecret (..),
KeyHash (..),
generateKeyPair,
generateKeyPair',
@@ -61,15 +60,7 @@ module Simplex.Messaging.Crypto
privateToX509,
-- * key encoding/decoding
serializePubKey,
serializePubKey',
serializePubKeyUri,
serializePubKeyUri',
strPubKeyP,
strPubKeyUriP,
encodePubKey,
encodePubKey',
binaryPubKeyP,
encodePrivKey,
-- * E2E hybrid encryption scheme
@@ -90,8 +81,7 @@ module Simplex.Messaging.Crypto
-- * DH derivation
dh',
dhSecret,
dhSecret',
dhSecretBytes',
-- * AES256 AEAD-GCM scheme
Key (..),
@@ -149,7 +139,6 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (bimap, first)
import qualified Data.ByteArray as BA
import Data.ByteString.Base64 (decode, encode)
import qualified Data.ByteString.Base64.URL as U
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Internal (c2w, w2c)
@@ -166,8 +155,8 @@ import Database.SQLite.Simple.ToField (ToField (..))
import GHC.TypeLits (ErrorMessage (..), TypeError)
import Network.Transport.Internal (decodeWord16, encodeWord16)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Parsers (base64P, base64UriP, blobFieldParser, parseAll, parseString)
import Simplex.Messaging.Util ((<$?>))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (blobFieldDecoder, parseAll, parseString)
type E2EEncryptionVersion = Word16
@@ -210,6 +199,11 @@ instance AlgorithmI X25519 where sAlgorithm = SX25519
instance AlgorithmI X448 where sAlgorithm = SX448
checkAlgorithm :: forall t a a'. (AlgorithmI a, AlgorithmI a') => t a' -> Either String (t a)
checkAlgorithm x = case testEquality (sAlgorithm @a) (sAlgorithm @a') of
Just Refl -> Right x
Nothing -> Left "bad algorithm"
instance TestEquality SAlgorithm where
testEquality SEd25519 SEd25519 = Just Refl
testEquality SEd448 SEd448 = Just Refl
@@ -267,39 +261,6 @@ deriving instance Show APrivateKey
type PrivateKeyX25519 = PrivateKey X25519
class AlgorithmPrefix k where
algorithmPrefix :: k -> ByteString
instance AlgorithmPrefix (SAlgorithm a) where
algorithmPrefix = \case
SEd25519 -> "ed25519"
SEd448 -> "ed448"
SX25519 -> "x25519"
SX448 -> "x448"
instance AlgorithmI a => AlgorithmPrefix (PublicKey a) where
algorithmPrefix _ = algorithmPrefix $ sAlgorithm @a
instance AlgorithmI a => AlgorithmPrefix (PrivateKey a) where
algorithmPrefix _ = algorithmPrefix $ sAlgorithm @a
instance AlgorithmPrefix APublicKey where
algorithmPrefix (APublicKey a _) = algorithmPrefix a
instance AlgorithmPrefix APrivateKey where
algorithmPrefix (APrivateKey a _) = algorithmPrefix a
prefixAlgorithm :: ByteString -> Either String Alg
prefixAlgorithm = \case
"ed25519" -> Right $ Alg SEd25519
"ed448" -> Right $ Alg SEd448
"x25519" -> Right $ Alg SX25519
"x448" -> Right $ Alg SX448
_ -> Left "unknown algorithm"
algP :: Parser Alg
algP = prefixAlgorithm <$?> A.takeTill (== ':')
type family SignatureAlgorithm (a :: Algorithm) :: Constraint where
SignatureAlgorithm Ed25519 = ()
SignatureAlgorithm Ed448 = ()
@@ -387,44 +348,29 @@ dhAlgorithm = \case
SX448 -> Just Dict
_ -> Nothing
class CryptoDhSecret s where
serializeDhSecret :: s -> ByteString
dhSecretBytes :: s -> ByteString
strDhSecretP :: Parser s
dhSecretP :: Parser s
dhSecretBytes' :: DhSecret a -> ByteString
dhSecretBytes' = \case
DhSecretX25519 s -> BA.convert s
DhSecretX448 s -> BA.convert s
instance AlgorithmI a => StrEncoding (DhSecret a) where
strEncode = strEncode . dhSecretBytes'
strDecode = (\(ADhSecret _ s) -> checkAlgorithm s) <=< strDecode
instance StrEncoding ADhSecret where
strEncode (ADhSecret _ s) = strEncode $ dhSecretBytes' s
strDecode = cryptoPassed . secret
where
secret bs
| B.length bs == x25519_size = ADhSecret SX25519 . DhSecretX25519 <$> X25519.dhSecret bs
| B.length bs == x448_size = ADhSecret SX448 . DhSecretX448 <$> X448.dhSecret bs
| otherwise = CE.CryptoFailed CE.CryptoError_SharedSecretSizeInvalid
cryptoPassed = \case
CE.CryptoPassed s -> Right s
CE.CryptoFailed e -> Left $ show e
instance AlgorithmI a => IsString (DhSecret a) where
fromString = parseString $ dhSecret >=> dhSecret'
instance CryptoDhSecret ADhSecret where
serializeDhSecret (ADhSecret _ s) = serializeDhSecret s
dhSecretBytes (ADhSecret _ s) = dhSecretBytes s
strDhSecretP = dhSecret <$?> base64P
dhSecretP = dhSecret <$?> A.takeByteString
dhSecret :: ByteString -> Either String ADhSecret
dhSecret = cryptoPassed . secret
where
secret bs
| B.length bs == x25519_size = ADhSecret SX25519 . DhSecretX25519 <$> X25519.dhSecret bs
| B.length bs == x448_size = ADhSecret SX448 . DhSecretX448 <$> X448.dhSecret bs
| otherwise = CE.CryptoFailed CE.CryptoError_SharedSecretSizeInvalid
cryptoPassed = \case
CE.CryptoPassed s -> Right s
CE.CryptoFailed e -> Left $ show e
instance forall a. AlgorithmI a => CryptoDhSecret (DhSecret a) where
serializeDhSecret = encode . dhSecretBytes
dhSecretBytes = \case
DhSecretX25519 s -> BA.convert s
DhSecretX448 s -> BA.convert s
strDhSecretP = dhSecret' <$?> strDhSecretP
dhSecretP = dhSecret' <$?> dhSecretP
dhSecret' :: forall a. AlgorithmI a => ADhSecret -> Either String (DhSecret a)
dhSecret' (ADhSecret a s) = case testEquality a $ sAlgorithm @a of
Just Refl -> Right s
_ -> Left "bad DH secret algorithm"
fromString = parseString strDecode
-- | Class for public key types
class CryptoPublicKey k where
@@ -449,76 +395,48 @@ instance CryptoPublicKey APublicDhKey where
instance AlgorithmI a => CryptoPublicKey (PublicKey a) where
toPubKey = id
pubKey (APublicKey a k) = case testEquality a $ sAlgorithm @a of
Just Refl -> Right k
_ -> Left "bad key algorithm"
pubKey (APublicKey _ k) = checkAlgorithm k
instance Encoding APublicVerifyKey where
smpEncode k = smpEncode $ encodePubKey k
smpP = parseAll binaryPubKeyP <$?> smpP
smpEncode = smpEncode . encodePubKey
{-# INLINE smpEncode #-}
smpDecode = decodePubKey
{-# INLINE smpDecode #-}
instance Encoding APublicDhKey where
smpEncode k = smpEncode $ encodePubKey k
smpP = parseAll binaryPubKeyP <$?> smpP
smpEncode = smpEncode . encodePubKey
{-# INLINE smpEncode #-}
smpDecode = decodePubKey
{-# INLINE smpDecode #-}
instance AlgorithmI a => Encoding (PublicKey a) where
smpEncode k = smpEncode $ encodePubKey' k
smpP = parseAll binaryPubKeyP <$?> smpP
smpEncode = smpEncode . encodePubKey
{-# INLINE smpEncode #-}
smpDecode = decodePubKey
{-# INLINE smpDecode #-}
-- | base64 X509 key encoding with algorithm prefix
serializePubKey :: CryptoPublicKey k => k -> ByteString
serializePubKey = toPubKey serializePubKey'
{-# INLINE serializePubKey #-}
instance StrEncoding APublicVerifyKey where
strEncode = strEncode . encodePubKey
{-# INLINE strEncode #-}
strDecode = decodePubKey
{-# INLINE strDecode #-}
-- | base64url X509 key encoding with algorithm prefix
serializePubKeyUri :: CryptoPublicKey k => k -> ByteString
serializePubKeyUri = toPubKey serializePubKeyUri'
{-# INLINE serializePubKeyUri #-}
instance StrEncoding APublicDhKey where
strEncode = strEncode . encodePubKey
{-# INLINE strEncode #-}
strDecode = decodePubKey
{-# INLINE strDecode #-}
serializePubKey' :: AlgorithmI a => PublicKey a -> ByteString
serializePubKey' k = algorithmPrefix k <> ":" <> encode (encodePubKey' k)
serializePubKeyUri' :: AlgorithmI a => PublicKey a -> ByteString
serializePubKeyUri' k = algorithmPrefix k <> ":" <> U.encode (encodePubKey' k)
-- | base64 X509 (with algorithm prefix) key parser
strPubKeyP :: CryptoPublicKey k => Parser k
strPubKeyP = pubKey <$?> aStrPubKeyP
{-# INLINE strPubKeyP #-}
-- | base64url X509 (with algorithm prefix) key parser
strPubKeyUriP :: CryptoPublicKey k => Parser k
strPubKeyUriP = pubKey <$?> aStrPubKeyUriP
{-# INLINE strPubKeyUriP #-}
aStrPubKeyP :: Parser APublicKey
aStrPubKeyP = strPublicKeyP_ base64P
aStrPubKeyUriP :: Parser APublicKey
aStrPubKeyUriP = strPublicKeyP_ base64UriP
strPublicKeyP_ :: Parser ByteString -> Parser APublicKey
strPublicKeyP_ b64P = do
Alg a <- algP <* A.char ':'
k@(APublicKey a' _) <- decodePubKey <$?> b64P
case testEquality a a' of
Just Refl -> pure k
_ -> fail $ "public key algorithm " <> show a <> " does not match prefix"
instance AlgorithmI a => StrEncoding (PublicKey a) where
strEncode = strEncode . encodePubKey
{-# INLINE strEncode #-}
strDecode = decodePubKey
{-# INLINE strDecode #-}
encodePubKey :: CryptoPublicKey pk => pk -> ByteString
encodePubKey = toPubKey encodePubKey'
encodePubKey = toPubKey $ encodeASNObj . publicToX509
{-# INLINE encodePubKey #-}
encodePubKey' :: PublicKey a -> ByteString
encodePubKey' = encodeASNObj . publicToX509
binaryPubKeyP :: CryptoPublicKey pk => Parser pk
binaryPubKeyP = pubKey <$?> aBinaryPubKeyP
{-# INLINE binaryPubKeyP #-}
aBinaryPubKeyP :: Parser APublicKey
aBinaryPubKeyP = decodePubKey <$?> A.takeByteString
class CryptoPrivateKey pk where
toPrivKey :: (forall a. AlgorithmI a => PrivateKey a -> b) -> pk -> b
privKey :: APrivateKey -> Either String pk
@@ -541,27 +459,16 @@ instance CryptoPrivateKey APrivateDhKey where
instance AlgorithmI a => CryptoPrivateKey (PrivateKey a) where
toPrivKey = id
privKey (APrivateKey a k) = case testEquality a $ sAlgorithm @a of
Just Refl -> Right k
_ -> Left "bad key algorithm"
privKey (APrivateKey _ k) = checkAlgorithm k
encodePrivKey :: CryptoPrivateKey pk => pk -> ByteString
encodePrivKey = toPrivKey encodePrivKey'
encodePrivKey' :: PrivateKey a -> ByteString
encodePrivKey' = encodeASNObj . privateToX509
binaryPrivKeyP :: CryptoPrivateKey pk => Parser pk
binaryPrivKeyP = privKey <$?> aBinaryPrivKeyP
aBinaryPrivKeyP :: Parser APrivateKey
aBinaryPrivKeyP = decodePrivKey <$?> A.takeByteString
encodePrivKey = toPrivKey $ encodeASNObj . privateToX509
instance AlgorithmI a => IsString (PrivateKey a) where
fromString = parseString $ decode >=> decodePrivKey >=> privKey
fromString = parseString $ decode >=> decodePrivKey
instance AlgorithmI a => IsString (PublicKey a) where
fromString = parseString $ decode >=> decodePubKey >=> pubKey
fromString = parseString $ decode >=> decodePubKey
-- | Tuple of RSA 'PublicKey' and 'PrivateKey'.
type KeyPair a = (PublicKey a, PrivateKey a)
@@ -608,25 +515,25 @@ instance ToField APrivateDhKey where toField = toField . encodePrivKey
instance ToField APublicDhKey where toField = toField . encodePubKey
instance ToField (PrivateKey a) where toField = toField . encodePrivKey'
instance AlgorithmI a => ToField (PrivateKey a) where toField = toField . encodePrivKey
instance ToField (PublicKey a) where toField = toField . encodePubKey'
instance AlgorithmI a => ToField (PublicKey a) where toField = toField . encodePubKey
instance AlgorithmI a => ToField (DhSecret a) where toField = toField . dhSecretBytes
instance AlgorithmI a => ToField (DhSecret a) where toField = toField . dhSecretBytes'
instance FromField APrivateSignKey where fromField = blobFieldParser binaryPrivKeyP
instance FromField APrivateSignKey where fromField = blobFieldDecoder decodePrivKey
instance FromField APublicVerifyKey where fromField = blobFieldParser binaryPubKeyP
instance FromField APublicVerifyKey where fromField = blobFieldDecoder decodePubKey
instance FromField APrivateDhKey where fromField = blobFieldParser binaryPrivKeyP
instance FromField APrivateDhKey where fromField = blobFieldDecoder decodePrivKey
instance FromField APublicDhKey where fromField = blobFieldParser binaryPubKeyP
instance FromField APublicDhKey where fromField = blobFieldDecoder decodePubKey
instance (Typeable a, AlgorithmI a) => FromField (PrivateKey a) where fromField = blobFieldParser binaryPrivKeyP
instance (Typeable a, AlgorithmI a) => FromField (PrivateKey a) where fromField = blobFieldDecoder decodePrivKey
instance (Typeable a, AlgorithmI a) => FromField (PublicKey a) where fromField = blobFieldParser binaryPubKeyP
instance (Typeable a, AlgorithmI a) => FromField (PublicKey a) where fromField = blobFieldDecoder decodePubKey
instance (Typeable a, AlgorithmI a) => FromField (DhSecret a) where fromField = blobFieldParser dhSecretP
instance (Typeable a, AlgorithmI a) => FromField (DhSecret a) where fromField = blobFieldDecoder strDecode
instance IsString (Maybe ASignature) where
fromString = parseString $ decode >=> decodeSignature
@@ -679,10 +586,8 @@ instance AlgorithmI a => CryptoSignature (Signature a) where
SignatureEd25519 s -> BA.convert s
SignatureEd448 s -> BA.convert s
decodeSignature s = do
ASignature a sig <- decodeSignature s
case testEquality a $ sAlgorithm @a of
Just Refl -> Right sig
_ -> Left "bad signature algorithm"
ASignature _ sig <- decodeSignature s
checkAlgorithm sig
class SignatureSize s where signatureSize :: s -> Int
@@ -760,12 +665,20 @@ newtype IV = IV {unIV :: ByteString}
-- Previously was used for server's public key hash in ad-hoc transport scheme, kept as is for compatibility.
newtype KeyHash = KeyHash {unKeyHash :: ByteString} deriving (Eq, Ord, Show)
instance Encoding KeyHash where
smpEncode = smpEncode . unKeyHash
smpP = KeyHash <$> smpP
instance StrEncoding KeyHash where
strEncode = strEncode . unKeyHash
strP = KeyHash <$> strP
instance IsString KeyHash where
fromString = parseString . parseAll $ KeyHash <$> base64P
fromString = parseString $ parseAll strP
instance ToField KeyHash where toField = toField . encode . unKeyHash
instance ToField KeyHash where toField = toField . strEncode
instance FromField KeyHash where fromField = blobFieldParser $ KeyHash <$> base64P
instance FromField KeyHash where fromField = blobFieldDecoder $ parseAll strP
-- | SHA256 digest.
sha256Hash :: ByteString -> ByteString
@@ -945,25 +858,29 @@ privateToX509 = \case
encodeASNObj :: ASN1Object a => a -> ByteString
encodeASNObj k = toStrict . encodeASN1 DER $ toASN1 k []
-- Decoding of binary X509 'PublicKey'.
decodePubKey :: ByteString -> Either String APublicKey
decodePubKey =
decodeKey >=> \case
(PubKeyEd25519 k, []) -> Right . APublicKey SEd25519 $ PublicKeyEd25519 k
(PubKeyEd448 k, []) -> Right . APublicKey SEd448 $ PublicKeyEd448 k
(PubKeyX25519 k, []) -> Right . APublicKey SX25519 $ PublicKeyX25519 k
(PubKeyX448 k, []) -> Right . APublicKey SX448 $ PublicKeyX448 k
r -> keyError r
-- Decoding of binary X509 'CryptoPublicKey'.
decodePubKey :: CryptoPublicKey k => ByteString -> Either String k
decodePubKey = decodeKey >=> x509ToPublic >=> pubKey
-- Decoding of binary PKCS8 'PrivateKey'.
decodePrivKey :: ByteString -> Either String APrivateKey
decodePrivKey =
decodeKey >=> \case
(PrivKeyEd25519 k, []) -> Right . APrivateKey SEd25519 . PrivateKeyEd25519 k $ Ed25519.toPublic k
(PrivKeyEd448 k, []) -> Right . APrivateKey SEd448 . PrivateKeyEd448 k $ Ed448.toPublic k
(PrivKeyX25519 k, []) -> Right . APrivateKey SX25519 $ PrivateKeyX25519 k
(PrivKeyX448 k, []) -> Right . APrivateKey SX448 $ PrivateKeyX448 k
r -> keyError r
decodePrivKey :: CryptoPrivateKey k => ByteString -> Either String k
decodePrivKey = decodeKey >=> x509ToPrivate >=> privKey
x509ToPublic :: (PubKey, [ASN1]) -> Either String APublicKey
x509ToPublic = \case
(PubKeyEd25519 k, []) -> Right . APublicKey SEd25519 $ PublicKeyEd25519 k
(PubKeyEd448 k, []) -> Right . APublicKey SEd448 $ PublicKeyEd448 k
(PubKeyX25519 k, []) -> Right . APublicKey SX25519 $ PublicKeyX25519 k
(PubKeyX448 k, []) -> Right . APublicKey SX448 $ PublicKeyX448 k
r -> keyError r
x509ToPrivate :: (PrivKey, [ASN1]) -> Either String APrivateKey
x509ToPrivate = \case
(PrivKeyEd25519 k, []) -> Right . APrivateKey SEd25519 . PrivateKeyEd25519 k $ Ed25519.toPublic k
(PrivKeyEd448 k, []) -> Right . APrivateKey SEd448 . PrivateKeyEd448 k $ Ed448.toPublic k
(PrivKeyX25519 k, []) -> Right . APrivateKey SX25519 $ PrivateKeyX25519 k
(PrivKeyX448 k, []) -> Right . APrivateKey SX448 $ PrivateKeyX448 k
r -> keyError r
decodeKey :: ASN1Object a => ByteString -> Either String (a, [ASN1])
decodeKey = fromASN1 <=< first show . decodeASN1 DER . fromStrict
+2 -2
View File
@@ -355,13 +355,13 @@ rcDecrypt' rc@Ratchet {rcRcv, rcMKSkipped, rcAD} msg' = do
initKdf :: (AlgorithmI a, DhAlgorithm a) => ByteString -> PublicKey a -> PrivateKey a -> (RatchetKey, Key, Key)
initKdf salt k pk =
let dhOut = dhSecretBytes $ dh' k pk
let dhOut = dhSecretBytes' $ dh' k pk
(sk, hk, nhk) = hkdf3 salt dhOut "SimpleXInitRatchet"
in (RatchetKey sk, Key hk, Key nhk)
rootKdf :: (AlgorithmI a, DhAlgorithm a) => RatchetKey -> PublicKey a -> PrivateKey a -> (RatchetKey, RatchetKey, Key)
rootKdf (RatchetKey rk) k pk =
let dhOut = dhSecretBytes $ dh' k pk
let dhOut = dhSecretBytes' $ dh' k pk
(rk', ck, nhk) = hkdf3 rk dhOut "SimpleXRootRatchet"
in (RatchetKey rk', RatchetKey ck, Key nhk)
+13
View File
@@ -18,10 +18,23 @@ import Data.Int (Int64)
import Data.Time.Clock.System (SystemTime (..))
import Data.Word (Word16, Word32)
import Network.Transport.Internal (decodeWord16, decodeWord32, encodeWord16, encodeWord32)
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util ((<$?>))
-- | SMP protocol encoding
class Encoding a where
{-# MINIMAL smpEncode, (smpDecode | smpP) #-}
-- | protocol encoding of type (default implementation uses protocol ByteString encoding)
smpEncode :: a -> ByteString
-- | decoding of type (default implementation uses parser)
smpDecode :: ByteString -> Either String a
smpDecode = parseAll smpP
-- | protocol parser of type (default implementation parses protocol ByteString encoding)
smpP :: Parser a
smpP = smpDecode <$?> smpP
instance Encoding Char where
smpEncode = B.singleton
+85
View File
@@ -0,0 +1,85 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Messaging.Encoding.String
( StrEncoding (..),
Str (..),
strP_,
)
where
import Control.Applicative (optional)
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Base64.URL as U
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isAlphaNum)
import qualified Data.List.NonEmpty as L
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util ((<$?>))
-- | Serializing human-readable and (where possible) URI-friendly strings for SMP and SMP agent protocols
class StrEncoding a where
{-# MINIMAL strEncode, (strDecode | strP) #-}
strEncode :: a -> ByteString
-- Please note - if you only specify strDecode, it will use base64urlP as default parser before decoding the string
strDecode :: ByteString -> Either String a
strDecode = parseAll strP
strP :: Parser a
strP = strDecode <$?> base64urlP
-- base64url encoding/decoding of ByteStrings - the parser only allows non-empty strings
instance StrEncoding ByteString where
strEncode = U.encode
strP = base64urlP
base64urlP :: Parser ByteString
base64urlP = do
str <- A.takeWhile1 (\c -> isAlphaNum c || c == '-' || c == '_')
pad <- A.takeWhile (== '=')
either fail pure $ U.decode (str <> pad)
newtype Str = Str {unStr :: ByteString}
instance StrEncoding Str where
strEncode = unStr
strP = Str <$> A.takeTill (== ' ') <* optional A.space
instance StrEncoding a => StrEncoding (Maybe a) where
strEncode = maybe "" strEncode
strP = optional strP
-- lists encode/parse as comma-separated strings
instance StrEncoding a => StrEncoding [a] where
strEncode = B.intercalate "," . map strEncode
strP = listItem `A.sepBy'` A.char ','
instance StrEncoding a => StrEncoding (L.NonEmpty a) where
strEncode = strEncode . L.toList
strP =
maybe (fail "empty list") pure . L.nonEmpty
=<< listItem `A.sepBy1'` A.char ','
listItem :: StrEncoding a => Parser a
listItem = strDecode <$?> A.takeTill (== ',')
instance (StrEncoding a, StrEncoding b) => StrEncoding (a, b) where
strEncode (a, b) = B.unwords [strEncode a, strEncode b]
strP = (,) <$> strP_ <*> strP
instance (StrEncoding a, StrEncoding b, StrEncoding c) => StrEncoding (a, b, c) where
strEncode (a, b, c) = B.unwords [strEncode a, strEncode b, strEncode c]
strP = (,,) <$> strP_ <*> strP_ <*> strP
instance (StrEncoding a, StrEncoding b, StrEncoding c, StrEncoding d) => StrEncoding (a, b, c, d) where
strEncode (a, b, c, d) = B.unwords [strEncode a, strEncode b, strEncode c, strEncode d]
strP = (,,,) <$> strP_ <*> strP_ <*> strP_ <*> strP
instance (StrEncoding a, StrEncoding b, StrEncoding c, StrEncoding d, StrEncoding e) => StrEncoding (a, b, c, d, e) where
strEncode (a, b, c, d, e) = B.unwords [strEncode a, strEncode b, strEncode c, strEncode d, strEncode e]
strP = (,,,,) <$> strP_ <*> strP_ <*> strP_ <*> strP_ <*> strP
strP_ :: StrEncoding a => Parser a
strP_ = strP <* A.space
+8 -15
View File
@@ -8,7 +8,6 @@ import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (first)
import Data.ByteString.Base64
import qualified Data.ByteString.Base64.URL as U
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isAlphaNum)
@@ -23,16 +22,7 @@ import Simplex.Messaging.Util ((<$?>))
import Text.Read (readMaybe)
base64P :: Parser ByteString
base64P = decode <$?> base64StringP
base64StringP :: Parser ByteString
base64StringP = paddedBase64 rawBase64P
base64UriP :: Parser ByteString
base64UriP = U.decode <$?> base64UriStringP
base64UriStringP :: Parser ByteString
base64UriStringP = paddedBase64 rawBase64UriP
base64P = decode <$?> paddedBase64 rawBase64P
paddedBase64 :: Parser ByteString -> Parser ByteString
paddedBase64 raw = (<>) <$> raw <*> pad
@@ -42,8 +32,8 @@ paddedBase64 raw = (<>) <$> raw <*> pad
rawBase64P :: Parser ByteString
rawBase64P = A.takeWhile1 (\c -> isAlphaNum c || c == '+' || c == '/')
rawBase64UriP :: Parser ByteString
rawBase64UriP = A.takeWhile1 (\c -> isAlphaNum c || c == '-' || c == '_')
-- rawBase64UriP :: Parser ByteString
-- rawBase64UriP = A.takeWhile1 (\c -> isAlphaNum c || c == '-' || c == '_')
tsISO8601P :: Parser UTCTime
tsISO8601P = maybe (fail "timestamp") pure . parseISO8601 . B.unpack =<< A.takeTill wordEnd
@@ -79,9 +69,12 @@ parseString :: (ByteString -> Either String a) -> (String -> a)
parseString p = either error id . p . B.pack
blobFieldParser :: Typeable k => Parser k -> FieldParser k
blobFieldParser p = \case
blobFieldParser = blobFieldDecoder . parseAll
blobFieldDecoder :: Typeable k => (ByteString -> Either String k) -> FieldParser k
blobFieldDecoder dec = \case
f@(Field (SQLBlob b) _) ->
case parseAll p b of
case dec b of
Right k -> Ok k
Left e -> returnError ConversionFailed f ("couldn't parse field: " ++ e)
f -> returnError ConversionFailed f "expecting SQLBlob column type"
+35 -1
View File
@@ -53,6 +53,8 @@ module Simplex.Messaging.Protocol
PubHeader (..),
ClientMessage (..),
PrivHeader (..),
SMPServer (..),
SrvLoc (..),
CorrId (..),
QueueId,
RecipientId,
@@ -98,8 +100,10 @@ import Data.Type.Equality
import Data.Word (Word16)
import GHC.Generics (Generic)
import Generic.Random (genericArbitraryU)
import Network.Socket (HostName, ServiceName)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers
import Simplex.Messaging.Transport (THandle (..), Transport, TransportError (..), tGetBlock, tPutBlock)
import Simplex.Messaging.Util ((<$?>))
@@ -330,7 +334,7 @@ instance Encoding PubHeader where
instance Encoding EncMessage where
smpEncode EncMessage {emHeader, emNonce, emBody} =
smpEncode emHeader <> smpEncode emNonce <> emBody
smpEncode (emHeader, emNonce, Tail emBody)
smpP = do
emHeader <- smpP
emNonce <- smpP
@@ -357,6 +361,36 @@ instance Encoding ClientMessage where
smpEncode (ClientMessage h msg) = smpEncode h <> msg
smpP = ClientMessage <$> smpP <*> A.takeByteString
-- | SMP server location and transport key digest (hash).
data SMPServer = SMPServer
{ host :: HostName,
port :: Maybe ServiceName,
keyHash :: C.KeyHash
}
deriving (Eq, Ord, Show)
instance IsString SMPServer where
fromString = parseString strDecode
instance StrEncoding SMPServer where
strEncode SMPServer {host, port, keyHash} =
"smp://" <> strEncode keyHash <> "@" <> strEncode (SrvLoc host port)
strP = do
_ <- "smp://"
keyHash <- strP <* A.char '@'
SrvLoc host port <- strP
pure SMPServer {host, port, keyHash}
data SrvLoc = SrvLoc HostName (Maybe ServiceName)
deriving (Eq, Ord, Show)
instance StrEncoding SrvLoc where
strEncode (SrvLoc host port) = B.pack $ host <> maybe "" (':' :) port
strP = SrvLoc <$> host <*> optional port
where
host = B.unpack <$> A.takeWhile1 (A.notInClass ":#,;/ ")
port = B.unpack <$> (A.char ':' *> A.takeWhile1 A.isDigit)
-- | Transmission correlation ID.
newtype CorrId = CorrId {bs :: ByteString} deriving (Eq, Ord, Show)
+6 -5
View File
@@ -117,11 +117,12 @@ runSMPServerBlocking started cfg@ServerConfig {transports} = do
writeTBQueue (sndQ c) (CorrId "", qId, END)
atomically . stateTVar (clientSubs c) $ \ss -> (M.lookup qId ss, M.delete qId ss)
runClient :: (Transport c, MonadUnliftIO m, MonadReader Env m) => TProxy c -> c -> m ()
runClient _ h =
liftIO (runExceptT $ serverHandshake h) >>= \case
Right th -> runClientTransport th
Left _ -> pure ()
runClient :: (Transport c, MonadUnliftIO m, MonadReader Env m) => TProxy c -> c -> m ()
runClient _ h = do
kh <- asks serverIdentity
liftIO (runExceptT $ serverHandshake h kh) >>= \case
Right th -> runClientTransport th
Left _ -> pure ()
runClientTransport :: (Transport c, MonadUnliftIO m, MonadReader Env m) => THandle c -> m ()
runClientTransport th@THandle {sessionId} = do
+7 -2
View File
@@ -11,15 +11,17 @@ import Crypto.Random
import Data.ByteString.Char8 (ByteString)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.X509.Validation (Fingerprint (..))
import Network.Socket (ServiceName)
import qualified Network.TLS as T
import Numeric.Natural
import Simplex.Messaging.Crypto (KeyHash (..))
import Simplex.Messaging.Protocol
import Simplex.Messaging.Server.MsgStore.STM
import Simplex.Messaging.Server.QueueStore (QueueRec (..))
import Simplex.Messaging.Server.QueueStore.STM
import Simplex.Messaging.Server.StoreLog
import Simplex.Messaging.Transport (ATransport, loadTLSServerParams)
import Simplex.Messaging.Transport (ATransport, loadFingerprint, loadTLSServerParams)
import System.IO (IOMode (..))
import UnliftIO.STM
@@ -40,6 +42,7 @@ data ServerConfig = ServerConfig
data Env = Env
{ config :: ServerConfig,
server :: Server,
serverIdentity :: KeyHash,
queueStore :: QueueStore,
msgStore :: STMMsgStore,
idsDrg :: TVar ChaChaDRG,
@@ -100,7 +103,9 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile}
idsDrg <- drgNew >>= newTVarIO
s' <- restoreQueues queueStore `mapM` storeLog (config :: ServerConfig)
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
return Env {config, server, queueStore, msgStore, idsDrg, storeLog = s', tlsServerParams}
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
let serverIdentity = KeyHash fp
return Env {config, server, serverIdentity, queueStore, msgStore, idsDrg, storeLog = s', tlsServerParams}
where
restoreQueues :: QueueStore -> StoreLog 'ReadMode -> m (StoreLog 'WriteMode)
restoreQueues queueStore s = do
+37 -44
View File
@@ -5,6 +5,7 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Simplex.Messaging.Server.StoreLog
( StoreLog, -- constructors are not exported
@@ -22,10 +23,7 @@ where
import Control.Applicative (optional, (<|>))
import Control.Monad (unless)
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (first, second)
import Data.ByteString.Base64 (encode)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
@@ -34,8 +32,7 @@ import Data.Functor (($>))
import Data.List (foldl')
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Parsers (base64P, parseAll)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
import Simplex.Messaging.Server.QueueStore (QueueRec (..), QueueStatus (..))
import Simplex.Messaging.Transport (trimCR)
@@ -54,44 +51,40 @@ data StoreLogRecord
| AddNotifier QueueId NotifierId NtfPublicVerifyKey
| DeleteQueue QueueId
storeLogRecordP :: Parser StoreLogRecord
storeLogRecordP =
"CREATE " *> createQueueP
<|> "SECURE " *> secureQueueP
<|> "NOTIFIER " *> addNotifierP
<|> "DELETE " *> (DeleteQueue <$> base64P)
where
createQueueP = CreateQueue <$> queueRecP
secureQueueP = SecureQueue <$> base64P <* A.space <*> C.strPubKeyP
addNotifierP =
AddNotifier <$> base64P <* A.space <*> base64P <* A.space <*> C.strPubKeyP
queueRecP = do
recipientId <- "rid=" *> base64P
recipientKey <- " rk=" *> C.strPubKeyP
rcvDhSecret <- " rdh=" *> C.strDhSecretP
senderId <- " sid=" *> base64P
senderKey <- " sk=" *> optional C.strPubKeyP
notifier <- optional $ (,) <$> (" nid=" *> base64P) <*> (" nk=" *> C.strPubKeyP)
pure QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, notifier, status = QueueActive}
instance StrEncoding QueueRec where
strEncode QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, notifier} =
B.unwords
[ "rid=" <> strEncode recipientId,
"rk=" <> strEncode recipientKey,
"rdh=" <> strEncode rcvDhSecret,
"sid=" <> strEncode senderId,
"sk=" <> strEncode senderKey
]
<> maybe "" notifierStr notifier
where
notifierStr (nId, nKey) = " nid=" <> strEncode nId <> " nk=" <> strEncode nKey
serializeStoreLogRecord :: StoreLogRecord -> ByteString
serializeStoreLogRecord = \case
CreateQueue q -> "CREATE " <> serializeQueue q
SecureQueue rId sKey -> "SECURE " <> encode rId <> " " <> C.serializePubKey sKey
AddNotifier rId nId nKey -> B.unwords ["NOTIFIER", encode rId, encode nId, C.serializePubKey nKey]
DeleteQueue rId -> "DELETE " <> encode rId
where
serializeQueue
QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, notifier} =
B.unwords
[ "rid=" <> encode recipientId,
"rk=" <> C.serializePubKey recipientKey,
"rdh=" <> C.serializeDhSecret rcvDhSecret,
"sid=" <> encode senderId,
"sk=" <> maybe "" C.serializePubKey senderKey
]
<> maybe "" serializeNotifier notifier
serializeNotifier (nId, nKey) = " nid=" <> encode nId <> " nk=" <> C.serializePubKey nKey
strP = do
recipientId <- "rid=" *> strP_
recipientKey <- "rk=" *> strP_
rcvDhSecret <- "rdh=" *> strP_
senderId <- "sid=" *> strP_
senderKey <- "sk=" *> strP
notifier <- optional $ (,) <$> (" nid=" *> strP_) <*> ("nk=" *> strP)
pure QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, notifier, status = QueueActive}
instance StrEncoding StoreLogRecord where
strEncode = \case
CreateQueue q -> strEncode (Str "CREATE", q)
SecureQueue rId sKey -> strEncode (Str "SECURE", rId, sKey)
AddNotifier rId nId nKey -> strEncode (Str "NOTIFIER", rId, nId, nKey)
DeleteQueue rId -> strEncode (Str "DELETE", rId)
strP =
"CREATE " *> (CreateQueue <$> strP)
<|> "SECURE " *> (SecureQueue <$> strP_ <*> strP)
<|> "NOTIFIER " *> (AddNotifier <$> strP_ <*> strP_ <*> strP)
<|> "DELETE " *> (DeleteQueue <$> strP)
openWriteStoreLog :: FilePath -> IO (StoreLog 'WriteMode)
openWriteStoreLog f = WriteStoreLog f <$> openFile f WriteMode
@@ -113,7 +106,7 @@ closeStoreLog = \case
writeStoreLogRecord :: StoreLog 'WriteMode -> StoreLogRecord -> IO ()
writeStoreLogRecord (WriteStoreLog _ h) r = do
B.hPutStrLn h $ serializeStoreLogRecord r
B.hPutStrLn h $ strEncode r
hFlush h
logCreateQueue :: StoreLog 'WriteMode -> QueueRec -> IO ()
@@ -151,7 +144,7 @@ readQueues (ReadStoreLog _ h) = LB.hGetContents h >>= returnResult . procStoreLo
returnResult :: ([LogParsingError], Map RecipientId QueueRec) -> IO (Map RecipientId QueueRec)
returnResult (errs, res) = mapM_ printError errs $> res
parseLogRecord :: LB.ByteString -> Either LogParsingError StoreLogRecord
parseLogRecord = (\s -> first (,s) $ parseAll storeLogRecordP s) . trimCR . LB.toStrict
parseLogRecord = (\s -> first (,s) $ strDecode s) . trimCR . LB.toStrict
procLogRecord :: Map RecipientId QueueRec -> StoreLogRecord -> Map RecipientId QueueRec
procLogRecord m = \case
CreateQueue q -> M.insert (recipientId q) q m
+35 -31
View File
@@ -41,7 +41,6 @@ module Simplex.Messaging.Transport
runTransportClient,
loadTLSServerParams,
loadFingerprint,
encodeFingerprint,
-- * TLS 1.2 Transport
TLS (..),
@@ -71,7 +70,6 @@ import qualified Crypto.Store.X509 as SX
import Data.Attoparsec.ByteString.Char8 (Parser)
import Data.Bifunctor (first)
import Data.Bitraversable (bimapM)
import Data.ByteString.Base64
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy as BL
@@ -82,6 +80,7 @@ import qualified Data.Set as S
import Data.Word (Word16)
import qualified Data.X509 as X
import qualified Data.X509.CertificateStore as XS
import Data.X509.Validation (Fingerprint (..))
import qualified Data.X509.Validation as XV
import GHC.Generics (Generic)
import GHC.IO.Exception (IOErrorType (..))
@@ -201,7 +200,7 @@ startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted
setStarted sock = atomically (tryPutTMVar started True) >> pure sock
-- | Connect to passed TCP host:port and pass handle to the client.
runTransportClient :: Transport c => MonadUnliftIO m => HostName -> ServiceName -> Maybe C.KeyHash -> (c -> m a) -> m a
runTransportClient :: Transport c => MonadUnliftIO m => HostName -> ServiceName -> C.KeyHash -> (c -> m a) -> m a
runTransportClient host port keyHash client = do
let clientParams = mkTLSClientParams host port keyHash
c <- liftIO $ startTCPClient host port clientParams
@@ -248,14 +247,11 @@ loadTLSServerParams caCertificateFile certificateFile privateKeyFile =
T.serverSupported = supportedParameters
}
loadFingerprint :: FilePath -> IO XV.Fingerprint
loadFingerprint :: FilePath -> IO Fingerprint
loadFingerprint certificateFile = do
(cert : _) <- SX.readSignedObject certificateFile
pure $ XV.getFingerprint (cert :: X.SignedExact X.Certificate) X.HashSHA256
encodeFingerprint :: XV.Fingerprint -> ByteString
encodeFingerprint (XV.Fingerprint bs) = encode bs
-- * TLS 1.2 Transport
data TLS = TLS
@@ -294,7 +290,7 @@ closeTLS ctx =
(T.bye ctx >> T.contextClose ctx) -- sometimes socket was closed before 'TLS.bye'
`E.catch` (\(_ :: E.SomeException) -> pure ()) -- so we catch the 'Broken pipe' error here
mkTLSClientParams :: HostName -> ServiceName -> Maybe C.KeyHash -> T.ClientParams
mkTLSClientParams :: HostName -> ServiceName -> C.KeyHash -> T.ClientParams
mkTLSClientParams host port keyHash = do
let p = B.pack port
(T.defaultParamsClient host p)
@@ -303,16 +299,14 @@ mkTLSClientParams host port keyHash = do
T.clientSupported = supportedParameters
}
validateCertificateChain :: Maybe C.KeyHash -> HostName -> ByteString -> X.CertificateChain -> IO [XV.FailedReason]
validateCertificateChain :: C.KeyHash -> HostName -> ByteString -> X.CertificateChain -> IO [XV.FailedReason]
validateCertificateChain _ _ _ (X.CertificateChain []) = pure [XV.EmptyChain]
validateCertificateChain _ _ _ (X.CertificateChain [_]) = pure [XV.EmptyChain]
validateCertificateChain keyHash host port cc@(X.CertificateChain sc@[_, caCert]) =
let fp = XV.getFingerprint caCert X.HashSHA256
in if maybe True (sameFingerprint fp) keyHash
then x509validate
else pure [XV.UnknownCA]
validateCertificateChain (C.KeyHash kh) host port cc@(X.CertificateChain sc@[_, caCert]) =
if Fingerprint kh == XV.getFingerprint caCert X.HashSHA256
then x509validate
else pure [XV.UnknownCA]
where
sameFingerprint (XV.Fingerprint s) (C.KeyHash s') = s == s'
x509validate :: IO [XV.FailedReason]
x509validate = XV.validate X.HashSHA256 hooks checks certStore cache serviceID cc
where
@@ -402,14 +396,19 @@ data ServerHandshake = ServerHandshake
sessionId :: ByteString
}
newtype ClientHandshake = ClientHandshake
data ClientHandshake = ClientHandshake
{ -- | agreed SMP server protocol version
smpVersion :: Word16
smpVersion :: Word16,
-- | server identity - CA certificate fingerprint
keyHash :: C.KeyHash
}
instance Encoding ClientHandshake where
smpEncode ClientHandshake {smpVersion} = smpEncode smpVersion
smpP = ClientHandshake <$> smpP
smpEncode ClientHandshake {smpVersion, keyHash} = smpEncode (smpVersion, keyHash)
smpP = do
smpVersion <- smpP
keyHash <- smpP
pure ClientHandshake {smpVersion, keyHash}
instance Encoding ServerHandshake where
smpEncode ServerHandshake {smpVersionRange, sessionId} =
@@ -434,6 +433,8 @@ data HandshakeError
PARSE
| -- | incompatible peer version
VERSION
| -- | incorrect server identity
IDENTITY
deriving (Eq, Generic, Read, Show, Exception)
instance Arbitrary TransportError where arbitrary = genericArbitraryU
@@ -472,29 +473,32 @@ tGetBlock THandle {connection = c} =
-- | Server SMP transport handshake.
--
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
serverHandshake :: forall c. Transport c => c -> ExceptT TransportError IO (THandle c)
serverHandshake c = do
serverHandshake :: forall c. Transport c => c -> C.KeyHash -> ExceptT TransportError IO (THandle c)
serverHandshake c kh = do
let th@THandle {sessionId} = tHandle c
sendHandshake th $ ServerHandshake {sessionId, smpVersionRange = supportedSMPVersions}
ClientHandshake smpVersion <- getHandshake th
if smpVersion `isCompatible` supportedSMPVersions
then pure (th :: THandle c) {smpVersion}
else throwE $ TEHandshake VERSION
getHandshake th >>= \case
ClientHandshake {smpVersion, keyHash}
| keyHash /= kh ->
throwE $ TEHandshake IDENTITY
| smpVersion `isCompatible` supportedSMPVersions -> do
pure (th :: THandle c) {smpVersion}
| otherwise -> throwE $ TEHandshake VERSION
-- | Client SMP transport handshake.
--
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
clientHandshake :: forall c. Transport c => c -> ExceptT TransportError IO (THandle c)
clientHandshake c = do
clientHandshake :: forall c. Transport c => c -> C.KeyHash -> ExceptT TransportError IO (THandle c)
clientHandshake c keyHash = do
let th@THandle {sessionId} = tHandle c
ServerHandshake {sessionId = sessId, smpVersionRange} <- getHandshake th
if sessionId == sessId
then case smpVersionRange `compatibleVersion` supportedSMPVersions of
if sessionId /= sessId
then throwE TEBadSession
else case smpVersionRange `compatibleVersion` supportedSMPVersions of
Just smpVersion -> do
sendHandshake th $ ClientHandshake smpVersion
sendHandshake th $ ClientHandshake {smpVersion, keyHash}
pure (th :: THandle c) {smpVersion}
Nothing -> throwE $ TEHandshake VERSION
else throwE TEBadSession
sendHandshake :: (Transport c, Encoding smp) => THandle c -> smp -> ExceptT TransportError IO ()
sendHandshake th = ExceptT . tPutBlock th . smpEncode
+1 -1
View File
@@ -40,7 +40,7 @@ infixl 4 <$$>, <$?>
{-# INLINE (<$$>) #-}
(<$?>) :: MonadFail m => (a -> Either String b) -> m a -> m b
f <$?> m = m >>= either fail pure . f
f <$?> m = either fail pure . f =<< m
{-# INLINE (<$?>) #-}
bshow :: Show a => a -> ByteString
+10
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
module Simplex.Messaging.Version
@@ -10,8 +11,11 @@ module Simplex.Messaging.Version
)
where
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Word (Word16)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util (bshow)
pattern VersionRange :: Word16 -> Word16 -> VersionRange
pattern VersionRange v1 v2 <- VRange v1 v2
@@ -41,6 +45,12 @@ instance Encoding VersionRange where
maybe (fail "invalid version range") pure
=<< versionRange <$> smpP <*> smpP
instance StrEncoding VersionRange where
strEncode (VRange v1 v2) = bshow v1 <> "-" <> bshow v2
strP =
maybe (fail "invalid version range") pure
=<< versionRange <$> A.decimal <* A.char '-' <*> A.decimal
compatibleVersion :: VersionRange -> VersionRange -> Maybe Word16
compatibleVersion (VersionRange min1 max1) (VersionRange min2 max2)
| min1 <= max2 && min2 <= max1 = Just $ min max1 max2
+9 -12
View File
@@ -21,6 +21,7 @@ import SMPAgentClient
import SMPClient (testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn)
import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Agent.Protocol as A
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..))
import System.Directory (removeFile)
@@ -111,7 +112,7 @@ pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody
testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO ()
testDuplexConnection _ alice bob = do
("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW INV")
let cReq' = serializeConnReq cReq
let cReq' = strEncode cReq
bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK)
("", "bob", Right (CONF confId "bob's connInfo")) <- (alice <#:)
alice #: ("2", "bob", "LET " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK)
@@ -143,7 +144,7 @@ testDuplexConnection _ alice bob = do
testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO ()
testDuplexConnRandomIds _ alice bob = do
("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW INV")
let cReq' = serializeConnReq cReq
let cReq' = strEncode cReq
("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo")
("", bobConn', Right (CONF confId "bob's connInfo")) <- (alice <#:)
bobConn' `shouldBe` bobConn
@@ -176,7 +177,7 @@ testDuplexConnRandomIds _ alice bob = do
testContactConnection :: Transport c => TProxy c -> c -> c -> c -> IO ()
testContactConnection _ alice bob tom = do
("1", "alice_contact", Right (INV cReq)) <- alice #: ("1", "alice_contact", "NEW CON")
let cReq' = serializeConnReq cReq
let cReq' = strEncode cReq
bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK)
("", "alice_contact", Right (REQ aInvId "bob's connInfo")) <- (alice <#:)
@@ -207,7 +208,7 @@ testContactConnection _ alice bob tom = do
testContactConnRandomIds :: Transport c => TProxy c -> c -> c -> IO ()
testContactConnRandomIds _ alice bob = do
("1", aliceContact, Right (INV cReq)) <- alice #: ("1", "", "NEW CON")
let cReq' = serializeConnReq cReq
let cReq' = strEncode cReq
("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo")
("", aliceContact', Right (REQ aInvId "bob's connInfo")) <- (alice <#:)
@@ -230,7 +231,7 @@ testContactConnRandomIds _ alice bob = do
testRejectContactRequest :: Transport c => TProxy c -> c -> c -> IO ()
testRejectContactRequest _ alice bob = do
("1", "a_contact", Right (INV cReq)) <- alice #: ("1", "a_contact", "NEW CON")
let cReq' = serializeConnReq cReq
let cReq' = strEncode cReq
bob #: ("11", "alice", "JOIN " <> cReq' <> " 10\nbob's info") #> ("11", "alice", OK)
("", "a_contact", Right (REQ aInvId "bob's info")) <- (alice <#:)
-- RJCT must use correct contact connection
@@ -329,7 +330,7 @@ testMsgDeliveryAgentRestart t bob = do
connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO ()
connect (h1, name1) (h2, name2) = do
("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW INV")
let cReq' = serializeConnReq cReq
let cReq' = strEncode cReq
h2 #: ("c2", name1, "JOIN " <> cReq' <> " 5\ninfo2") #> ("c2", name1, OK)
("", _, Right (CONF connId "info2")) <- (h1 <#:)
h1 #: ("c3", name2, "LET " <> connId <> " 5\ninfo1") #> ("c3", name2, OK)
@@ -340,7 +341,7 @@ connect (h1, name1) (h2, name2) = do
-- connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString)
-- connect' h1 h2 = do
-- ("c1", conn2, Right (INV cReq)) <- h1 #: ("c1", "", "NEW INV")
-- let cReq' = serializeConnReq cReq
-- let cReq' = strEncode cReq
-- ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> cReq' <> " 5\ninfo2")
-- ("", _, Right (REQ connId "info2")) <- (h1 <#:)
-- h1 #: ("c3", conn2, "ACPT " <> connId <> " 5\ninfo1") =#> \case ("c3", c, OK) -> c == conn2; _ -> False
@@ -349,11 +350,8 @@ connect (h1, name1) (h2, name2) = do
-- h1 <# ("", conn2, CON)
-- pure (conn1, conn2)
samplePublicKey :: ByteString
samplePublicKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSGfad0aUg0tJ0kG2nzrIPGLiz8wb3dQSJC9xkRHyzHhEE8Kmy2cM4q7rNZIlLcm4M7oXOTe7SC4x59bLQG9bteZPKqXu9wk41hNamV25PWQ4zIcIRmZKETVGbwN7jFMpH7wxLdI1zzMArAPKXCDCJ5ctWh4OWDI6OR6AcCtEj-toCI6N6pjxxn5VigJtwiKhxYpoUJSdNM60wVEDCSUrZYBAuDH8pOxPfP-Tm4sokaFDTIG3QJFzOjC-_9nW4MUjAOFll9PCp9kaEFHJ_YmOYKMWNOCCPvLS6lxA83i0UaardkNLNoFS5paWfTlroxRwOC2T6PwO2ywKBgDjtXcSED61zK1seocQMyGRINnlWdhceD669kIHju_f6kAayvYKW3_lbJNXCmyinAccBosO08_0sUxvtuniIo18kfYJE0UmP1ReCjhMP-O-yOmwZJini_QelJk_Pez8IIDDWnY1qYQsN_q7ocjakOYrpGG7mig6JMFpDJtD6istR"
sampleDhKey :: ByteString
sampleDhKey = "x25519:MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o="
sampleDhKey = "MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o="
syntaxTests :: forall c. Transport c => TProxy c -> Spec
syntaxTests t = do
@@ -378,7 +376,6 @@ syntaxTests t = do
<> "%40localhost%3A5001%2F3456-w%3D%3D%23"
<> urlEncode True sampleDhKey
<> "&e2e="
<> urlEncode True samplePublicKey
<> " 14\nbob's connInfo"
)
>#> ("311", "a", "ERR SMP AUTH")
+12 -14
View File
@@ -8,7 +8,8 @@ import Data.ByteString (ByteString)
import Network.HTTP.Types (urlEncode)
import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (smpClientVersion)
import Test.Hspec
uri :: String
@@ -19,7 +20,7 @@ srv =
SMPServer
{ host = "smp.simplex.im",
port = Just "5223",
keyHash = Just (C.KeyHash "\215m\248\251")
keyHash = C.KeyHash "\215m\248\251"
}
queue :: SMPQueueUri
@@ -27,6 +28,7 @@ queue =
SMPQueueUri
{ smpServer = srv,
senderId = "\223\142z\251",
smpVersionRange = smpClientVersion,
dhPublicKey = testDhKey
}
@@ -34,19 +36,16 @@ testDhKey :: C.PublicKeyX25519
testDhKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o="
testDhKeyStr :: ByteString
testDhKeyStr = C.serializePubKeyUri' testDhKey
testDhKeyStr = strEncode testDhKey
testDhKeyStrUri :: ByteString
testDhKeyStrUri = urlEncode True testDhKeyStr
appServer :: ConnReqScheme
appServer = CRSAppServer "simplex.chat" Nothing
connectionRequest :: AConnectionRequest
connectionRequest =
ACR SCMInvitation . CRInvitation $
ConnReqData
{ crScheme = appServer,
{ crScheme = simplexChat,
crSmpQueues = [queue],
crEncryption = ConnectionEncryption
}
@@ -55,23 +54,22 @@ connectionRequestTests :: Spec
connectionRequestTests =
describe "connection request parsing / serializing" $ do
it "should serialize SMP queue URIs" $ do
serializeSMPQueueUri queue {smpServer = srv {port = Nothing}}
strEncode queue {smpServer = srv {port = Nothing}}
`shouldBe` "smp://1234-w==@smp.simplex.im/3456-w==#" <> testDhKeyStr
serializeSMPQueueUri queue
strEncode queue
`shouldBe` "smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr
it "should parse SMP queue URIs" $ do
parseAll smpQueueUriP ("smp://1234-w==@smp.simplex.im/3456-w==#" <> testDhKeyStr)
strDecode ("smp://1234-w==@smp.simplex.im/3456-w==#" <> testDhKeyStr)
`shouldBe` Right queue {smpServer = srv {port = Nothing}}
parseAll smpQueueUriP ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr)
strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr)
`shouldBe` Right queue
it "should serialize connection requests" $ do
serializeConnReq connectionRequest
strEncode connectionRequest
`shouldBe` "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23"
<> testDhKeyStrUri
<> "&e2e="
it "should parse connection requests" $ do
parseAll
connReqP
strDecode
( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23"
<> testDhKeyStrUri
<> "&e2e="
+1 -1
View File
@@ -156,7 +156,7 @@ cfg :: AgentConfig
cfg =
defaultAgentConfig
{ tcpPort = agentTestPort,
smpServers = L.fromList ["localhost:5001#9VjLsOY5ZvB4hoglNdBzJFAUi/vP4GkZnJFahQOXV20="],
smpServers = L.fromList ["smp://9VjLsOY5ZvB4hoglNdBzJFAUi_vP4GkZnJFahQOXV20=@localhost:5001"],
tbqSize = 1,
dbFile = testDB,
smpCfg =
+8 -11
View File
@@ -36,11 +36,8 @@ testPort = "5001"
testPort2 :: ServiceName
testPort2 = "5002"
testKeyHashStr :: ByteString
testKeyHashStr = "9VjLsOY5ZvB4hoglNdBzJFAUi/vP4GkZnJFahQOXV20="
testKeyHash :: Maybe C.KeyHash
testKeyHash = Just "9VjLsOY5ZvB4hoglNdBzJFAUi/vP4GkZnJFahQOXV20="
testKeyHash :: C.KeyHash
testKeyHash = "9VjLsOY5ZvB4hoglNdBzJFAUi_vP4GkZnJFahQOXV20="
testStoreLogFile :: FilePath
testStoreLogFile = "tests/tmp/smp-server-store.log"
@@ -48,7 +45,7 @@ testStoreLogFile = "tests/tmp/smp-server-store.log"
testSMPClient :: (Transport c, MonadUnliftIO m) => (THandle c -> m a) -> m a
testSMPClient client =
runTransportClient testHost testPort testKeyHash $ \h ->
liftIO (runExceptT $ clientHandshake h) >>= \case
liftIO (runExceptT $ clientHandshake h testKeyHash) >>= \case
Right th -> client th
Left e -> error $ show e
@@ -68,17 +65,17 @@ cfg =
}
withSmpServerStoreLogOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a
withSmpServerStoreLogOn t port client = do
withSmpServerStoreLogOn t port' client = do
s <- liftIO $ openReadStoreLog testStoreLogFile
serverBracket
(\started -> runSMPServerBlocking started cfg {transports = [(port, t)], storeLog = Just s})
(\started -> runSMPServerBlocking started cfg {transports = [(port', t)], storeLog = Just s})
(pure ())
client
withSmpServerThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a
withSmpServerThreadOn t port =
withSmpServerThreadOn t port' =
serverBracket
(\started -> runSMPServerBlocking started cfg {transports = [(port, t)]})
(\started -> runSMPServerBlocking started cfg {transports = [(port', t)]})
(pure ())
serverBracket :: MonadUnliftIO m => (TMVar Bool -> m ()) -> m () -> (ThreadId -> m a) -> m a
@@ -95,7 +92,7 @@ serverBracket process afterProcess f = do
_ -> pure ()
withSmpServerOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> m a -> m a
withSmpServerOn t port = withSmpServerThreadOn t port . const
withSmpServerOn t port' = withSmpServerThreadOn t port' . const
withSmpServer :: (MonadUnliftIO m, MonadRandom m) => ATransport -> m a -> m a
withSmpServer t = withSmpServerOn t testPort
+5 -4
View File
@@ -19,6 +19,7 @@ import qualified Data.ByteString.Char8 as B
import SMPClient
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
import Simplex.Messaging.Transport
import System.Directory (removeFile)
@@ -214,13 +215,13 @@ testDuplex (ATransport t) =
-- aSnd ID is passed to Bob out-of-band
(bsPub, bsKey) <- C.generateSignatureKeyPair C.SEd448
Resp "bcda" _ OK <- sendRecv bob ("", "bcda", aSnd, SEND $ "key " <> C.serializePubKey bsPub)
Resp "bcda" _ OK <- sendRecv bob ("", "bcda", aSnd, SEND $ "key " <> strEncode bsPub)
-- "key ..." is ad-hoc, not a part of SMP protocol
Resp "" _ (MSG mId1 _ msg1) <- tGet alice
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, ACK)
Right ["key", bobKey] <- pure $ B.words <$> aDec mId1 msg1
(bobKey, C.serializePubKey bsPub) #== "key received from Bob"
(bobKey, strEncode bsPub) #== "key received from Bob"
Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, KEY bsPub)
(brPub, brKey) <- C.generateSignatureKeyPair C.SEd448
@@ -236,13 +237,13 @@ testDuplex (ATransport t) =
(bId, encode bSnd) #== "reply queue ID received from Bob"
(asPub, asKey) <- C.generateSignatureKeyPair C.SEd448
Resp "dabc" _ OK <- sendRecv alice ("", "dabc", bSnd, SEND $ "key " <> C.serializePubKey asPub)
Resp "dabc" _ OK <- sendRecv alice ("", "dabc", bSnd, SEND $ "key " <> strEncode asPub)
-- "key ..." is ad-hoc, not a part of SMP protocol
Resp "" _ (MSG mId3 _ msg3) <- tGet bob
Resp "abcd" _ OK <- signSendRecv bob brKey ("abcd", bRcv, ACK)
Right ["key", aliceKey] <- pure $ B.words <$> bDec mId3 msg3
(aliceKey, C.serializePubKey asPub) #== "key received from Alice"
(aliceKey, strEncode asPub) #== "key received from Alice"
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, KEY asPub)
Resp "cdab" _ OK <- signSendRecv bob bsKey ("cdab", aSnd, SEND "hi alice")