mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-03-30 16:26:02 +00:00
* parameterize version scopes with phantom types * move Version to another module * parens
170 lines
5.8 KiB
Haskell
170 lines
5.8 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Simplex.RemoteControl.Invitation
|
|
( RCInvitation (..)
|
|
, signInvitation
|
|
, RCSignedInvitation (..)
|
|
, verifySignedInvitation
|
|
, RCVerifiedInvitation (..)
|
|
, RCEncInvitation (..)
|
|
) where
|
|
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
|
import Data.ByteString (ByteString)
|
|
import qualified Data.ByteString as B
|
|
import qualified Data.ByteString.Lazy as LB
|
|
import Data.Time.Clock.System (SystemTime)
|
|
import Data.Word (Word16)
|
|
import Network.HTTP.Types (parseSimpleQuery)
|
|
import Network.HTTP.Types.URI (SimpleQuery, renderSimpleQuery, urlDecode)
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Encoding
|
|
import Simplex.Messaging.Encoding.String
|
|
import Simplex.Messaging.Parsers (parseAll)
|
|
import Simplex.Messaging.Transport.Client (TransportHost)
|
|
import Simplex.RemoteControl.Types (VersionRangeRCP)
|
|
|
|
data RCInvitation = RCInvitation
|
|
{ -- | CA TLS certificate fingerprint of the controller.
|
|
--
|
|
-- This is part of long term identity of the controller established during the first session, and repeated in the subsequent session announcements.
|
|
ca :: C.KeyHash,
|
|
host :: TransportHost,
|
|
port :: Word16,
|
|
-- | Supported version range for remote control protocol
|
|
v :: VersionRangeRCP,
|
|
-- | Application information
|
|
app :: J.Value,
|
|
-- | Session start time in seconds since epoch
|
|
ts :: SystemTime,
|
|
-- | Session Ed25519 public key used to verify the announcement and commands
|
|
--
|
|
-- This mitigates the compromise of the long term signature key, as the controller will have to sign each command with this key first.
|
|
skey :: C.PublicKeyEd25519,
|
|
-- | Long-term Ed25519 public key used to verify the announcement and commands.
|
|
--
|
|
-- Is apart of the long term controller identity.
|
|
idkey :: C.PublicKeyEd25519,
|
|
-- | Session X25519 DH key
|
|
dh :: C.PublicKeyX25519
|
|
}
|
|
deriving (Show)
|
|
|
|
instance StrEncoding RCInvitation where
|
|
strEncode RCInvitation {ca, host, port, v, app, ts, skey, idkey, dh} =
|
|
mconcat
|
|
[ "xrcp:/",
|
|
strEncode ca,
|
|
"@",
|
|
strEncode host,
|
|
":",
|
|
strEncode port,
|
|
"#/?",
|
|
renderSimpleQuery False query
|
|
]
|
|
where
|
|
query =
|
|
[ ("v", strEncode v),
|
|
("app", LB.toStrict $ J.encode app),
|
|
("ts", strEncode ts),
|
|
("skey", strEncode skey),
|
|
("idkey", strEncode idkey),
|
|
("dh", strEncode dh)
|
|
]
|
|
|
|
strP = do
|
|
_ <- A.string "xrcp:/"
|
|
ca <- strP
|
|
_ <- A.char '@'
|
|
host <- A.takeWhile (/= ':') >>= either fail pure . strDecode . urlDecode True
|
|
_ <- A.char ':'
|
|
port <- strP
|
|
_ <- A.string "#/?"
|
|
|
|
q <- parseSimpleQuery <$> A.takeWhile (/= ' ')
|
|
v <- requiredP q "v" strDecode
|
|
app <- requiredP q "app" $ J.eitherDecodeStrict . urlDecode True
|
|
ts <- requiredP q "ts" $ strDecode . urlDecode True
|
|
skey <- requiredP q "skey" $ parseAll strP
|
|
idkey <- requiredP q "idkey" $ parseAll strP
|
|
dh <- requiredP q "dh" $ parseAll strP
|
|
pure RCInvitation {ca, host, port, v, app, ts, skey, idkey, dh}
|
|
|
|
data RCSignedInvitation = RCSignedInvitation
|
|
{ invitation :: RCInvitation,
|
|
ssig :: C.Signature 'C.Ed25519,
|
|
idsig :: C.Signature 'C.Ed25519
|
|
}
|
|
deriving (Show)
|
|
|
|
-- | URL-encoded and signed for showing in QR code
|
|
instance StrEncoding RCSignedInvitation where
|
|
strEncode RCSignedInvitation {invitation, ssig, idsig} =
|
|
mconcat
|
|
[ strEncode invitation,
|
|
"&ssig=",
|
|
strEncode $ C.signatureBytes ssig,
|
|
"&idsig=",
|
|
strEncode $ C.signatureBytes idsig
|
|
]
|
|
|
|
strP = do
|
|
-- TODO this assumes some order or parameters, can be made independent
|
|
(url, invitation) <- A.match strP
|
|
sigs <- case B.breakSubstring "&ssig=" url of
|
|
(_, sigs) | B.null sigs -> fail "missing signatures"
|
|
(_, sigs) -> pure $ parseSimpleQuery $ B.drop 1 sigs
|
|
ssig <- requiredP sigs "ssig" $ parseAll strP
|
|
idsig <- requiredP sigs "idsig" $ parseAll strP
|
|
pure RCSignedInvitation {invitation, ssig, idsig}
|
|
|
|
signInvitation :: C.PrivateKey C.Ed25519 -> C.PrivateKey C.Ed25519 -> RCInvitation -> RCSignedInvitation
|
|
signInvitation sKey idKey invitation = RCSignedInvitation {invitation, ssig, idsig}
|
|
where
|
|
uri = strEncode invitation
|
|
ssig =
|
|
case C.sign (C.APrivateSignKey C.SEd25519 sKey) uri of
|
|
C.ASignature C.SEd25519 s -> s
|
|
_ -> error "signing with ed25519"
|
|
inviteUrlSigned = mconcat [uri, "&ssig=", strEncode ssig]
|
|
idsig =
|
|
case C.sign (C.APrivateSignKey C.SEd25519 idKey) inviteUrlSigned of
|
|
C.ASignature C.SEd25519 s -> s
|
|
_ -> error "signing with ed25519"
|
|
|
|
newtype RCVerifiedInvitation = RCVerifiedInvitation RCInvitation
|
|
deriving (Show)
|
|
|
|
verifySignedInvitation :: RCSignedInvitation -> Maybe RCVerifiedInvitation
|
|
verifySignedInvitation RCSignedInvitation {invitation, ssig, idsig} =
|
|
if C.verify' skey ssig inviteURL && C.verify' idkey idsig inviteURLS
|
|
then Just $ RCVerifiedInvitation invitation
|
|
else Nothing
|
|
where
|
|
RCInvitation {skey, idkey} = invitation
|
|
inviteURL = strEncode invitation
|
|
inviteURLS = mconcat [inviteURL, "&ssig=", strEncode ssig]
|
|
|
|
data RCEncInvitation = RCEncInvitation
|
|
{ dhPubKey :: C.PublicKeyX25519,
|
|
nonce :: C.CbNonce,
|
|
encInvitation :: ByteString
|
|
}
|
|
|
|
instance Encoding RCEncInvitation where
|
|
smpEncode RCEncInvitation {dhPubKey, nonce, encInvitation} =
|
|
smpEncode (dhPubKey, nonce, Tail encInvitation)
|
|
smpP = do
|
|
(dhPubKey, nonce, Tail encInvitation) <- smpP
|
|
pure RCEncInvitation {dhPubKey, nonce, encInvitation}
|
|
|
|
-- * Utils
|
|
|
|
requiredP :: MonadFail m => SimpleQuery -> ByteString -> (ByteString -> Either String a) -> m a
|
|
requiredP q k f = maybe (fail $ "missing " <> show k) (either fail pure . f) $ lookup k q
|