diff --git a/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd b/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd index 75887dd0b..c155f7e11 100644 --- a/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd +++ b/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd @@ -9,8 +9,8 @@ sequenceDiagram A ->> S: SEND: QADD (R'): send address
of the new queue(s) S ->> B: MSG: QADD (R') B ->> R': SKEY: secure new queue - B ->> R': SEND: QTEST - R' ->> A: MSG: QTEST + B ->> R': SEND: QSEC: to agree shared secret + R' ->> A: MSG: QSEC A ->> R: DEL: delete the old queue B ->> R': SEND: send messages to the new queue R' ->> A: MSG: receive messages from the new queue diff --git a/rfcs/2024-06-14-fast-connection.md b/rfcs/2024-06-14-fast-connection.md index 000f0ef10..8e9d0b392 100644 --- a/rfcs/2024-06-14-fast-connection.md +++ b/rfcs/2024-06-14-fast-connection.md @@ -31,12 +31,18 @@ These are the proposed changes: 5. Accepting client will secure the messaging queue before sending the confirmation to it. 6. Initiating client will secure the messaging queue before sending the confirmation. -See [this sequence diagram](../protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd) for the updated handshake protocol. +See [this sequence diagram](../protocol/diagrams/duplex-messaging/duplex-creating-fast.mmd) for the updated handshake protocol. Changes to threat model: the attacker who compromised TLS and knows the queue address can block the connection, as the protocol no longer requires the recipient to decrypt the confirmation to secure the queue. Possibly, "fast connection" should be an option in Privacy & security settings. +## Queue rotation + +It is possible to design a faster connection rotation protocol that also uses only 2 instead of 4 messages, QADD and SMP confirmation (to agree per-queue encryption) - it would require to stop delivery to the old queue as soon as QSEC message is sent, without any additional test messages. + +It would also require sending a new message envelope with the DH key in the public header instead of the usual confirmation message or a normal message. + ## Implementation questions Currently we store received confirmations in the database, so that the client can confirm them. This becomes unnecessary. diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 29ea05ead..9fb39a9e0 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -1195,6 +1195,25 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do notify . SWITCH QDRcv SPSecured $ connectionStats conn' _ -> internalErr "ICQSecure: no switching queue found" _ -> internalErr "ICQSecure: queue address not found in connection" + ICQSndSecure sId -> + withServer $ \srv -> tryWithLock "ICQSndSecure" . withDuplexConn $ \(DuplexConnection cData rqs sqs) -> + case find (sameQueue (srv, sId)) sqs of + Just sq'@SndQueue {server, sndId, sndSecure, status, smpClientVersion, e2ePubKey = Just dhPublicKey, dbReplaceQueueId = Just replaceQId} -> + case find ((replaceQId ==) . dbQId) sqs of + Just sq1 -> when (status == New) $ do + secureSndQueue c sq' + withStore' c $ \db -> setSndQueueStatus db sq' Secured + let sq'' = (sq' :: SndQueue) {status = Secured} + queueAddress = SMPQueueAddress {smpServer = server, senderId = sndId, dhPublicKey, sndSecure} + qInfo = SMPQueueInfo {clientVersion = smpClientVersion, queueAddress} + -- sending QSEC to the new queue only, the old one will be removed if sent successfully + void . enqueueMessages c cData [sq''] SMP.noMsgFlags $ QSEC [qInfo] + sq1' <- withStore' c $ \db -> setSndSwitchStatus db sq1 $ Just SSSendingQSEC + let sqs' = updatedQs sq1' sqs + conn' = DuplexConnection cData rqs sqs' + notify . SWITCH QDSnd SPCompleted $ connectionStats conn' + _ -> internalErr "ICQSndSecure: no switching queue found" + _ -> internalErr "ICQSndSecure: queue address not found in connection" ICQDelete rId -> do withServer $ \srv -> tryWithLock "ICQDelete" . withDuplexConn $ \(DuplexConnection cData rqs sqs) -> do case removeQ (srv, rId) rqs of @@ -1393,6 +1412,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI AM_QCONT_ -> notifyDel msgId err AM_QADD_ -> qError msgId "QADD: AUTH" AM_QKEY_ -> qError msgId "QKEY: AUTH" + AM_QSEC_ -> qError msgId "QKEY: AUTH" AM_QUSE_ -> qError msgId "QUSE: AUTH" AM_QTEST_ -> qError msgId "QTEST: AUTH" AM_EREADY_ -> notifyDel msgId err @@ -1446,8 +1466,13 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI AM_QKEY_ -> do SomeConn _ conn <- withStore c (`getConn` connId) notify . SWITCH QDSnd SPConfirmed $ connectionStats conn + AM_QSEC_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QSEC_" $ completeConnSwitch "QSEC" SSSendingQSEC AM_QUSE_ -> pure () - AM_QTEST_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QTEST_" $ do + AM_QTEST_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QTEST_" $ completeConnSwitch "QTEST" SSSendingQTEST + AM_EREADY_ -> pure () + delMsgKeep (msgType == AM_A_MSG_) msgId + where + completeConnSwitch msgTag expectedStatus = do withStore' c $ \db -> setSndQueueStatus db sq Active SomeConn _ conn <- withStore c (`getConn` connId) case conn of @@ -1459,9 +1484,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI Just SndQueue {dbReplaceQueueId = Just replacedId, primary} -> -- second part of this condition is a sanity check because dbReplaceQueueId cannot point to the same queue, see switchConnection' case removeQP (\sq' -> dbQId sq' == replacedId && not (sameQueue addr sq')) sqs of - Nothing -> internalErr msgId "sent QTEST: queue not found in connection" + Nothing -> internalErr msgId $ "sent " <> msgTag <> ": queue not found in connection" Just (sq', sq'' : sqs') -> do - checkSQSwchStatus sq' SSSendingQTEST + checkSQSwchStatus sq' expectedStatus -- remove the delivery from the map to stop the thread when the delivery loop is complete atomically $ TM.delete (qAddress sq') $ smpDeliveryWorkers c withStore' c $ \db -> do @@ -1471,12 +1496,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI let sqs'' = sq'' :| sqs' conn' = DuplexConnection cData' rqs sqs'' notify . SWITCH QDSnd SPCompleted $ connectionStats conn' - _ -> internalErr msgId "sent QTEST: there is only one queue in connection" - _ -> internalErr msgId "sent QTEST: queue not in connection or not replacing another queue" - _ -> internalErr msgId "QTEST sent not in duplex connection" - AM_EREADY_ -> pure () - delMsgKeep (msgType == AM_A_MSG_) msgId - where + _ -> internalErr msgId $ "sent " <> msgTag <> ": there is only one queue in connection" + _ -> internalErr msgId $ "sent " <> msgTag <> ": queue not in connection or not replacing another queue" + _ -> internalErr msgId $ msgTag <> " sent not in duplex connection" setStatus status = do withStore' c $ \db -> do setSndQueueStatus db sq status @@ -2249,8 +2271,9 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) (DuplexConnection _ rqs _, Just replacedId) -> do when primary . withStore' c $ \db -> setRcvQueuePrimary db connId rq case find ((replacedId ==) . dbQId) rqs of - Just rq'@RcvQueue {server, rcvId} -> do - checkRQSwchStatus rq' RSSendingQUSE + Just rq'@RcvQueue {server, rcvId, rcvSwchStatus} -> do + unless (rcvSwchStatus == Just RSSendingQUSE || rcvSwchStatus == Just RSSendingQADD) $ + switchStatusError rq RSSendingQUSE rcvSwchStatus void $ withStore' c $ \db -> setRcvSwitchStatus db rq' $ Just RSReceivedMessage enqueueCommand c "" connId (Just server) $ AInternalCommand $ ICQDelete rcvId _ -> notify . ERR . AGENT $ A_QUEUE "replaced RcvQueue not found in connection" @@ -2271,6 +2294,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) A_QCONT addr -> qDuplexAckDel conn'' "QCONT" $ continueSending srvMsgId addr QADD qs -> qDuplexAckDel conn'' "QADD" $ qAddMsg srvMsgId qs QKEY qs -> qDuplexAckDel conn'' "QKEY" $ qKeyMsg srvMsgId qs + QSEC qs -> qDuplexAckDel conn'' "QSEC" $ qSecMsg srvMsgId qs QUSE qs -> qDuplexAckDel conn'' "QUSE" $ qUseMsg srvMsgId qs -- no action needed for QTEST -- any message in the new queue will mark it active and trigger deletion of the old queue @@ -2543,14 +2567,20 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) let (delSqs, keepSqs) = L.partition ((Just dbQueueId ==) . dbReplaceQId) sqs case L.nonEmpty keepSqs of Just sqs' -> do - (sq_@SndQueue {sndPublicKey}, dhPublicKey) <- lift $ newSndQueue userId connId qInfo + (sq_@SndQueue {sndId, sndPublicKey, sndSecure = sndSecure'}, dhPublicKey) <- lift $ newSndQueue userId connId qInfo sq2 <- withStore c $ \db -> do liftIO $ mapM_ (deleteConnSndQueue db connId) delSqs addConnSndQueue db connId (sq_ :: NewSndQueue) {primary = True, dbReplaceQueueId = Just dbQueueId} logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId <> " " <> logSecret (senderId queueAddress) - let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}} - void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', sndPublicKey)] - sq1 <- withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY + sq1 <- + if sndSecure' + then do + enqueueCommand c "" connId (Just $ qServer sq2) $ AInternalCommand $ ICQSndSecure sndId + withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSecuringQueue + else do + let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}} + void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', sndPublicKey)] + withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY let sqs'' = updatedQs sq1 sqs' <> [sq2] conn' = DuplexConnection cData' rqs sqs'' notify . SWITCH QDSnd SPStarted $ connectionStats conn' @@ -2578,6 +2608,24 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) where SMPQueueInfo cVer' SMPQueueAddress {smpServer, senderId, dhPublicKey} = qInfo + qSecMsg :: SMP.MsgId -> NonEmpty SMPQueueInfo -> Connection 'CDuplex -> AM () + qSecMsg srvMsgId (qInfo :| _) conn'@(DuplexConnection cData' rqs _) = do + when (ratchetSyncSendProhibited cData') $ throwE $ AGENT (A_QUEUE "ratchet is not synchronized") + clientVRange <- asks $ smpClientVRange . config + unless (qInfo `isCompatible` clientVRange) . throwE $ AGENT A_VERSION + case findRQ (smpServer, senderId) rqs of + Just rq'@RcvQueue {e2ePrivKey = dhPrivKey, smpClientVersion = cVer, status = status'} + | status' == New || status' == Confirmed -> do + checkRQSwchStatus rq RSSendingQADD + logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId <> " " <> logSecret senderId + let dhSecret = C.dh' dhPublicKey dhPrivKey + withStore' c $ \db -> setRcvQueueConfirmedE2E db rq' dhSecret $ min cVer cVer' + notify . SWITCH QDRcv SPCompleted $ connectionStats conn' + | otherwise -> qError "QSEC: queue already secured" + _ -> qError "QSEC: queue address not found in connection" + where + SMPQueueInfo cVer' SMPQueueAddress {smpServer, senderId, dhPublicKey} = qInfo + -- processed by queue sender -- mark queue as Secured and to start sending messages to it qUseMsg :: SMP.MsgId -> NonEmpty ((SMPServer, SMP.SenderId), Bool) -> Connection 'CDuplex -> AM () diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index b123fc1ec..796072b5f 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -526,16 +526,22 @@ instance FromJSON RcvSwitchStatus where data SndSwitchStatus = SSSendingQKEY | SSSendingQTEST + | SSSecuringQueue + | SSSendingQSEC deriving (Eq, Show) instance StrEncoding SndSwitchStatus where strEncode = \case SSSendingQKEY -> "sending_qkey" SSSendingQTEST -> "sending_qtest" + SSSecuringQueue -> "securing_queue" + SSSendingQSEC -> "sending_qsec" strP = A.takeTill (== ' ') >>= \case "sending_qkey" -> pure SSSendingQKEY "sending_qtest" -> pure SSSendingQTEST + "securing_queue" -> pure SSSecuringQueue + "sending_qsec" -> pure SSSendingQSEC _ -> fail "bad SndSwitchStatus" instance ToField SndSwitchStatus where toField = toField . decodeLatin1 . strEncode @@ -795,6 +801,7 @@ data AgentMessageType | AM_QCONT_ | AM_QADD_ | AM_QKEY_ + | AM_QSEC_ | AM_QUSE_ | AM_QTEST_ | AM_EREADY_ @@ -811,6 +818,7 @@ instance Encoding AgentMessageType where AM_QCONT_ -> "QC" AM_QADD_ -> "QA" AM_QKEY_ -> "QK" + AM_QSEC_ -> "QS" AM_QUSE_ -> "QU" AM_QTEST_ -> "QT" AM_EREADY_ -> "E" @@ -827,6 +835,7 @@ instance Encoding AgentMessageType where 'C' -> pure AM_QCONT_ 'A' -> pure AM_QADD_ 'K' -> pure AM_QKEY_ + 'S' -> pure AM_QSEC_ 'U' -> pure AM_QUSE_ 'T' -> pure AM_QTEST_ _ -> fail "bad AgentMessageType" @@ -849,6 +858,7 @@ agentMessageType = \case A_QCONT _ -> AM_QCONT_ QADD _ -> AM_QADD_ QKEY _ -> AM_QKEY_ + QSEC _ -> AM_QSEC_ QUSE _ -> AM_QUSE_ QTEST _ -> AM_QTEST_ EREADY _ -> AM_EREADY_ @@ -873,6 +883,7 @@ data AMsgType | A_QCONT_ | QADD_ | QKEY_ + | QSEC_ | QUSE_ | QTEST_ | EREADY_ @@ -886,6 +897,7 @@ instance Encoding AMsgType where A_QCONT_ -> "QC" QADD_ -> "QA" QKEY_ -> "QK" + QSEC_ -> "QS" QUSE_ -> "QU" QTEST_ -> "QT" EREADY_ -> "E" @@ -899,6 +911,7 @@ instance Encoding AMsgType where 'C' -> pure A_QCONT_ 'A' -> pure QADD_ 'K' -> pure QKEY_ + 'S' -> pure QSEC_ 'U' -> pure QUSE_ 'T' -> pure QTEST_ _ -> fail "bad AMsgType" @@ -921,6 +934,10 @@ data AMessage QADD (NonEmpty (SMPQueueUri, Maybe SndQAddr)) | -- key to secure the added queues and agree e2e encryption key (sent by sender) QKEY (NonEmpty (SMPQueueInfo, SndPublicAuthKey)) + | -- sent by the sender who secured the queue with SKEY (SMP protocol v9). + -- This message is needed to agree shared secret - it completes switching. + -- This message requires a new envelope that is sent together with public DH key. + QSEC (NonEmpty SMPQueueInfo) | -- inform that the queues are ready to use (sent by recipient) QUSE (NonEmpty (SndQAddr, Bool)) | -- sent by the sender to test new queues and to complete switching @@ -977,6 +994,7 @@ instance Encoding AMessage where A_QCONT addr -> smpEncode (A_QCONT_, addr) QADD qs -> smpEncode (QADD_, qs) QKEY qs -> smpEncode (QKEY_, qs) + QSEC qs -> smpEncode (QSEC_, qs) QUSE qs -> smpEncode (QUSE_, qs) QTEST qs -> smpEncode (QTEST_, qs) EREADY lastDecryptedMsgId -> smpEncode (EREADY_, lastDecryptedMsgId) @@ -989,6 +1007,7 @@ instance Encoding AMessage where A_QCONT_ -> A_QCONT <$> smpP QADD_ -> QADD <$> smpP QKEY_ -> QKEY <$> smpP + QSEC_ -> QSEC <$> smpP QUSE_ -> QUSE <$> smpP QTEST_ -> QTEST <$> smpP EREADY_ -> EREADY <$> smpP diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index baec2ef93..c48a4c632 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -382,6 +382,7 @@ data InternalCommand | ICDeleteConn | ICDeleteRcvQueue SMP.RecipientId | ICQSecure SMP.RecipientId SMP.SndPublicAuthKey + | ICQSndSecure SMP.SenderId | ICQDelete SMP.RecipientId data InternalCommandTag @@ -392,6 +393,7 @@ data InternalCommandTag | ICDeleteConn_ | ICDeleteRcvQueue_ | ICQSecure_ + | ICQSndSecure_ | ICQDelete_ deriving (Show) @@ -404,6 +406,7 @@ instance StrEncoding InternalCommand where ICDeleteConn -> strEncode ICDeleteConn_ ICDeleteRcvQueue rId -> strEncode (ICDeleteRcvQueue_, rId) ICQSecure rId senderKey -> strEncode (ICQSecure_, rId, senderKey) + ICQSndSecure sId -> strEncode (ICQSndSecure_, sId) ICQDelete rId -> strEncode (ICQDelete_, rId) strP = strP >>= \case @@ -414,6 +417,7 @@ instance StrEncoding InternalCommand where ICDeleteConn_ -> pure ICDeleteConn ICDeleteRcvQueue_ -> ICDeleteRcvQueue <$> _strP ICQSecure_ -> ICQSecure <$> _strP <*> _strP + ICQSndSecure_ -> ICQSndSecure <$> _strP ICQDelete_ -> ICQDelete <$> _strP instance StrEncoding InternalCommandTag where @@ -425,6 +429,7 @@ instance StrEncoding InternalCommandTag where ICDeleteConn_ -> "DELETE_CONN" ICDeleteRcvQueue_ -> "DELETE_RCV_QUEUE" ICQSecure_ -> "QSECURE" + ICQSndSecure_ -> "QSND_SECURE" ICQDelete_ -> "QDELETE" strP = A.takeTill (== ' ') >>= \case @@ -435,6 +440,7 @@ instance StrEncoding InternalCommandTag where "DELETE_CONN" -> pure ICDeleteConn_ "DELETE_RCV_QUEUE" -> pure ICDeleteRcvQueue_ "QSECURE" -> pure ICQSecure_ + "QSND_SECURE" -> pure ICQSndSecure_ "QDELETE" -> pure ICQDelete_ _ -> fail "bad InternalCommandTag" @@ -452,6 +458,7 @@ internalCmdTag = \case ICDeleteConn -> ICDeleteConn_ ICDeleteRcvQueue {} -> ICDeleteRcvQueue_ ICQSecure {} -> ICQSecure_ + ICQSndSecure {} -> ICQSndSecure_ ICQDelete _ -> ICQDelete_ -- * Confirmation types