Skip to content

Commit b48b86f

Browse files
committed
agent: attempt at faster queue rotation (does not work)
1 parent a99ce61 commit b48b86f

File tree

6 files changed

+98
-81
lines changed

6 files changed

+98
-81
lines changed

protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd

-63
This file was deleted.

protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ sequenceDiagram
99
A ->> S: SEND: QADD (R'): send address<br>of the new queue(s)
1010
S ->> B: MSG: QADD (R')
1111
B ->> R': SKEY: secure new queue
12-
B ->> R': SEND: QTEST
13-
R' ->> A: MSG: QTEST
12+
B ->> R': SEND: QSEC: to agree shared secret
13+
R' ->> A: MSG: QSEC
1414
A ->> R: DEL: delete the old queue
1515
B ->> R': SEND: send messages to the new queue
1616
R' ->> A: MSG: receive messages from the new queue

rfcs/2024-06-14-fast-connection.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,18 @@ These are the proposed changes:
3131
5. Accepting client will secure the messaging queue before sending the confirmation to it.
3232
6. Initiating client will secure the messaging queue before sending the confirmation.
3333

34-
See [this sequence diagram](../protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd) for the updated handshake protocol.
34+
See [this sequence diagram](../protocol/diagrams/duplex-messaging/duplex-creating-fast.mmd) for the updated handshake protocol.
3535

3636
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.
3737

3838
Possibly, "fast connection" should be an option in Privacy & security settings.
3939

40+
## Queue rotation
41+
42+
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.
43+
44+
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.
45+
4046
## Implementation questions
4147

4248
Currently we store received confirmations in the database, so that the client can confirm them. This becomes unnecessary.

src/Simplex/Messaging/Agent.hs

+63-15
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,25 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do
11941194
notify . SWITCH QDRcv SPSecured $ connectionStats conn'
11951195
_ -> internalErr "ICQSecure: no switching queue found"
11961196
_ -> internalErr "ICQSecure: queue address not found in connection"
1197+
ICQSndSecure sId ->
1198+
withServer $ \srv -> tryWithLock "ICQSndSecure" . withDuplexConn $ \(DuplexConnection cData rqs sqs) ->
1199+
case find (sameQueue (srv, sId)) sqs of
1200+
Just sq'@SndQueue {server, sndId, sndSecure, status, smpClientVersion, e2ePubKey = Just dhPublicKey, dbReplaceQueueId = Just replaceQId} ->
1201+
case find ((replaceQId ==) . dbQId) sqs of
1202+
Just sq1 -> when (status == New) $ do
1203+
secureSndQueue c sq'
1204+
withStore' c $ \db -> setSndQueueStatus db sq' Secured
1205+
let sq'' = (sq' :: SndQueue) {status = Secured}
1206+
queueAddress = SMPQueueAddress {smpServer = server, senderId = sndId, dhPublicKey, sndSecure}
1207+
qInfo = SMPQueueInfo {clientVersion = smpClientVersion, queueAddress}
1208+
-- sending QSEC to the new queue only, the old one will be removed if sent successfully
1209+
void . enqueueMessages c cData [sq''] SMP.noMsgFlags $ QSEC [qInfo]
1210+
sq1' <- withStore' c $ \db -> setSndSwitchStatus db sq1 $ Just SSSendingQSEC
1211+
let sqs' = updatedQs sq1' sqs
1212+
conn' = DuplexConnection cData rqs sqs'
1213+
notify . SWITCH QDSnd SPCompleted $ connectionStats conn'
1214+
_ -> internalErr "ICQSndSecure: no switching queue found"
1215+
_ -> internalErr "ICQSndSecure: queue address not found in connection"
11971216
ICQDelete rId -> do
11981217
withServer $ \srv -> tryWithLock "ICQDelete" . withDuplexConn $ \(DuplexConnection cData rqs sqs) -> do
11991218
case removeQ (srv, rId) rqs of
@@ -1392,6 +1411,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
13921411
AM_QCONT_ -> notifyDel msgId err
13931412
AM_QADD_ -> qError msgId "QADD: AUTH"
13941413
AM_QKEY_ -> qError msgId "QKEY: AUTH"
1414+
AM_QSEC_ -> qError msgId "QKEY: AUTH"
13951415
AM_QUSE_ -> qError msgId "QUSE: AUTH"
13961416
AM_QTEST_ -> qError msgId "QTEST: AUTH"
13971417
AM_EREADY_ -> notifyDel msgId err
@@ -1445,8 +1465,13 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
14451465
AM_QKEY_ -> do
14461466
SomeConn _ conn <- withStore c (`getConn` connId)
14471467
notify . SWITCH QDSnd SPConfirmed $ connectionStats conn
1468+
AM_QSEC_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QSEC_" $ completeConnSwitch "QSEC" SSSendingQSEC
14481469
AM_QUSE_ -> pure ()
1449-
AM_QTEST_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QTEST_" $ do
1470+
AM_QTEST_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QTEST_" $ completeConnSwitch "QTEST" SSSendingQTEST
1471+
AM_EREADY_ -> pure ()
1472+
delMsgKeep (msgType == AM_A_MSG_) msgId
1473+
where
1474+
completeConnSwitch msgTag expectedStatus = do
14501475
withStore' c $ \db -> setSndQueueStatus db sq Active
14511476
SomeConn _ conn <- withStore c (`getConn` connId)
14521477
case conn of
@@ -1458,9 +1483,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
14581483
Just SndQueue {dbReplaceQueueId = Just replacedId, primary} ->
14591484
-- second part of this condition is a sanity check because dbReplaceQueueId cannot point to the same queue, see switchConnection'
14601485
case removeQP (\sq' -> dbQId sq' == replacedId && not (sameQueue addr sq')) sqs of
1461-
Nothing -> internalErr msgId "sent QTEST: queue not found in connection"
1486+
Nothing -> internalErr msgId $ "sent " <> msgTag <> ": queue not found in connection"
14621487
Just (sq', sq'' : sqs') -> do
1463-
checkSQSwchStatus sq' SSSendingQTEST
1488+
checkSQSwchStatus sq' expectedStatus
14641489
-- remove the delivery from the map to stop the thread when the delivery loop is complete
14651490
atomically $ TM.delete (qAddress sq') $ smpDeliveryWorkers c
14661491
withStore' c $ \db -> do
@@ -1470,12 +1495,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
14701495
let sqs'' = sq'' :| sqs'
14711496
conn' = DuplexConnection cData' rqs sqs''
14721497
notify . SWITCH QDSnd SPCompleted $ connectionStats conn'
1473-
_ -> internalErr msgId "sent QTEST: there is only one queue in connection"
1474-
_ -> internalErr msgId "sent QTEST: queue not in connection or not replacing another queue"
1475-
_ -> internalErr msgId "QTEST sent not in duplex connection"
1476-
AM_EREADY_ -> pure ()
1477-
delMsgKeep (msgType == AM_A_MSG_) msgId
1478-
where
1498+
_ -> internalErr msgId $ "sent " <> msgTag <> ": there is only one queue in connection"
1499+
_ -> internalErr msgId $ "sent " <> msgTag <> ": queue not in connection or not replacing another queue"
1500+
_ -> internalErr msgId $ msgTag <> " sent not in duplex connection"
14791501
setStatus status = do
14801502
withStore' c $ \db -> do
14811503
setSndQueueStatus db sq status
@@ -2249,8 +2271,9 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
22492271
(DuplexConnection _ rqs _, Just replacedId) -> do
22502272
when primary . withStore' c $ \db -> setRcvQueuePrimary db connId rq
22512273
case find ((replacedId ==) . dbQId) rqs of
2252-
Just rq'@RcvQueue {server, rcvId} -> do
2253-
checkRQSwchStatus rq' RSSendingQUSE
2274+
Just rq'@RcvQueue {server, rcvId, rcvSwchStatus} -> do
2275+
unless (rcvSwchStatus == Just RSSendingQUSE || rcvSwchStatus == Just RSSendingQADD) $
2276+
switchStatusError rq RSSendingQUSE rcvSwchStatus
22542277
void $ withStore' c $ \db -> setRcvSwitchStatus db rq' $ Just RSReceivedMessage
22552278
enqueueCommand c "" connId (Just server) $ AInternalCommand $ ICQDelete rcvId
22562279
_ -> notify . ERR . AGENT $ A_QUEUE "replaced RcvQueue not found in connection"
@@ -2271,6 +2294,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
22712294
A_QCONT addr -> qDuplexAckDel conn'' "QCONT" $ continueSending srvMsgId addr
22722295
QADD qs -> qDuplexAckDel conn'' "QADD" $ qAddMsg srvMsgId qs
22732296
QKEY qs -> qDuplexAckDel conn'' "QKEY" $ qKeyMsg srvMsgId qs
2297+
QSEC qs -> qDuplexAckDel conn'' "QSEC" $ qSecMsg srvMsgId qs
22742298
QUSE qs -> qDuplexAckDel conn'' "QUSE" $ qUseMsg srvMsgId qs
22752299
-- no action needed for QTEST
22762300
-- 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)
25432567
let (delSqs, keepSqs) = L.partition ((Just dbQueueId ==) . dbReplaceQId) sqs
25442568
case L.nonEmpty keepSqs of
25452569
Just sqs' -> do
2546-
(sq_@SndQueue {sndPublicKey}, dhPublicKey) <- lift $ newSndQueue userId connId qInfo
2570+
(sq_@SndQueue {sndId, sndPublicKey, sndSecure = sndSecure'}, dhPublicKey) <- lift $ newSndQueue userId connId qInfo
25472571
sq2 <- withStore c $ \db -> do
25482572
liftIO $ mapM_ (deleteConnSndQueue db connId) delSqs
25492573
addConnSndQueue db connId (sq_ :: NewSndQueue) {primary = True, dbReplaceQueueId = Just dbQueueId}
25502574
logServer "<--" c srv rId $ "MSG <QADD>:" <> logSecret srvMsgId <> " " <> logSecret (senderId queueAddress)
2551-
let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}}
2552-
void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', sndPublicKey)]
2553-
sq1 <- withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY
2575+
sq1 <-
2576+
if sndSecure'
2577+
then do
2578+
enqueueCommand c "" connId (Just $ qServer sq2) $ AInternalCommand $ ICQSndSecure sndId
2579+
withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSecuringQueue
2580+
else do
2581+
let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}}
2582+
void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', sndPublicKey)]
2583+
withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY
25542584
let sqs'' = updatedQs sq1 sqs' <> [sq2]
25552585
conn' = DuplexConnection cData' rqs sqs''
25562586
notify . SWITCH QDSnd SPStarted $ connectionStats conn'
@@ -2578,6 +2608,24 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
25782608
where
25792609
SMPQueueInfo cVer' SMPQueueAddress {smpServer, senderId, dhPublicKey} = qInfo
25802610

2611+
qSecMsg :: SMP.MsgId -> NonEmpty SMPQueueInfo -> Connection 'CDuplex -> AM ()
2612+
qSecMsg srvMsgId (qInfo :| _) conn'@(DuplexConnection cData' rqs _) = do
2613+
when (ratchetSyncSendProhibited cData') $ throwE $ AGENT (A_QUEUE "ratchet is not synchronized")
2614+
clientVRange <- asks $ smpClientVRange . config
2615+
unless (qInfo `isCompatible` clientVRange) . throwE $ AGENT A_VERSION
2616+
case findRQ (smpServer, senderId) rqs of
2617+
Just rq'@RcvQueue {e2ePrivKey = dhPrivKey, smpClientVersion = cVer, status = status'}
2618+
| status' == New || status' == Confirmed -> do
2619+
checkRQSwchStatus rq RSSendingQADD
2620+
logServer "<--" c srv rId $ "MSG <QSEC>:" <> logSecret srvMsgId <> " " <> logSecret senderId
2621+
let dhSecret = C.dh' dhPublicKey dhPrivKey
2622+
withStore' c $ \db -> setRcvQueueConfirmedE2E db rq' dhSecret $ min cVer cVer'
2623+
notify . SWITCH QDRcv SPCompleted $ connectionStats conn'
2624+
| otherwise -> qError "QSEC: queue already secured"
2625+
_ -> qError "QSEC: queue address not found in connection"
2626+
where
2627+
SMPQueueInfo cVer' SMPQueueAddress {smpServer, senderId, dhPublicKey} = qInfo
2628+
25812629
-- processed by queue sender
25822630
-- mark queue as Secured and to start sending messages to it
25832631
qUseMsg :: SMP.MsgId -> NonEmpty ((SMPServer, SMP.SenderId), Bool) -> Connection 'CDuplex -> AM ()

src/Simplex/Messaging/Agent/Protocol.hs

+19
Original file line numberDiff line numberDiff line change
@@ -526,16 +526,22 @@ instance FromJSON RcvSwitchStatus where
526526
data SndSwitchStatus
527527
= SSSendingQKEY
528528
| SSSendingQTEST
529+
| SSSecuringQueue
530+
| SSSendingQSEC
529531
deriving (Eq, Show)
530532

531533
instance StrEncoding SndSwitchStatus where
532534
strEncode = \case
533535
SSSendingQKEY -> "sending_qkey"
534536
SSSendingQTEST -> "sending_qtest"
537+
SSSecuringQueue -> "securing_queue"
538+
SSSendingQSEC -> "sending_qsec"
535539
strP =
536540
A.takeTill (== ' ') >>= \case
537541
"sending_qkey" -> pure SSSendingQKEY
538542
"sending_qtest" -> pure SSSendingQTEST
543+
"securing_queue" -> pure SSSecuringQueue
544+
"sending_qsec" -> pure SSSendingQSEC
539545
_ -> fail "bad SndSwitchStatus"
540546

541547
instance ToField SndSwitchStatus where toField = toField . decodeLatin1 . strEncode
@@ -795,6 +801,7 @@ data AgentMessageType
795801
| AM_QCONT_
796802
| AM_QADD_
797803
| AM_QKEY_
804+
| AM_QSEC_
798805
| AM_QUSE_
799806
| AM_QTEST_
800807
| AM_EREADY_
@@ -811,6 +818,7 @@ instance Encoding AgentMessageType where
811818
AM_QCONT_ -> "QC"
812819
AM_QADD_ -> "QA"
813820
AM_QKEY_ -> "QK"
821+
AM_QSEC_ -> "QS"
814822
AM_QUSE_ -> "QU"
815823
AM_QTEST_ -> "QT"
816824
AM_EREADY_ -> "E"
@@ -827,6 +835,7 @@ instance Encoding AgentMessageType where
827835
'C' -> pure AM_QCONT_
828836
'A' -> pure AM_QADD_
829837
'K' -> pure AM_QKEY_
838+
'S' -> pure AM_QSEC_
830839
'U' -> pure AM_QUSE_
831840
'T' -> pure AM_QTEST_
832841
_ -> fail "bad AgentMessageType"
@@ -849,6 +858,7 @@ agentMessageType = \case
849858
A_QCONT _ -> AM_QCONT_
850859
QADD _ -> AM_QADD_
851860
QKEY _ -> AM_QKEY_
861+
QSEC _ -> AM_QSEC_
852862
QUSE _ -> AM_QUSE_
853863
QTEST _ -> AM_QTEST_
854864
EREADY _ -> AM_EREADY_
@@ -873,6 +883,7 @@ data AMsgType
873883
| A_QCONT_
874884
| QADD_
875885
| QKEY_
886+
| QSEC_
876887
| QUSE_
877888
| QTEST_
878889
| EREADY_
@@ -886,6 +897,7 @@ instance Encoding AMsgType where
886897
A_QCONT_ -> "QC"
887898
QADD_ -> "QA"
888899
QKEY_ -> "QK"
900+
QSEC_ -> "QS"
889901
QUSE_ -> "QU"
890902
QTEST_ -> "QT"
891903
EREADY_ -> "E"
@@ -899,6 +911,7 @@ instance Encoding AMsgType where
899911
'C' -> pure A_QCONT_
900912
'A' -> pure QADD_
901913
'K' -> pure QKEY_
914+
'S' -> pure QSEC_
902915
'U' -> pure QUSE_
903916
'T' -> pure QTEST_
904917
_ -> fail "bad AMsgType"
@@ -921,6 +934,10 @@ data AMessage
921934
QADD (NonEmpty (SMPQueueUri, Maybe SndQAddr))
922935
| -- key to secure the added queues and agree e2e encryption key (sent by sender)
923936
QKEY (NonEmpty (SMPQueueInfo, SndPublicAuthKey))
937+
| -- sent by the sender who secured the queue with SKEY (SMP protocol v9).
938+
-- This message is needed to agree shared secret - it completes switching.
939+
-- This message requires a new envelope that is sent together with public DH key.
940+
QSEC (NonEmpty SMPQueueInfo)
924941
| -- inform that the queues are ready to use (sent by recipient)
925942
QUSE (NonEmpty (SndQAddr, Bool))
926943
| -- sent by the sender to test new queues and to complete switching
@@ -977,6 +994,7 @@ instance Encoding AMessage where
977994
A_QCONT addr -> smpEncode (A_QCONT_, addr)
978995
QADD qs -> smpEncode (QADD_, qs)
979996
QKEY qs -> smpEncode (QKEY_, qs)
997+
QSEC qs -> smpEncode (QSEC_, qs)
980998
QUSE qs -> smpEncode (QUSE_, qs)
981999
QTEST qs -> smpEncode (QTEST_, qs)
9821000
EREADY lastDecryptedMsgId -> smpEncode (EREADY_, lastDecryptedMsgId)
@@ -989,6 +1007,7 @@ instance Encoding AMessage where
9891007
A_QCONT_ -> A_QCONT <$> smpP
9901008
QADD_ -> QADD <$> smpP
9911009
QKEY_ -> QKEY <$> smpP
1010+
QSEC_ -> QSEC <$> smpP
9921011
QUSE_ -> QUSE <$> smpP
9931012
QTEST_ -> QTEST <$> smpP
9941013
EREADY_ -> EREADY <$> smpP

0 commit comments

Comments
 (0)