diff --git a/rfcs/2025-03-16-smp-queues.md b/rfcs/2025-03-16-smp-queues.md index d3d06d444..5b46b383b 100644 --- a/rfcs/2025-03-16-smp-queues.md +++ b/rfcs/2025-03-16-smp-queues.md @@ -84,7 +84,7 @@ data NtfRequest = NtfRequest NtfPublicAuthKey RcvNtfPublicDhKey -- LinkId is required with QRContact, to have shorter link - it will be derived from the link_uri. And in this case we do not need to prevent checks that this queue exists. data QueueReqData = QRMessaging (Maybe QueueLinkData) | QRContact (Maybe (LinkId, QueueLinkData)) --- SenderId should be computed client-side as sha3-256(correlation_id), +-- SenderId should be computed client-side as the first 24 bytes of sha3-384(correlation_id), -- The server must verify it and reject if it is not. type QueueLinkData = (SenderId, EncImmutableDataBytes, EncUserDataBytes) @@ -152,7 +152,7 @@ LGET :: Command Sender LNK :: SenderId -> QueueLinkData -> BrokerMsg ``` -To both include sender_id into the full link before the server response, and to prevent "oracle attack" when a failure to create the queue with the supplied `sender_id` can be used as a proof of queue existense, it is proposed that `sender_id` is computed client-side as `sha3-256(correlation_id)` and validated server-side, where `corelation_id` is the transmission correlation ID. +To both include sender_id into the full link before the server response, and to prevent "oracle attack" when a failure to create the queue with the supplied `sender_id` can be used as a proof of queue existense, it is proposed that `sender_id` is computed client-side as the first 24 bytes of 48 in `sha3-384(correlation_id)` and validated server-side, where `corelation_id` is the transmission correlation ID. To allow retries and to avoid regenerating all queue data, NEW command must be idempotent, and `correlation_id` must be preserved in command for queue creation, so that the same `correlation_id` and all other data is used in retries. `correlation_id` should be removed after queue creation success. diff --git a/rfcs/2025-03-30-ios-notifications-3.md b/rfcs/2025-03-30-ios-notifications-3.md new file mode 100644 index 000000000..441525767 --- /dev/null +++ b/rfcs/2025-03-30-ios-notifications-3.md @@ -0,0 +1,93 @@ +# New notifications protocol + +## Problem + +iOS notifications have these problems: +- iOS notification service crashes exceeding memory limit. This is being addressed by changes in GHC RTS. +- there is a large number of connections, because each member in a group requires individual connection. This will improve with chat relays when each group would require 2-3 connections. +- some notification may be not shown if notification with reply/mention is skipped, and instead some other message is delivered, which may be muted. This would not improve without some changes, as notifications may be skipped anyway. +- client devices delay communication with ntf server because it is done in background, and by that time the app may be suspended. +- notification server represents a bottleneck, as it has to be owned by the app vendor, and the current design when ntf server subscribes to notifications scales very badly. + +This RFC is based on the previous [RFC related to notifications](./2024-09-25-ios-notifications-2.md). + +## Solution + +As notification server has to know client token and currently it associates subscriptions with this token anyway, we are not gaining any privacy and security by using per-subscription keys - both authorization and encryption keys of notification subscription can be dropped. + +We still need to store the list of queue IDs associated with the token on the notification server, but we do not need any per-queue keys on the notification server, and we don't need subscriptions - it's effectively a simple set of IDs, with no other information. + +In this case, when queue is created the client would supply notifier ID - it has to be derived from correlation ID, to prevent existense check (see previous RFC). As we also supply sender ID, instead of deriving it as sha3-192 of correlation ID, they both can be derived as sha3-384 and split to two IDs - 24 bytes each. + +The notification server will maintain a rotating list of server keys with the latest key communicated to the client every time the token is registered and checked. The keys would expire after, say, 1 week or 1 month, and removed from notification server on expiration. + +The packet containing association between notifier queue ID and token will be crypto_box encrypted using key agreement between identified notification server master key and an ephemeral per packet (effectively, per-queue) client-key. + +Deleting the queue may also include encrypted packet that would verify that the client deleted the queue. + +Instead of notification server subscribing to the notifications creating a lot of traffic for the queues without messages, the SMP server would push notifications via NTF server connection (whether via NTF or via SMP protocol). This could be used as a mechanism to migrate existing queues when with the next subscription the notification server would communicate it's address to SMP server and this association would be stored together with the queue. + +## Protocol design + +Additional/changed SMP commands: + +```haskell +-- register notification server +-- should be signed with server key +NSRV :: NtfServerCreds -> Command NtfServer + +-- response +NSID :: NtfServerId -> BrokerMsg + +-- to communicate which server is responsible for the queue +-- should be signed with queue key +NSUB :: Maybe NtfServerId -> Command Notifier + +-- subscribe to notificaions from all queues associated with the server +-- should be signed with server key +-- entity ID - NtfServerId +NSSUB :: Command NtfServer + +data NtfServerCreds = NtfServerCreds + { server :: NtfServer, + -- NTF server certificate chain that should match fingerpring in address + cert :: X.CertificateChain, + -- server autorizatio key to sign server subscription requests + authKey :: X.SignedExact X.PubKey + } + +-- entity ID is recipient ID +NSKEY :: NtfSubscription -> Command Recipient + +data NtfSubscription = NtfSubscription + -- key to encrypt notifications e2e with the client + { ntfPubDbKey :: RcvNtfPublicDhKey, + ntfServer :: NtfServer, + -- should be linked to correlation ID to prevent existense check + -- the ID sent to notification server could be its hash? + ntfId :: NotifierId, + encNtfTokenAssoc :: EncDataBytes + } + +-- before the encryption - equivalent to NSUB command, but without key to authorize requests to specific queue +data NtfTokenAssoc = NtfTokenAssoc + { signature :: SignatureEd25519, + tknId :: NtfTokenId, + ntfQueue :: SMPQueueNtf + } +``` + +SMP server will need to maintain the list of Ntf servers and their credentials, and when NSSUB arrives to make only one subscription. When message arrives it would deliver notification to the correct connection via queue / ntf server association. + +Ntf server needs to maintain three indices to the same data: +- `(smpServer, queueId) -> tokenId` - to deliver notification to the correct token +- `tokenId -> [smpServer -> [queueId]]` - to remove all queues when token is removed, and to store/update these associations effficiently - store log may have one compact line per token (after compacting), or per token/server combination. +- `[smpServer]` - array of SMP servers to subscribe to. + +## Mention notifications + +Currently we are marking messages with T (true) for messages that require notifications and F (false) for messages that don't require. Sender does not know whether the recipient has notifications disabled, enabled or in mentions-only mode. + +The proposal is to: +- add additional values to this metadata, e.g. 2 (priority) and 3 (high priority) (and T/F could be sent as 0/1 respectively) - that is, to deliver notifications even if notifications are generally disabled (they can still be further filtered by the client). +- instead of deleting notification credentials when notifications are disabled - which is costly - communicate to SMP server the change of notificaion priority level, e.g. the client could set minimal notification priority to deliver notifications, where 0 would mean disabling it completely, 1 enable for all, 2 for priority 2+, 3 for priority 3. The downside here is that it could be used for timing correlation of queues in the group, but it already can be used on bulk deletions of ntf credentials for these queues and when sending messages. diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 2cedf5fbd..5d9a7ceef 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -908,8 +908,9 @@ newRcvConnSrv c userId connId enableNtfs cMode userData_ clientData pqInitKeys s createRcvQueue :: Maybe C.CbNonce -> ClntQueueReqData -> C.KeyPairX25519 -> AM (RcvQueue, SMPQueueUri) createRcvQueue nonce_ qd e2eKeys = do AgentConfig {smpClientVRange = vr} <- asks config - let ntfCreds_ = Nothing -- TODO [short links] - (rq, qUri, tSess, sessId) <- newRcvQueue_ c userId connId srvWithAuth vr qd subMode ntfCreds_ nonce_ e2eKeys `catchAgentError` \e -> liftIO (print e) >> throwE e + -- TODO [notifications] send correct NTF credentials here + -- let ntfCreds_ = Nothing + (rq, qUri, tSess, sessId) <- newRcvQueue_ c userId connId srvWithAuth vr qd subMode nonce_ e2eKeys `catchAgentError` \e -> liftIO (print e) >> throwE e atomically $ incSMPServerStat c userId srv connCreated rq' <- withStore c $ \db -> updateNewConnRcv db connId rq lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId @@ -934,7 +935,8 @@ newRcvConnSrv c userId connId enableNtfs cMode userData_ clientData pqInitKeys s nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g sigKeys@(_, privSigKey) <- atomically $ C.generateKeyPair @'C.Ed25519 g AgentConfig {smpClientVRange = vr, smpAgentVRange} <- asks config - let sndId = SMP.EntityId $ C.sha3_256 corrId + -- TODO [notifications] the remaining 24 bytes are reserved for notifier ID + let sndId = SMP.EntityId $ B.take 24 $ C.sha3_384 corrId sndSecure = case cMode of SCMContact -> False; SCMInvitation -> True qUri = SMPQueueUri vr $ SMPQueueAddress srv sndId e2eDhKey sndSecure connReq <- createConnReq qUri @@ -1117,8 +1119,8 @@ joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _subMode createReplyQueue :: AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> SMPServerWithAuth -> AM SMPQueueInfo createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do - -- TODO [short links] ntf credentials - (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) SCMInvitation subMode Nothing + -- TODO [notifications] send correct NTF credentials here + (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) SCMInvitation subMode -- Nothing atomically $ incSMPServerStat c userId (qServer rq) connCreated let qInfo = toVersionT qUri smpClientVersion rq' <- withStore c $ \db -> upgradeSndConnToDuplex db connId rq @@ -1886,8 +1888,8 @@ switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId} rqs s -- try to get the server that is different from all queues, or at least from the primary rcv queue srvAuth@(ProtoServerWithAuth srv _) <- getNextSMPServer c userId $ map qServer (L.toList rqs) <> map qServer (L.toList sqs) srv' <- if srv == server then getNextSMPServer c userId [server] else pure srvAuth - -- TODO [short links] send correct NTF credentials here - (q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SCMInvitation SMSubscribe Nothing + -- TODO [notifications] send correct NTF credentials here + (q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SCMInvitation SMSubscribe -- Nothing let rq' = (q :: NewRcvQueue) {primary = True, dbReplaceQueueId = Just dbQueueId} rq'' <- withStore c $ \db -> addConnRcvQueue db connId rq' lift $ addNewQueueSubscription c rq'' tSess sessId diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 0ea7e1e1a..2355cdefa 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -267,7 +267,6 @@ import Simplex.Messaging.Protocol SubscriptionMode (..), QueueReqData (..), QueueLinkData, - NewNtfCreds, UserProtocol, VersionRangeSMPC, VersionSMPC, @@ -1232,7 +1231,8 @@ runSMPServerTest c userId (ProtoServerWithAuth srv auth) = do (sKey, spKey) <- atomically $ C.generateAuthKeyPair sa g (dhKey, _) <- atomically $ C.generateKeyPair g r <- runExceptT $ do - SMP.QIK {rcvId, sndId, queueMode} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp Nothing rKeys dhKey auth SMSubscribe (QRMessaging Nothing) Nothing + -- TODO [notifications] + SMP.QIK {rcvId, sndId, queueMode} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp Nothing rKeys dhKey auth SMSubscribe (QRMessaging Nothing) -- Nothing liftError (testErr TSSecureQueue) $ case queueMode of Just QMMessaging -> secureSndSMPQueue smp spKey sndId sKey @@ -1343,12 +1343,12 @@ getSessionMode :: MonadIO m => AgentClient -> m TransportSessionMode getSessionMode = fmap sessionMode . getNetworkConfig {-# INLINE getSessionMode #-} --- TODO [short links] add ntf credentials to RcvQueue -newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SConnectionMode c -> SubscriptionMode -> Maybe NewNtfCreds -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId) -newRcvQueue c userId connId srv vRange cMode subMode ntfCreds = do +-- TODO [notifications] +newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SConnectionMode c -> SubscriptionMode -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId) +newRcvQueue c userId connId srv vRange cMode subMode = do let qrd = case cMode of SCMInvitation -> CQRMessaging Nothing; SCMContact -> CQRContact Nothing e2eKeys <- atomically . C.generateKeyPair =<< asks random - newRcvQueue_ c userId connId srv vRange qrd subMode ntfCreds Nothing e2eKeys + newRcvQueue_ c userId connId srv vRange qrd subMode Nothing e2eKeys data ClntQueueReqData = CQRMessaging (Maybe (CQRData (SMP.SenderId, QueueLinkData))) @@ -1365,18 +1365,18 @@ queueReqData = \case CQRMessaging d -> QRMessaging $ srvReq <$> d CQRContact d -> QRContact $ srvReq <$> d -newRcvQueue_ :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> ClntQueueReqData -> SubscriptionMode -> Maybe NewNtfCreds -> Maybe C.CbNonce -> C.KeyPairX25519 -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId) -newRcvQueue_ c userId connId (ProtoServerWithAuth srv auth) vRange cqrd subMode ntfCreds nonce_ (e2eDhKey, e2ePrivKey) = do +newRcvQueue_ :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> ClntQueueReqData -> SubscriptionMode -> Maybe C.CbNonce -> C.KeyPairX25519 -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId) +newRcvQueue_ c userId connId (ProtoServerWithAuth srv auth) vRange cqrd subMode nonce_ (e2eDhKey, e2ePrivKey) = do C.AuthAlg a <- asks (rcvAuthAlg . config) g <- asks random rKeys@(_, rcvPrivateKey) <- atomically $ C.generateAuthKeyPair a g (dhKey, privDhKey) <- atomically $ C.generateKeyPair g logServer "-->" c srv NoEntity "NEW" tSess <- mkTransportSession c userId srv connId - -- TODO [short links] serverNtfCreds - r@(thParams', QIK {rcvId, sndId, rcvPublicDhKey, queueMode, serverNtfCreds}) <- + -- TODO [notifications] + r@(thParams', QIK {rcvId, sndId, rcvPublicDhKey, queueMode}) <- withClient c tSess $ \(SMPConnectedClient smp _) -> - (thParams smp,) <$> createSMPQueue smp nonce_ rKeys dhKey auth subMode (queueReqData cqrd) ntfCreds + (thParams smp,) <$> createSMPQueue smp nonce_ rKeys dhKey auth subMode (queueReqData cqrd) liftIO . logServer "<--" c srv NoEntity $ B.unwords ["IDS", logSecret rcvId, logSecret sndId] shortLink <- mkShortLinkCreds r let rq = @@ -1398,7 +1398,7 @@ newRcvQueue_ c userId connId (ProtoServerWithAuth srv auth) vRange cqrd subMode dbReplaceQueueId = Nothing, rcvSwchStatus = Nothing, smpClientVersion = maxVersion vRange, - clientNtfCreds = Nothing, -- TODO [short links] + clientNtfCreds = Nothing, deleteErrors = 0 } qUri = SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey sndSecure diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 5352658b2..7c18a0aa1 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -719,10 +719,11 @@ createSMPQueue :: Maybe BasicAuth -> SubscriptionMode -> QueueReqData -> - Maybe NewNtfCreds -> + -- TODO [notifications] + -- Maybe NewNtfCreds -> ExceptT SMPClientError IO QueueIdsKeys -createSMPQueue c nonce_ (rKey, rpKey) dhKey auth subMode qrd ntfCreds = - sendProtocolCommand_ c nonce_ Nothing (Just rpKey) NoEntity (Cmd SRecipient $ NEW $ NewQueueReq rKey dhKey auth subMode (Just qrd) ntfCreds) >>= \case +createSMPQueue c nonce_ (rKey, rpKey) dhKey auth subMode qrd = + sendProtocolCommand_ c nonce_ Nothing (Just rpKey) NoEntity (Cmd SRecipient $ NEW $ NewQueueReq rKey dhKey auth subMode (Just qrd)) >>= \case IDS qik -> pure qik r -> throwE $ unexpectedResponse r diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index 88b882432..39fc65330 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -171,6 +171,7 @@ module Simplex.Messaging.Crypto sha256Hash, sha512Hash, sha3_256, + sha3_384, -- * Message padding / un-padding canPad, @@ -211,7 +212,7 @@ import Crypto.Cipher.AES (AES256) import qualified Crypto.Cipher.Types as AES import qualified Crypto.Cipher.XSalsa as XSalsa import qualified Crypto.Error as CE -import Crypto.Hash (Digest, SHA256 (..), SHA3_256, SHA512 (..), hash, hashDigestSize) +import Crypto.Hash (Digest, SHA3_256, SHA3_384, SHA256 (..), SHA512 (..), hash, hashDigestSize) import qualified Crypto.KDF.HKDF as H import qualified Crypto.MAC.Poly1305 as Poly1305 import qualified Crypto.PubKey.Curve25519 as X25519 @@ -978,6 +979,11 @@ sha3_256 :: ByteString -> ByteString sha3_256 = BA.convert . (hash :: ByteString -> Digest SHA3_256) {-# INLINE sha3_256 #-} +-- | SHA3-384 digest. +sha3_384 :: ByteString -> ByteString +sha3_384 = BA.convert . (hash :: ByteString -> Digest SHA3_384) +{-# INLINE sha3_384 #-} + -- | AEAD-GCM encryption with associated data. -- -- Used as part of double ratchet encryption. diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 6ae012027..fde0f8e68 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -65,7 +65,6 @@ module Simplex.Messaging.Protocol EncFixedDataBytes, EncUserDataBytes, EncDataBytes (..), - NewNtfCreds (..), Party (..), Cmd (..), DirectParty, @@ -73,7 +72,6 @@ module Simplex.Messaging.Protocol SParty (..), PartyI (..), QueueIdsKeys (..), - ServerNtfCreds (..), ProtocolErrorType (..), ErrorType (..), CommandError (..), @@ -457,8 +455,9 @@ data NewQueueReq = NewQueueReq rcvDhKey :: RcvPublicDhKey, auth_ :: Maybe BasicAuth, subMode :: SubscriptionMode, - queueReqData :: Maybe QueueReqData, - ntfCreds :: Maybe NewNtfCreds + queueReqData :: Maybe QueueReqData + -- TODO [notifications] + -- ntfCreds :: Maybe NewNtfCreds } deriving (Show) @@ -502,7 +501,8 @@ instance ToField EncDataBytes where toField (EncDataBytes s) = toField (Binary s) {-# INLINE toField #-} -data NewNtfCreds = NewNtfCreds NtfPublicAuthKey RcvNtfPublicDhKey deriving (Show) +-- TODO [notifications] +-- data NewNtfCreds = NewNtfCreds NtfPublicAuthKey RcvNtfPublicDhKey deriving (Show) instance StrEncoding SubscriptionMode where strEncode = \case @@ -533,9 +533,10 @@ instance Encoding QueueReqData where 'C' -> QRContact <$> smpP _ -> fail "bad QueueReqData" -instance Encoding NewNtfCreds where - smpEncode (NewNtfCreds authKey dhKey) = smpEncode (authKey, dhKey) - smpP = NewNtfCreds <$> smpP <*> smpP +-- TODO [notifications] +-- instance Encoding NewNtfCreds where +-- smpEncode (NewNtfCreds authKey dhKey) = smpEncode (authKey, dhKey) +-- smpP = NewNtfCreds <$> smpP <*> smpP type SenderCanSecure = Bool @@ -1243,17 +1244,19 @@ data QueueIdsKeys = QIK sndId :: SenderId, rcvPublicDhKey :: RcvPublicDhKey, queueMode :: Maybe QueueMode, -- TODO remove Maybe when min version is 9 (sndAuthKeySMPVersion) - linkId :: Maybe LinkId, - serverNtfCreds :: Maybe ServerNtfCreds + linkId :: Maybe LinkId + -- TODO [notifications] + -- serverNtfCreds :: Maybe ServerNtfCreds } deriving (Eq, Show) -data ServerNtfCreds = ServerNtfCreds NotifierId RcvNtfPublicDhKey - deriving (Eq, Show) +-- TODO [notifications] +-- data ServerNtfCreds = ServerNtfCreds NotifierId RcvNtfPublicDhKey +-- deriving (Eq, Show) -instance Encoding ServerNtfCreds where - smpEncode (ServerNtfCreds nId dhKey) = smpEncode (nId, dhKey) - smpP = ServerNtfCreds <$> smpP <*> smpP +-- instance Encoding ServerNtfCreds where +-- smpEncode (ServerNtfCreds nId dhKey) = smpEncode (nId, dhKey) +-- smpP = ServerNtfCreds <$> smpP <*> smpP -- | Recipient's private key used by the recipient to authorize (v6: sign, v7: encrypt hash) SMP commands. -- @@ -1481,8 +1484,8 @@ class ProtocolMsgTag (Tag msg) => ProtocolEncoding v err msg | msg -> err, msg - instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where type Tag (Command p) = CommandTag p encodeProtocol v = \case - NEW NewQueueReq {rcvAuthKey = rKey, rcvDhKey = dhKey, auth_, subMode, queueReqData, ntfCreds} - | v >= shortLinksSMPVersion -> new <> e (auth_, subMode, queueReqData, ntfCreds) + NEW NewQueueReq {rcvAuthKey = rKey, rcvDhKey = dhKey, auth_, subMode, queueReqData} + | v >= shortLinksSMPVersion -> new <> e (auth_, subMode, queueReqData) | v >= sndAuthKeySMPVersion -> new <> e (auth_, subMode, senderCanSecure (queueReqMode <$> queueReqData)) | otherwise -> new <> auth <> e subMode where @@ -1556,18 +1559,19 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where CT SRecipient tag -> Cmd SRecipient <$> case tag of NEW_ - | v >= shortLinksSMPVersion -> NEW <$> new smpP smpP smpP - | v >= sndAuthKeySMPVersion -> NEW <$> new smpP (qReq <$> smpP) (pure Nothing) - | otherwise -> NEW <$> new auth (pure Nothing) (pure Nothing) + | v >= shortLinksSMPVersion -> NEW <$> new smpP smpP + | v >= sndAuthKeySMPVersion -> NEW <$> new smpP (qReq <$> smpP) + | otherwise -> NEW <$> new auth (pure Nothing) where - new p1 p2 p3 = do + new p1 p2 = do rcvAuthKey <- _smpP rcvDhKey <- smpP auth_ <- p1 subMode <- smpP queueReqData <- p2 - ntfCreds <- p3 - pure NewQueueReq {rcvAuthKey, rcvDhKey, auth_, subMode, queueReqData, ntfCreds} + -- TODO [notifications] + -- ntfCreds <- p3 + pure NewQueueReq {rcvAuthKey, rcvDhKey, auth_, subMode, queueReqData} -- ntfCreds auth = optional (A.char 'A' *> smpP) qReq sndSecure = Just $ if sndSecure then QRMessaging Nothing else QRContact Nothing SUB_ -> pure SUB @@ -1605,8 +1609,8 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where type Tag BrokerMsg = BrokerMsgTag encodeProtocol v = \case - IDS QIK {rcvId, sndId, rcvPublicDhKey = srvDh, queueMode, linkId, serverNtfCreds} - | v >= shortLinksSMPVersion -> ids <> e queueMode <> e linkId <> e serverNtfCreds + IDS QIK {rcvId, sndId, rcvPublicDhKey = srvDh, queueMode, linkId} + | v >= shortLinksSMPVersion -> ids <> e queueMode <> e linkId | v >= sndAuthKeySMPVersion -> ids <> e (senderCanSecure queueMode) | otherwise -> ids where @@ -1640,13 +1644,21 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where where bodyP = EncRcvMsgBody . unTail <$> smpP IDS_ - | v >= shortLinksSMPVersion -> ids smpP smpP smpP - | v >= sndAuthKeySMPVersion -> ids (qm <$> smpP) nothing nothing - | otherwise -> ids nothing nothing nothing + | v >= shortLinksSMPVersion -> ids smpP smpP + | v >= sndAuthKeySMPVersion -> ids (qm <$> smpP) nothing + | otherwise -> ids nothing nothing where qm sndSecure = Just $ if sndSecure then QMMessaging else QMContact nothing = pure Nothing - ids p1 p2 p3 = IDS <$> (QIK <$> _smpP <*> smpP <*> smpP <*> p1 <*> p2 <*> p3) + ids p1 p2 = do + rcvId <- _smpP + sndId <- smpP + rcvPublicDhKey <- smpP + queueMode <- p1 + linkId <- p2 + -- TODO [notifications] + -- serverNtfCreds <- p3 + pure $ IDS QIK {rcvId, sndId, rcvPublicDhKey, queueMode, linkId} LNK_ -> LNK <$> _smpP <*> smpP NID_ -> NID <$> _smpP <*> smpP NMSG_ -> NMSG <$> _smpP <*> smpP diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index cebcb4735..8cf730e06 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -1278,15 +1278,18 @@ client QUE -> withQueue $ \q qr -> (corrId,entId,) <$> getQueueInfo q qr where createQueue :: NewQueueReq -> M (Transmission BrokerMsg) - createQueue NewQueueReq {rcvAuthKey, rcvDhKey, subMode, queueReqData, ntfCreds} = time "NEW" $ do + createQueue NewQueueReq {rcvAuthKey, rcvDhKey, subMode, queueReqData} = time "NEW" $ do g <- asks random idSize <- asks $ queueIdBytes . config updatedAt <- Just <$> liftIO getSystemDate (rcvPublicDhKey, privDhKey) <- atomically $ C.generateKeyPair g - ntfKeys_ <- forM ntfCreds $ \(NewNtfCreds notifierKey dhKey) -> do - (ntfPubDhKey, ntfPrivDhKey) <- atomically $ C.generateKeyPair g - pure (notifierKey, C.dh' dhKey ntfPrivDhKey, ntfPubDhKey) + -- TODO [notifications] + -- ntfKeys_ <- forM ntfCreds $ \(NewNtfCreds notifierKey dhKey) -> do + -- (ntfPubDhKey, ntfPrivDhKey) <- atomically $ C.generateKeyPair g + -- pure (notifierKey, C.dh' dhKey ntfPrivDhKey, ntfPubDhKey) let randId = EntityId <$> atomically (C.randomBytes idSize g) + -- TODO [notifications] the remaining 24 bytes are reserver for notifier ID + sndId' = B.take 24 $ C.sha3_384 (bs corrId) tryCreate 0 = pure $ ERR INTERNAL tryCreate n = do (sndId, clntIds, queueData) <- case queueReqData of @@ -1296,13 +1299,14 @@ client -- The condition that client-provided sender ID must match hash of correlation ID -- prevents "ID oracle" attack, when creating queue with supplied ID can be used to check -- if queue with this ID still exists. - if clntIds && unEntityId sndId /= C.sha3_256 (bs corrId) + if clntIds && unEntityId sndId /= sndId' then pure $ ERR $ CMD PROHIBITED else do rcvId <- randId - ntf <- forM ntfKeys_ $ \(notifierKey, rcvNtfDhSecret, rcvPubDhKey) -> do - notifierId <- randId - pure (NtfCreds {notifierId, notifierKey, rcvNtfDhSecret}, ServerNtfCreds notifierId rcvPubDhKey) + -- TODO [notifications] + -- ntf <- forM ntfKeys_ $ \(notifierKey, rcvNtfDhSecret, rcvPubDhKey) -> do + -- notifierId <- randId + -- pure (NtfCreds {notifierId, notifierKey, rcvNtfDhSecret}, ServerNtfCreds notifierId rcvPubDhKey) let queueMode = queueReqMode <$> queueReqData qr = QueueRec @@ -1312,7 +1316,8 @@ client senderKey = Nothing, queueMode, queueData, - notifier = fst <$> ntf, + -- TODO [notifications] + notifier = Nothing, -- fst <$> ntf, status = EntityActive, updatedAt } @@ -1325,11 +1330,12 @@ client stats <- asks serverStats incStat $ qCreated stats incStat $ qCount stats - when (isJust ntf) $ incStat $ ntfCreated stats + -- TODO [notifications] + -- when (isJust ntf) $ incStat $ ntfCreated stats case subMode of SMOnlyCreate -> pure () SMSubscribe -> void $ subscribeQueue q qr - pure $ IDS QIK {rcvId, sndId, rcvPublicDhKey, queueMode, linkId = fst <$> queueData, serverNtfCreds = snd <$> ntf} + pure $ IDS QIK {rcvId, sndId, rcvPublicDhKey, queueMode, linkId = fst <$> queueData} -- , serverNtfCreds = snd <$> ntf (corrId,entId,) <$> tryCreate (3 :: Int) checkMode :: QueueMode -> QueueRec -> M (Either ErrorType BrokerMsg) -> M (Transmission BrokerMsg) diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 0480f28f9..e815d6f36 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -148,7 +148,7 @@ smpBlockSize = 16384 -- 11 - additional encryption of transport blocks with forward secrecy (10/06/2024) -- 12 - BLOCKED error for blocked queues (1/11/2025) -- 14 - proxyServer handshake property to disable transport encryption between server and proxy (1/19/2025) --- 15 - short links, include creating NTF credentials in NEW command +-- 15 - short links, with associated data passed in NEW of LSET command (3/30/2025) data SMPVersion diff --git a/tests/NtfServerTests.hs b/tests/NtfServerTests.hs index 50c94f2fb..b2e868cc2 100644 --- a/tests/NtfServerTests.hs +++ b/tests/NtfServerTests.hs @@ -50,7 +50,7 @@ ntfServerTests :: ATransport -> Spec ntfServerTests t = do describe "Notifications server protocol syntax" $ ntfSyntaxTests t describe "Notification subscriptions (NKEY)" $ testNotificationSubscription t createNtfQueueNKEY - describe "Notification subscriptions (NEW with ntf creds)" $ testNotificationSubscription t createNtfQueueNEW + -- describe "Notification subscriptions (NEW with ntf creds)" $ testNotificationSubscription t createNtfQueueNEW ntfSyntaxTests :: ATransport -> Spec ntfSyntaxTests (ATransport t) = do @@ -185,17 +185,18 @@ createNtfQueueNKEY h sPub nPub = do let rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey pure ((sId, rId, rKey, rcvDhSecret), nId, rcvNtfDhSecret) -createNtfQueueNEW :: CreateQueueFunc -createNtfQueueNEW h sPub nPub = do - g <- C.newRandom - (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g - (dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - (rcvNtfPubDhKey, rcvNtfPrivDhKey) <- atomically $ C.generateKeyPair g - let cmd = NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRMessaging Nothing)) (Just (NewNtfCreds nPub rcvNtfPubDhKey))) - Resp "abcd" NoEntity (IDS (QIK rId sId srvDh _sndSecure _linkId (Just (ServerNtfCreds nId rcvNtfSrvPubDhKey)))) <- - signSendRecv h rKey ("abcd", NoEntity, cmd) - let dhShared = C.dh' srvDh dhPriv - Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, KEY sPub) - (rId', rId) #== "same queue ID" - let rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey - pure ((sId, rId, rKey, dhShared), nId, rcvNtfDhSecret) +-- TODO [notifications] +-- createNtfQueueNEW :: CreateQueueFunc +-- createNtfQueueNEW h sPub nPub = do +-- g <- C.newRandom +-- (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g +-- (dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g +-- (rcvNtfPubDhKey, rcvNtfPrivDhKey) <- atomically $ C.generateKeyPair g +-- let cmd = NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRMessaging Nothing)) (Just (NewNtfCreds nPub rcvNtfPubDhKey))) +-- Resp "abcd" NoEntity (IDS (QIK rId sId srvDh _sndSecure _linkId (Just (ServerNtfCreds nId rcvNtfSrvPubDhKey)))) <- +-- signSendRecv h rKey ("abcd", NoEntity, cmd) +-- let dhShared = C.dh' srvDh dhPriv +-- Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, KEY sPub) +-- (rId', rId) #== "same queue ID" +-- let rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey +-- pure ((sId, rId, rKey, dhShared), nId, rcvNtfDhSecret) diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index f67564865..e39cb3449 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -178,7 +178,7 @@ deliverMessagesViaProxy proxyServ relayServ alg unsecuredMsgs securedMsgs = do -- prepare receiving queue (rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g (rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - SMP.QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc Nothing (rPub, rPriv) rdhPub (Just "correct") SMSubscribe (QRMessaging Nothing) Nothing + SMP.QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc Nothing (rPub, rPriv) rdhPub (Just "correct") SMSubscribe (QRMessaging Nothing) let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv -- get proxy session sess0 <- runExceptT' $ connectSMPProxiedRelay pc relayServ (Just "correct") diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index b52057b7c..00351befa 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -88,10 +88,10 @@ pattern Resp :: CorrId -> QueueId -> BrokerMsg -> SignedTransmission ErrorType B pattern Resp corrId queueId command <- (_, _, (corrId, queueId, Right command)) pattern New :: RcvPublicAuthKey -> RcvPublicDhKey -> Command 'Recipient -pattern New rPub dhPub = NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRMessaging Nothing)) Nothing) +pattern New rPub dhPub = NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRMessaging Nothing))) pattern Ids :: RecipientId -> SenderId -> RcvPublicDhKey -> BrokerMsg -pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh _sndSecure _linkId _srvNtfCreds) +pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh _sndSecure _linkId) pattern Msg :: MsgId -> MsgBody -> BrokerMsg pattern Msg msgId body <- MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body} @@ -264,7 +264,7 @@ testSndSecureProhibited = g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g (dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g - Resp "abcd" rId1 (Ids _rId sId _srvDh) <- signSendRecv r rKey ("abcd", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRContact Nothing)) Nothing)) + Resp "abcd" rId1 (Ids _rId sId _srvDh) <- signSendRecv r rKey ("abcd", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRContact Nothing)))) (rId1, NoEntity) #== "creates queue" (sPub, sKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g @@ -1064,14 +1064,14 @@ testInvQueueLinkData = (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g (dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g C.CbNonce corrId <- atomically $ C.randomCbNonce g - let sId = EntityId $ C.sha3_256 corrId + let sId = EntityId $ B.take 24 $ C.sha3_384 corrId ld = (EncDataBytes "fixed data", EncDataBytes "user data") qrd = QRMessaging $ Just (sId, ld) -- sender ID must be derived from corrId Resp "1" NoEntity (ERR (CMD PROHIBITED)) <- - signSendRecv r rKey ("1", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd) Nothing)) - Resp corrId' NoEntity (IDS (QIK rId sId' _srvDh (Just QMMessaging) (Just lnkId) Nothing)) <- - signSendRecv r rKey (corrId, NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd) Nothing)) + signSendRecv r rKey ("1", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd))) + Resp corrId' NoEntity (IDS (QIK rId sId' _srvDh (Just QMMessaging) (Just lnkId))) <- + signSendRecv r rKey (corrId, NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd))) (sId', sId) #== "should return the same sender ID" corrId' `shouldBe` CorrId corrId -- can't read link data with LGET @@ -1118,14 +1118,14 @@ testContactQueueLinkData = (dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g C.CbNonce corrId <- atomically $ C.randomCbNonce g lnkId <- EntityId <$> atomically (C.randomBytes 24 g) - let sId = EntityId $ C.sha3_256 corrId + let sId = EntityId $ B.take 24 $ C.sha3_384 corrId ld = (EncDataBytes "fixed data", EncDataBytes "user data") qrd = QRContact $ Just (lnkId, (sId, ld)) -- sender ID must be derived from corrId Resp "1" NoEntity (ERR (CMD PROHIBITED)) <- - signSendRecv r rKey ("1", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd) Nothing)) - Resp corrId' NoEntity (IDS (QIK rId sId' _srvDh (Just QMContact) (Just lnkId') Nothing)) <- - signSendRecv r rKey (corrId, NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd) Nothing)) + signSendRecv r rKey ("1", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd))) + Resp corrId' NoEntity (IDS (QIK rId sId' _srvDh (Just QMContact) (Just lnkId'))) <- + signSendRecv r rKey (corrId, NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd))) (lnkId', lnkId) #== "should return the same link ID" (sId', sId) #== "should return the same sender ID" corrId' `shouldBe` CorrId corrId @@ -1194,8 +1194,8 @@ serverSyntaxTests (ATransport t) = do describe "NEW" $ do it "no parameters" $ (sampleSig, "bcda", "", NEW_) >#> ("", "bcda", "", ERR $ CMD SYNTAX) it "many parameters" $ (sampleSig, "cdab", "", (NEW_, ' ', ('\x01', 'A'), samplePubKey, sampleDhPubKey)) >#> ("", "cdab", "", ERR $ CMD SYNTAX) - it "no signature" $ ("", "dabc", "", ((NEW_, ' ', samplePubKey, sampleDhPubKey), ('0', SMSubscribe, Just (QRMessaging Nothing), '0'))) >#> ("", "dabc", "", ERR $ CMD NO_AUTH) - it "queue ID" $ (sampleSig, "abcd", "12345678", ((NEW_, ' ', samplePubKey, sampleDhPubKey), ('0', SMSubscribe, Just (QRMessaging Nothing), '0'))) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH) + it "no signature" $ ("", "dabc", "", ((NEW_, ' ', samplePubKey, sampleDhPubKey), ('0', SMSubscribe, Just (QRMessaging Nothing)))) >#> ("", "dabc", "", ERR $ CMD NO_AUTH) + it "queue ID" $ (sampleSig, "abcd", "12345678", ((NEW_, ' ', samplePubKey, sampleDhPubKey), ('0', SMSubscribe, Just (QRMessaging Nothing)))) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH) describe "KEY" $ do it "valid syntax" $ (sampleSig, "bcda", "12345678", (KEY_, ' ', samplePubKey)) >#> ("", "bcda", "12345678", ERR AUTH) it "no parameters" $ (sampleSig, "cdab", "12345678", KEY_) >#> ("", "cdab", "12345678", ERR $ CMD SYNTAX)