core: add agreed connection version field (#3881)

* core: add agreed connection version field

* fix

* progress

* use pqSupport and version to decide compression in messages

* pass version to encodeConnInfoPQ

* update pq enable/disable api

* remove TestConfig

* update nix dependencies

* update texts

* corrections

* create e2ee info items when connection switches from off to on first time

* corrections

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* comment

* increase test timeout

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
Evgeny Poberezkin 2024-03-10 11:31:14 +00:00 committed by GitHub
parent 93d56a25bc
commit 60a73a539e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 380 additions and 259 deletions

View file

@ -270,7 +270,7 @@ jobs:
- name: Unix test
if: matrix.os != 'windows-latest'
timeout-minutes: 30
timeout-minutes: 40
shell: bash
run: cabal test --test-show-details=direct

View file

@ -258,15 +258,15 @@ func apiSetEncryptLocalFiles(_ enable: Bool) throws {
throw r
}
func apiSetPQEnabled(_ enable: Bool) throws {
let r = chatSendCmdSync(.apiSetPQEnabled(enable: enable))
func apiSetPQEncryption(_ enable: Bool) throws {
let r = chatSendCmdSync(.apiSetPQEncryption(enable: enable))
if case .cmdOk = r { return }
throw r
}
func apiAllowContactPQ(_ contactId: Int64) async throws -> Contact {
let r = await chatSendCmd(.apiAllowContactPQ(contactId: contactId))
if case let .contactPQAllowed(_, contact) = r { return contact }
func apiSetContactPQ(_ contactId: Int64, _ enable: Bool) async throws -> Contact {
let r = await chatSendCmd(.apiSetContactPQ(contactId: contactId, enable: enable))
if case let .contactPQAllowed(_, contact, _) = r { return contact }
throw r
}
@ -1256,7 +1256,7 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
try apiSetPQEnabled(pqExperimentalEnabledDefault.get())
try apiSetPQEncryption(pqExperimentalEnabledDefault.get())
m.chatInitialized = true
m.currentUser = try apiGetActiveUser()
if m.currentUser == nil {

View file

@ -171,15 +171,15 @@ struct ChatInfoView: View {
if pqExperimentalEnabled,
let conn = contact.activeConn {
Section {
infoRow(Text(String("PQ E2E encryption")), conn.connPQEnabled ? "Enabled" : "Disabled")
if !conn.pqSupport {
infoRow(Text(String("E2E encryption")), conn.connPQEnabled ? "Quantum resistant" : "Standard")
if !conn.pqEncryption {
allowPQButton()
}
} header: {
Text(String("Post-quantum E2E encryption"))
Text(String("Quantum resistant E2E encryption"))
} footer: {
if !conn.pqSupport {
Text(String("After allowing post-quantum encryption, it will be enabled after several messages if your contact also allows it."))
if !conn.pqEncryption {
Text(String("After allowing quantum resistant encryption, it will be enabled after several messages if your contact also allows it."))
}
}
}
@ -576,14 +576,14 @@ struct ChatInfoView: View {
private func allowContactPQEncryption() {
Task {
do {
let ct = try await apiAllowContactPQ(contact.apiId)
let ct = try await apiSetContactPQ(contact.apiId, true)
contact = ct
await MainActor.run {
chatModel.updateContact(contact)
dismiss()
}
} catch let error {
logger.error("allowContactPQEncryption apiAllowContactPQ error: \(responseError(error))")
logger.error("allowContactPQEncryption apiSetContactPQ error: \(responseError(error))")
let a = getErrorAlert(error, "Error allowing contact PQ encryption")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
@ -594,8 +594,8 @@ struct ChatInfoView: View {
func allowContactPQEncryptionAlert() -> Alert {
Alert(
title: Text(String("Allow post-quantum encryption?")),
message: Text(String("This is an experimental feature, it is not recommended to enable it for high importance communications. It may result in connection errors!")),
title: Text(String("Allow quantum resistant encryption?")),
message: Text(String("This is an experimental feature, it is not recommended to enable it for important chats.")),
primaryButton: .destructive(Text(String("Allow")), action: allowContactPQEncryption),
secondaryButton: .cancel()
)

View file

@ -64,10 +64,10 @@ struct DeveloperView: View {
private func setPQExperimentalEnabled(_ enable: Bool) {
do {
try apiSetPQEnabled(enable)
try apiSetPQEncryption(enable)
} catch let error {
let err = responseError(error)
logger.error("apiSetPQEnabled \(err)")
logger.error("apiSetPQEncryption \(err)")
}
}
}

View file

@ -32,8 +32,8 @@ public enum ChatCommand {
case setTempFolder(tempFolder: String)
case setFilesFolder(filesFolder: String)
case apiSetEncryptLocalFiles(enable: Bool)
case apiSetPQEnabled(enable: Bool)
case apiAllowContactPQ(contactId: Int64)
case apiSetPQEncryption(enable: Bool)
case apiSetContactPQ(contactId: Int64, enable: Bool)
case apiExportArchive(config: ArchiveConfig)
case apiImportArchive(config: ArchiveConfig)
case apiDeleteStorage
@ -164,8 +164,8 @@ public enum ChatCommand {
case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)"
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))"
case let .apiSetPQEnabled(enable): return "/_pq \(onOff(enable))"
case let .apiAllowContactPQ(contactId): return "/_pq allow \(contactId)"
case let .apiSetPQEncryption(enable): return "/pq \(onOff(enable))"
case let .apiSetContactPQ(contactId, enable): return "/_pq @\(contactId) \(onOff(enable))"
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
case .apiDeleteStorage: return "/_db delete"
@ -310,8 +310,8 @@ public enum ChatCommand {
case .setTempFolder: return "setTempFolder"
case .setFilesFolder: return "setFilesFolder"
case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles"
case .apiSetPQEnabled: return "apiSetPQEnabled"
case .apiAllowContactPQ: return "apiAllowContactPQ"
case .apiSetPQEncryption: return "apiSetPQEncryption"
case .apiSetContactPQ: return "apiSetContactPQ"
case .apiExportArchive: return "apiExportArchive"
case .apiImportArchive: return "apiImportArchive"
case .apiDeleteStorage: return "apiDeleteStorage"
@ -624,7 +624,7 @@ public enum ChatResponse: Decodable, Error {
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason)
// pq
case contactPQAllowed(user: UserRef, contact: Contact)
case contactPQAllowed(user: UserRef, contact: Contact, pqEncryption: Bool)
case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool)
// misc
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
@ -926,7 +926,7 @@ public enum ChatResponse: Decodable, Error {
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)"
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
case .remoteCtrlStopped: return noDetails
case let .contactPQAllowed(u, contact): return withUser(u, "contact: \(String(describing: contact))")
case let .contactPQAllowed(u, contact, pqEncryption): return withUser(u, "contact: \(String(describing: contact))\npqEncryption: \(pqEncryption)")
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
case .cmdOk: return noDetails

View file

@ -3625,9 +3625,9 @@ public enum RcvConnEvent: Decodable {
return NSLocalizedString("security code changed", comment: "chat item text")
case let .pqEnabled(enabled):
if enabled {
return NSLocalizedString("enabled post-quantum encryption", comment: "chat item text")
return NSLocalizedString("quantum resistant e2e encryption", comment: "chat item text")
} else {
return NSLocalizedString("disabled post-quantum encryption", comment: "chat item text")
return NSLocalizedString("standard end-to-end encryption", comment: "chat item text")
}
}
}
@ -3672,9 +3672,9 @@ public enum SndConnEvent: Decodable {
return ratchetSyncStatusToText(syncStatus)
case let .pqEnabled(enabled):
if enabled {
return NSLocalizedString("enabled post-quantum encryption", comment: "chat item text")
return NSLocalizedString("quantum resistant e2e encryption", comment: "chat item text")
} else {
return NSLocalizedString("disabled post-quantum encryption", comment: "chat item text")
return NSLocalizedString("standard end-to-end encryption", comment: "chat item text")
}
}
}

View file

@ -635,12 +635,12 @@ object ChatController {
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
suspend fun apiSetPQEnabled(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetPQEnabled(enable))
suspend fun apiSetPQEncryption(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetPQEncryption(enable))
suspend fun apiAllowContactPQ(rh: Long?, contactId: Long): Contact? {
val r = sendCmd(rh, CC.ApiAllowContactPQ(contactId))
suspend fun apiSetContactPQ(rh: Long?, contactId: Long, enable: Boolean): Contact? {
val r = sendCmd(rh, CC.ApiSetContactPQ(contactId, enable))
if (r is CR.ContactPQAllowed) return r.contact
apiErrorAlert("apiAllowContactPQ", "Error allowing contact PQ", r)
apiErrorAlert("apiSetContactPQ", "Error allowing contact PQ", r)
return null
}
@ -2289,8 +2289,8 @@ sealed class CC {
class SetFilesFolder(val filesFolder: String): CC()
class SetRemoteHostsFolder(val remoteHostsFolder: String): CC()
class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
class ApiSetPQEnabled(val enable: Boolean): CC()
class ApiAllowContactPQ(val contactId: Long): CC()
class ApiSetPQEncryption(val enable: Boolean): CC()
class ApiSetContactPQ(val contactId: Long, val enable: Boolean): CC()
class ApiExportArchive(val config: ArchiveConfig): CC()
class ApiImportArchive(val config: ArchiveConfig): CC()
class ApiDeleteStorage: CC()
@ -2420,8 +2420,8 @@ sealed class CC {
is SetFilesFolder -> "/_files_folder $filesFolder"
is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder"
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
is ApiSetPQEnabled -> "/_pq ${onOff(enable)}"
is ApiAllowContactPQ -> "/_pq allow $contactId"
is ApiSetPQEncryption -> "/pq ${onOff(enable)}"
is ApiSetContactPQ -> "/_pq @$contactId ${onOff(enable)}"
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
is ApiDeleteStorage -> "/_db delete"
@ -2556,8 +2556,8 @@ sealed class CC {
is SetFilesFolder -> "setFilesFolder"
is SetRemoteHostsFolder -> "setRemoteHostsFolder"
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
is ApiSetPQEnabled -> "apiSetPQEnabled"
is ApiAllowContactPQ -> "apiAllowContactPQ"
is ApiSetPQEncryption -> "apiSetPQEncryption"
is ApiSetContactPQ -> "apiSetContactPQ"
is ApiExportArchive -> "apiExportArchive"
is ApiImportArchive -> "apiImportArchive"
is ApiDeleteStorage -> "apiDeleteStorage"
@ -4024,7 +4024,7 @@ sealed class CR {
@Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrl: RemoteCtrlInfo): CR()
@Serializable @SerialName("remoteCtrlStopped") class RemoteCtrlStopped(val rcsState: RemoteCtrlSessionState, val rcStopReason: RemoteCtrlStopReason): CR()
// pq
@Serializable @SerialName("contactPQAllowed") class ContactPQAllowed(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("contactPQAllowed") class ContactPQAllowed(val user: UserRef, val contact: Contact, val pqEncryption: Boolean): CR()
@Serializable @SerialName("contactPQEnabled") class ContactPQEnabled(val user: UserRef, val contact: Contact, val pqEnabled: Boolean): CR()
// misc
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR()
@ -4342,7 +4342,7 @@ sealed class CR {
"\nsessionCode: $sessionCode"
is RemoteCtrlConnected -> json.encodeToString(remoteCtrl)
is RemoteCtrlStopped -> noDetails()
is ContactPQAllowed -> withUser(user, "contact: ${contact.id}")
is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption")
is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled")
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
"chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" +

View file

@ -92,7 +92,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
}
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
controller.apiSetPQEnabled(controller.appPrefs.pqExperimentalEnabled.get())
controller.apiSetPQEncryption(controller.appPrefs.pqExperimentalEnabled.get())
// If we migrated successfully means previous re-encryption process on database level finished successfully too
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
val user = chatController.apiGetActiveUser(null)

View file

@ -143,7 +143,7 @@ fun ChatInfoView(
allowContactPQ = {
showAllowContactPQAlert(allowContactPQ = {
withBGApi {
val ct = chatModel.controller.apiAllowContactPQ(chatRh, contact.contactId)
val ct = chatModel.controller.apiSetContactPQ(chatRh, contact.contactId, true)
if (ct != null) {
chatModel.updateContact(chatRh, contact)
}
@ -362,11 +362,11 @@ fun ChatInfoLayout(
val conn = contact.activeConn
if (pqExperimentalEnabled && conn != null) {
SectionView("Post-quantum E2E encryption") {
InfoRow("PQ E2E encryption", if (conn.connPQEnabled) "Enabled" else "Disabled")
if (!conn.pqSupport) {
SectionView("Quantum resistant E2E encryption") {
InfoRow("E2E encryption", if (conn.connPQEnabled) "Quantum resistant" else "Standard")
if (!conn.pqEncryption) {
AllowContactPQButton(allowContactPQ)
SectionTextFooter("After allowing post-quantum encryption, it will be enabled after several messages if your contact also allows it.")
SectionTextFooter("After allowing quantum resistant e2e encryption, it will be enabled after several messages if your contact also allows it.")
}
SectionDividerSpaced()
}
@ -744,8 +744,8 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
fun showAllowContactPQAlert(allowContactPQ: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = "Allow post-quantum encryption?",
text = "This is an experimental feature, it is not recommended to enable it for high importance communications. It may result in connection errors!",
title = "Allow quantum resistant encryption?",
text = "This is an experimental feature, it is not recommended to enable it for important chats.",
confirmText = "Allow",
onConfirm = allowContactPQ,
destructive = true,

View file

@ -62,7 +62,7 @@ fun DeveloperView(
SectionSpacer()
SectionView("Experimental".uppercase()) {
SettingsPreferenceItem(painterResource(MR.images.ic_vpn_key_filled), "Post-quantum E2EE", m.controller.appPrefs.pqExperimentalEnabled, onChange = { enable ->
withBGApi { m.controller.apiSetPQEnabled(enable) }
withBGApi { m.controller.apiSetPQEncryption(enable) }
})
SectionTextFooter("In this version applies only to new contacts.")
}

View file

@ -1242,8 +1242,8 @@
<string name="snd_conn_event_ratchet_sync_started">agreeing encryption for %s…</string>
<string name="snd_conn_event_ratchet_sync_agreed">encryption agreed for %s</string>
<string name="rcv_conn_event_verification_code_reset">security code changed</string>
<string name="conn_event_enabled_pq">enabled post-quantum encryption</string>
<string name="conn_event_disabled_pq">disabled post-quantum encryption</string>
<string name="conn_event_enabled_pq">quantum resistant e2e encryption</string>
<string name="conn_event_disabled_pq">standard end-to-end encryption</string>
<!-- GroupMemberRole -->
<string name="group_member_role_observer">observer</string>

View file

@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 8cdd49b91256aee56427f8b8e351cf415045e9c7
tag: dab55e0a9b03577f643af7922afa061801d82ed5
source-repository-package
type: git

View file

@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."8cdd49b91256aee56427f8b8e351cf415045e9c7" = "0wgj9ypr6ry414bb15ixyg75cpivwycyh4icy33xm5whksvwy93r";
"https://github.com/simplex-chat/simplexmq.git"."dab55e0a9b03577f643af7922afa061801d82ed5" = "0dzqsvzxby83nla0rpx3xzj2y18lvmgs5ldjv5i1yp52npc88s1m";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";

View file

@ -596,18 +596,20 @@ processChatCommand' vr = \case
ok_
APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_
SetContactMergeEnabled onOff -> chatWriteVar contactMergeEnabled onOff >> ok_
APISetPQEnabled onOff -> chatWriteVar pqExperimentalEnabled onOff >> ok_
APIAllowContactPQ contactId -> withUser $ \user -> do
ct@Contact {activeConn} <- withStore $ \db -> getContact db user contactId
APISetPQEncryption onOff -> chatWriteVar pqExperimentalEnabled onOff >> ok_
APISetContactPQ ctId pqEnc -> withUser $ \user -> do
ct@Contact {activeConn} <- withStore $ \db -> getContact db user ctId
case activeConn of
Just conn@Connection {connId, pqSupport} -> case pqSupport of
PQSupportOn -> pure $ chatCmdError (Just user) "already allowed"
PQSupportOff -> do
withStore' $ \db -> updateConnSupportPQ db connId PQSupportOn
let conn' = conn {pqSupport = PQSupportOn, pqEncryption = PQEncOn} :: Connection
ct' = ct {activeConn = Just conn'} :: Contact
pure $ CRContactPQAllowed user ct'
Just conn@Connection {connId, pqSupport, pqEncryption}
| pqEncryption == pqEnc -> pure $ CRContactPQAllowed user ct pqEnc
| otherwise -> do
let pqSup = PQSupport $ pqEnc == PQEncOn || pqSupport == PQSupportOn
conn' = conn {pqSupport = pqSup, pqEncryption = pqEnc} :: Connection
ct' = ct {activeConn = Just conn'} :: Contact
withStore' $ \db -> updateConnSupportPQ db connId pqSup pqEnc
pure $ CRContactPQAllowed user ct' pqEnc
Nothing -> throwChatError $ CEContactNotActive ct
SetContactPQ cName pqEnc -> withContactName cName (`APISetContactPQ` pqEnc)
APIExportArchive cfg -> checkChatStopped $ exportArchive cfg >> ok_
ExportArchive -> do
ts <- liftIO getCurrentTime
@ -1401,7 +1403,8 @@ processChatCommand' vr = \case
subMode <- chatReadVar subscriptionMode
pqSup <- chatReadVar pqExperimentalEnabled
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing (IKNoPQ pqSup) subMode
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode pqSup
-- TODO PQ pass minVersion from the current range
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode initialChatVersion pqSup
pure $ CRInvitation user cReq conn
AddContact incognito -> withUser $ \User {userId} ->
processChatCommand $ APIAddContact userId incognito
@ -1431,10 +1434,12 @@ processChatCommand' vr = \case
pqSup <- chatReadVar pqExperimentalEnabled
withAgent' (\a -> connRequestPQSupport a pqSup cReq) >>= \case
Nothing -> throwChatError CEInvalidConnReq
Just pqSup' -> do
dm <- encodeConnInfoPQ pqSup' $ XInfo profileToSend
-- TODO PQ the error above should be CEIncompatibleConnReqVersion, also the same API should be called in Plan
Just (agentV, pqSup') -> do
let chatV = agentToChatVersion agentV
dm <- encodeConnInfoPQ pqSup' (Just chatV) $ XInfo profileToSend
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm pqSup' subMode
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode pqSup'
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode chatV pqSup'
pure $ CRSentConfirmation user conn
APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
@ -1652,8 +1657,9 @@ processChatCommand' vr = \case
subMode <- chatReadVar subscriptionMode
dm <- encodeConnInfo $ XGrpAcpt membershipMemId
agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm PQSupportOff subMode
let chatV = vr `compatibleChatVersion` peerChatVRange
withStore' $ \db -> do
createMemberConnection db userId fromMember agentConnId peerChatVRange subMode
createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode
updateGroupMemberStatus db userId fromMember GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` \_ -> pure ()
@ -2163,19 +2169,19 @@ processChatCommand' vr = \case
where
connect' groupLinkId cReqHash xContactId inGroup = do
pqSup <- if inGroup then pure PQSupportOff else chatReadVar pqExperimentalEnabled
(connId, incognitoProfile, subMode, pqSup') <- requestContact user incognito cReq xContactId inGroup pqSup
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode pqSup'
(connId, incognitoProfile, subMode, chatV) <- requestContact user incognito cReq xContactId inGroup pqSup
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup
pure $ CRSentInvitation user conn incognitoProfile
connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> m ChatResponse
connectContactViaAddress user incognito ct cReq =
withChatLock "connectViaContact" $ do
newXContactId <- XContactId <$> drgRandomBytes 16
pqSup <- chatReadVar pqExperimentalEnabled
(connId, incognitoProfile, subMode, pqSup') <- requestContact user incognito cReq newXContactId False pqSup
(connId, incognitoProfile, subMode, chatV) <- requestContact user incognito cReq newXContactId False pqSup
let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
ct' <- withStore $ \db -> createAddressContactConnection db user ct connId cReqHash newXContactId incognitoProfile subMode pqSup'
ct' <- withStore $ \db -> createAddressContactConnection db user ct connId cReqHash newXContactId incognitoProfile subMode chatV pqSup
pure $ CRSentInvitationToContact user ct' incognitoProfile
requestContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> XContactId -> Bool -> PQSupport -> m (ConnId, Maybe Profile, SubscriptionMode, PQSupport)
requestContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> XContactId -> Bool -> PQSupport -> m (ConnId, Maybe Profile, SubscriptionMode, VersionChat)
requestContact user incognito cReq xContactId inGroup pqSup = do
-- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
@ -2185,14 +2191,12 @@ processChatCommand' vr = \case
-- 2) toggle enabled, address doesn't support PQ - PQSupportOn but without compression, with version range indicating support
withAgent' (\a -> connRequestPQSupport a pqSup cReq) >>= \case
Nothing -> throwChatError CEInvalidConnReq
Just pqCompress -> do
let (pqSup', pqCompress') = case pqSup of
PQSupportOff -> (PQSupportOff, PQSupportOff)
PQSupportOn -> (PQSupportOn, pqCompress)
dm <- encodeConnInfoPQ pqCompress' (XContact profileToSend $ Just xContactId)
Just (agentV, _) -> do
let chatV = agentToChatVersion agentV
dm <- encodeConnInfoPQ pqSup (Just chatV) (XContact profileToSend $ Just xContactId)
subMode <- chatReadVar subscriptionMode
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm pqSup' subMode
pure (connId, incognitoProfile, subMode, pqSup')
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm pqSup subMode
pure (connId, incognitoProfile, subMode, chatV)
contactMember :: Contact -> [GroupMember] -> Maybe GroupMember
contactMember Contact {contactId} =
find $ \GroupMember {memberContactId = cId, memberStatus = s} ->
@ -2889,17 +2893,19 @@ acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId inv
let profileToSend = profileToSendOnAccept user incognitoProfile False
pqSup <- chatReadVar pqExperimentalEnabled
let pqSup' = pqSup `CR.pqSupportAnd` pqSupport
dm <- encodeConnInfoPQ pqSup' $ XInfo profileToSend
chatV <- pqCompatibleVersion pqSup cReqChatVRange
dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend
acId <- withAgent $ \a -> acceptContact a True invId dm pqSup' subMode
withStore' $ \db -> createAcceptedContact db user acId cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode pqSup' contactUsed
withStore' $ \db -> createAcceptedContact db user acId chatV cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode pqSup' contactUsed
acceptContactRequestAsync :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> Bool -> PQSupport -> m Contact
acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile contactUsed pqSup = do
subMode <- chatReadVar subscriptionMode
let profileToSend = profileToSendOnAccept user incognitoProfile False
(cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode pqSup
chatV <- chatReadVar pqExperimentalEnabled >>= (`pqCompatibleVersion` cReqChatVRange)
(cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode pqSup chatV
withStore' $ \db -> do
ct@Contact {activeConn} <- createAcceptedContact db user acId cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed
ct@Contact {activeConn} <- createAcceptedContact db user acId chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed
forM_ activeConn $ \Connection {connId} -> setCommandConnId db user cmdId connId
pure ct
@ -2907,7 +2913,7 @@ acceptGroupJoinRequestAsync :: ChatMonad m => User -> GroupInfo -> UserContactRe
acceptGroupJoinRequestAsync
user
gInfo@GroupInfo {groupProfile, membership}
ucr@UserContactRequest {agentInvitationId = AgentInvId invId}
ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange}
gLinkMemRole
incognitoProfile = do
gVar <- asks random
@ -2925,9 +2931,10 @@ acceptGroupJoinRequestAsync
groupSize = Just currentMemCount
}
subMode <- chatReadVar subscriptionMode
connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff
chatV <- chatReadVar pqExperimentalEnabled >>= (`pqCompatibleVersion` cReqChatVRange)
connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV
withStore $ \db -> do
liftIO $ createAcceptedMemberConnection db user connIds ucr groupMemberId subMode
liftIO $ createAcceptedMemberConnection db user connIds chatV ucr groupMemberId subMode
getGroupMemberById db user groupMemberId
profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Bool -> Profile
@ -3516,8 +3523,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
processCONFpqSupport :: Connection -> PQSupport -> m Connection
processCONFpqSupport conn@Connection {connId, pqSupport = pq} pq'
| pq == PQSupportOn && pq' == PQSupportOff = do
withStore' $ \db -> updateConnSupportPQ db connId pq'
pure (conn {pqSupport = pq', pqEncryption = CR.pqSupportToEnc pq'} :: Connection)
let pqEnc' = CR.pqSupportToEnc pq'
withStore' $ \db -> updateConnSupportPQ db connId pq' pqEnc'
pure (conn {pqSupport = pq', pqEncryption = pqEnc'} :: Connection)
| pq /= pq' = do
messageWarning "processCONFpqSupport: unexpected pqSupport change"
pure conn
@ -3528,7 +3536,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
when (pq /= pq') $ messageWarning "processINFOpqSupport: unexpected pqSupport change"
processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> m ()
processDirectMessage agentMsg connEntity conn@Connection {connId, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case
processDirectMessage agentMsg connEntity conn@Connection {connId, connChatVersion, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case
Nothing -> case agentMsg of
CONF confId pqSupport _ connInfo -> do
conn' <- processCONFpqSupport conn pqSupport
@ -3652,7 +3660,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
CON pqEnc ->
withStore' (\db -> getViaGroupMember db vr user ct) >>= \case
Nothing -> do
withStore' $ \db -> updateConnPQEnabledCON db connId pqEnc
when (pqEnc == PQEncOn) $ withStore' $ \db -> updateConnPQEnabledCON db connId pqEnc
let conn' = conn {pqSndEnabled = Just pqEnc, pqRcvEnabled = Just pqEnc} :: Connection
ct' = ct {activeConn = Just conn'} :: Contact
-- [incognito] print incognito profile used for this contact
@ -3680,7 +3688,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
subMode <- chatReadVar subscriptionMode
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode
gVar <- asks random
withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct' gLinkMemRole groupConnIds peerChatVRange subMode
withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct' gLinkMemRole groupConnIds connChatVersion peerChatVRange subMode
Just (gInfo, m@GroupMember {activeConn}) ->
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
notifyMemberConnected gInfo m $ Just ct
@ -4960,7 +4968,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
processGroupInvitation ct inv msg msgMeta = do
let Contact {localDisplayName = c, activeConn} = ct
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
forM_ activeConn $ \Connection {connId, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do
forM_ activeConn $ \Connection {connId, connChatVersion, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
-- [incognito] if direct connection with host is incognito, create membership using the same incognito profile
@ -4973,7 +4981,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
connIds <- joinAgentConnectionAsync user True connRequest dm subMode
withStore' $ \db -> do
setViaGroupLinkHash db groupId connId
createMemberConnectionAsync db user hostId connIds peerChatVRange subMode
createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode
updateGroupMemberStatusById db userId hostId GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct)
@ -5413,7 +5421,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
| maxVersion mcvr >= groupDirectInvVersion -> pure Nothing
| otherwise -> Just <$> createConn subMode
let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo
void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo memRestrictions groupConnIds directConnIds customUserProfileId subMode
chatV = (vr `compatibleChatVersion` ) . fromChatVRange =<< memChatVRange
void $ withStore $ \db -> createIntroReMember db user gInfo m chatV memInfo memRestrictions groupConnIds directConnIds customUserProfileId subMode
_ -> messageError "x.grp.mem.intro can be only sent by host member"
where
createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation subMode
@ -5460,7 +5469,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user True dcr dm subMode
let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo
mcvr = maybe chatInitialVRange fromChatVRange memChatVRange
withStore' $ \db -> createIntroToMemberContact db user m toMember mcvr groupConnIds directConnIds customUserProfileId subMode
chatV = vr `compatibleChatVersion` mcvr
withStore' $ \db -> createIntroToMemberContact db user m toMember chatV mcvr groupConnIds directConnIds customUserProfileId subMode
xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> UTCTime -> m ()
xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg brokerTs
@ -5814,27 +5824,36 @@ metaBrokerTs MsgMeta {broker = (_, brokerTs)} = brokerTs
sameMemberId :: MemberId -> GroupMember -> Bool
sameMemberId memId GroupMember {memberId} = memId == memberId
-- TODO v5.7 for contacts only version upgrade should trigger enabling PQ support/encryption
updatePeerChatVRange :: ChatMonad m => Connection -> VersionRangeChat -> m Connection
updatePeerChatVRange conn@Connection {connId, peerChatVRange} msgChatVRange = do
let jMsgChatVRange = msgChatVRange
if jMsgChatVRange /= peerChatVRange
updatePeerChatVRange conn@Connection {connId, connChatVersion = v, peerChatVRange, pqSupport} msgVRange = do
v' <- upgradedConnVersion pqSupport v msgVRange
if msgVRange /= peerChatVRange || v' /= v
then do
withStore' $ \db -> setPeerChatVRange db connId msgChatVRange
pure conn {peerChatVRange = jMsgChatVRange}
withStore' $ \db -> setPeerChatVRange db connId v' msgVRange
pure conn {connChatVersion = v', peerChatVRange = msgVRange}
else pure conn
updateMemberChatVRange :: ChatMonad m => GroupMember -> Connection -> VersionRangeChat -> m (GroupMember, Connection)
updateMemberChatVRange mem@GroupMember {groupMemberId} conn@Connection {connId, peerChatVRange} msgChatVRange = do
let jMsgChatVRange = msgChatVRange
if jMsgChatVRange /= peerChatVRange
updateMemberChatVRange mem@GroupMember {groupMemberId} conn@Connection {connId, connChatVersion = v, peerChatVRange} msgVRange = do
v' <- upgradedConnVersion PQSupportOff v msgVRange
if msgVRange /= peerChatVRange || v' /= v
then do
withStore' $ \db -> do
setPeerChatVRange db connId msgChatVRange
setMemberChatVRange db groupMemberId msgChatVRange
let conn' = conn {peerChatVRange = jMsgChatVRange}
pure (mem {memberChatVRange = jMsgChatVRange, activeConn = Just conn'}, conn')
setPeerChatVRange db connId v' msgVRange
setMemberChatVRange db groupMemberId msgVRange
let conn' = conn {connChatVersion = v', peerChatVRange = msgVRange}
pure (mem {memberChatVRange = msgVRange, activeConn = Just conn'}, conn')
else pure (mem, conn)
upgradedConnVersion :: ChatMonad' m => PQSupport -> Maybe VersionChat -> VersionRangeChat -> m (Maybe VersionChat)
upgradedConnVersion pqSup v_ vr = do
v_' <- pqCompatibleVersion pqSup vr
pure $ case (v_, v_') of
(Just v, Just v') -> Just $ max v v'
(Nothing, v'@Just {}) -> v'
(v, Nothing) -> v
parseFileDescription :: (ChatMonad m, FilePartyI p) => Text -> m (ValidFileDescription p)
parseFileDescription =
liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8)
@ -6127,18 +6146,19 @@ batchSndMessagesJSON = batchMessages maxRawMsgLength . L.toList
-- SMP.TBTransmission {} -> Left . ChatError $ CEInternalError "batchTransmissions_ didn't produce a batch"
encodeConnInfo :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> m ByteString
encodeConnInfo = encodeConnInfoPQ PQSupportOff
encodeConnInfo chatMsgEvent = do
vr <- chatVersionRange PQSupportOff
encodeConnInfoPQ PQSupportOff (Just $ maxVersion vr) chatMsgEvent
-- TODO PQ check size after compression (in compressedBatchMsgBody_ ?)
encodeConnInfoPQ :: (MsgEncodingI e, ChatMonad m) => PQSupport -> ChatMsgEvent e -> m ByteString
encodeConnInfoPQ pqSup chatMsgEvent = do
encodeConnInfoPQ :: (MsgEncodingI e, ChatMonad m) => PQSupport -> Maybe VersionChat -> ChatMsgEvent e -> m ByteString
encodeConnInfoPQ pqSup v chatMsgEvent = do
chatVRange <- chatVersionRange pqSup
let shouldCompress = maxVersion chatVRange >= pqEncryptionCompressionVersion
r = encodeChatMessage maxConnInfoLength ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent}
case r of
ECMEncoded encodedBody
| shouldCompress -> liftIO $ compressedBatchMsgBody encodedBody
| otherwise -> pure encodedBody
let msg = ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent}
case encodeChatMessage maxConnInfoLength msg of
ECMEncoded encodedBody -> case pqSup of
PQSupportOn | maybe False (>= pqEncryptionCompressionVersion) v -> liftIO $ compressedBatchMsgBody encodedBody
_ -> pure encodedBody
ECMLarge -> throwChatError $ CEException "large message"
where
compressedBatchMsgBody msgBody =
@ -6160,7 +6180,7 @@ type MsgReq = (Connection, MsgFlags, MsgBody, MessageId)
deliverMessages :: ChatMonad' m => NonEmpty MsgReq -> m (NonEmpty (Either ChatError (Int64, PQEncryption)))
deliverMessages msgs = deliverMessagesB $ L.map Right msgs
deliverMessagesB :: ChatMonad' m => NonEmpty (Either ChatError MsgReq) -> m (NonEmpty (Either ChatError (Int64, PQEncryption)))
deliverMessagesB :: forall m. ChatMonad' m => NonEmpty (Either ChatError MsgReq) -> m (NonEmpty (Either ChatError (Int64, PQEncryption)))
deliverMessagesB msgReqs = do
msgReqs' <- compressBodies
sent <- L.zipWith prepareBatch msgReqs' <$> withAgent' (`sendMessagesB` L.map toAgent msgReqs')
@ -6168,18 +6188,10 @@ deliverMessagesB msgReqs = do
withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent
where
compressBodies = liftIO $ withCompressCtx (toEnum maxRawMsgLength) $ \cctx -> do
forM msgReqs $ \case
mr@(Right (conn@Connection {pqSupport, pqEncryption, peerChatVRange}, msgFlags, msgBody, msgId))
| shouldCompress pqSupport pqEncryption ->
Right . (\cBody -> (conn, msgFlags, cBody, msgId)) <$> compressedBatchMsgBody_ cctx msgBody
| otherwise -> pure mr
where
--- TODO PQ
-- This version agreement is ephemeral and in case of peer downgrade it will get reduced, and pqSupport may be turned off in the result
-- We probably should store agreed version on Connection and do not allow reducing it.
chatV = maybe currentChatVersion (\(Compatible v') -> v') $ supportedChatVRange pqSupport `compatibleVersion` peerChatVRange
shouldCompress (PQSupport sup) (PQEncryption enc) = sup && (chatV >= pqEncryptionCompressionVersion && enc)
skip -> pure skip
forME msgReqs $ \mr@(conn@Connection {pqSupport, connChatVersion}, msgFlags, msgBody, msgId) -> Right <$> case pqSupport of
PQSupportOn | maybe False (>= pqEncryptionCompressionVersion) connChatVersion ->
(\cBody -> (conn, msgFlags, cBody, msgId)) <$> compressedBatchMsgBody_ cctx msgBody
_ -> pure mr
toAgent = \case
Right (conn@Connection {pqEncryption}, msgFlags, msgBody, _msgId) -> Right (aConnId conn, pqEncryption, msgFlags, msgBody)
Left _ce -> Left (AP.INTERNAL "ChatError, skip") -- as long as it is Left, the agent batchers should just step over it
@ -6449,16 +6461,16 @@ joinAgentConnectionAsync user enableNtfs cReqUri cInfo subMode = do
pure (cmdId, connId)
allowAgentConnectionAsync :: (MsgEncodingI e, ChatMonad m) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m ()
allowAgentConnectionAsync user conn@Connection {connId, pqSupport} confId msg = do
allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersion} confId msg = do
cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn
dm <- encodeConnInfoPQ pqSupport msg
dm <- encodeConnInfoPQ pqSupport connChatVersion msg
withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm
withStore' $ \db -> updateConnectionStatus db conn ConnAccepted
agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> PQSupport -> m (CommandId, ConnId)
agentAcceptContactAsync user enableNtfs invId msg subMode pqSup = do
agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> PQSupport -> Maybe VersionChat -> m (CommandId, ConnId)
agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do
cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact
dm <- encodeConnInfoPQ pqSup msg
dm <- encodeConnInfoPQ pqSup chatV msg
connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm pqSup subMode
pure (cmdId, connId)
@ -6698,6 +6710,12 @@ chatVersionRange pq = do
ChatConfig {chatVRange} <- asks config
pure $ chatVRange pq
compatibleChatVersion :: VersionRangeChat -> VersionRangeChat -> Maybe VersionChat
compatibleChatVersion vr vr' = (\(Compatible v) -> v) <$> (vr `compatibleVersion` vr')
pqCompatibleVersion :: ChatMonad' m => PQSupport -> VersionRangeChat -> m (Maybe VersionChat)
pqCompatibleVersion pq vr' = (`compatibleChatVersion` vr') <$> chatVersionRange pq
chatCommandP :: Parser ChatCommand
chatCommandP =
choice
@ -6740,8 +6758,9 @@ chatCommandP =
"/remote_hosts_folder " *> (SetRemoteHostsFolder <$> filePath),
"/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP),
"/contact_merge " *> (SetContactMergeEnabled <$> onOffP),
"/_pq " *> (APISetPQEnabled . PQSupport <$> onOffP),
"/_pq allow " *> (APIAllowContactPQ <$> A.decimal),
"/_pq @" *> (APISetContactPQ <$> A.decimal <* A.space <*> (PQEncryption <$> onOffP)),
"/pq @" *> (SetContactPQ <$> displayName <* A.space <*> (PQEncryption <$> onOffP)),
"/pq " *> (APISetPQEncryption . PQSupport <$> onOffP),
"/_db export " *> (APIExportArchive <$> jsonP),
"/db export" $> ExportArchive,
"/_db import " *> (APIImportArchive <$> jsonP),

View file

@ -244,8 +244,9 @@ data ChatCommand
| SetRemoteHostsFolder FilePath
| APISetEncryptLocalFiles Bool
| SetContactMergeEnabled Bool
| APISetPQEnabled PQSupport
| APIAllowContactPQ ContactId
| APISetPQEncryption PQSupport
| APISetContactPQ ContactId PQEncryption
| SetContactPQ ContactName PQEncryption
| APIExportArchive ArchiveConfig
| ExportArchive
| APIImportArchive ArchiveConfig
@ -702,7 +703,7 @@ data ChatResponse
| CRRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text}
| CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo}
| CRRemoteCtrlStopped {rcsState :: RemoteCtrlSessionState, rcStopReason :: RemoteCtrlStopReason}
| CRContactPQAllowed {user :: User, contact :: Contact}
| CRContactPQAllowed {user :: User, contact :: Contact, pqEncryption :: PQEncryption}
| CRContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption}
| CRSQLResult {rows :: [Text]}
| CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]}

View file

@ -185,6 +185,8 @@ contactsHelpInfo =
indent <> highlight "/verify @<name> " <> " - clear security code verification",
indent <> highlight "/info @<name> " <> " - info about contact connection",
indent <> highlight "/switch @<name> " <> " - switch receiving messages to another SMP relay",
indent <> highlight "/pq @<name> on/off " <> " - [BETA] toggle quantum resistant / standard e2e encryption for a contact",
indent <> " " <> " (both have to enable for quantum resistance)",
"",
green "Contact chat preferences:",
indent <> highlight "/set voice @<name> yes/no/always " <> " - allow/prohibit voice messages with the contact",
@ -320,6 +322,7 @@ settingsInfo =
map
styleMarkdown
[ green "Chat settings:",
indent <> highlight "/pq on/off " <> " - [BETA] toggle quantum resistant / standard e2e encryption for the new contacts",
indent <> highlight "/network " <> " - show / set network access options",
indent <> highlight "/smp " <> " - show / set configured SMP servers",
indent <> highlight "/xftp " <> " - show / set configured XFTP servers",

View file

@ -328,8 +328,8 @@ rcvConnEventToText = \case
RCERatchetSync syncStatus -> ratchetSyncStatusToText syncStatus
RCEVerificationCodeReset -> "security code changed"
RCEPqEnabled pqEnc -> case pqEnc of
PQEncOn -> "post-quantum encryption enabled"
PQEncOff -> "post-quantum encryption disabled"
PQEncOn -> "quantum resistant e2e encryption"
PQEncOff -> "standard end-to-end encryption"
ratchetSyncStatusToText :: RatchetSyncState -> Text
ratchetSyncStatusToText = \case
@ -348,8 +348,8 @@ sndConnEventToText = \case
SPCompleted -> "you changed address" <> forMember m
SCERatchetSync syncStatus m -> ratchetSyncStatusToText syncStatus <> forMember m
SCEPqEnabled pqEnc -> case pqEnc of
PQEncOn -> "post-quantum encryption enabled"
PQEncOff -> "post-quantum encryption disabled"
PQEncOn -> "quantum resistant e2e encryption"
PQEncOff -> "standard end-to-end encryption"
where
forMember member_ =
maybe "" (\GroupMemberRef {profile = Profile {displayName}} -> " for " <> displayName) member_

View file

@ -8,6 +8,7 @@ import Database.SQLite.Simple.QQ (sql)
m20240228_pq :: Query
m20240228_pq =
[sql|
ALTER TABLE connections ADD COLUMN conn_chat_version INTEGER;
ALTER TABLE connections ADD COLUMN pq_support INTEGER NOT NULL DEFAULT 0;
ALTER TABLE connections ADD COLUMN pq_encryption INTEGER NOT NULL DEFAULT 0;
ALTER TABLE connections ADD COLUMN pq_snd_enabled INTEGER;
@ -21,6 +22,7 @@ down_m20240228_pq =
[sql|
ALTER TABLE contact_requests DROP COLUMN pq_support;
ALTER TABLE connections DROP COLUMN conn_chat_version;
ALTER TABLE connections DROP COLUMN pq_support;
ALTER TABLE connections DROP COLUMN pq_encryption;
ALTER TABLE connections DROP COLUMN pq_snd_enabled;

View file

@ -277,6 +277,7 @@ CREATE TABLE connections(
peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
to_subscribe INTEGER DEFAULT 0 NOT NULL,
contact_conn_initiated INTEGER NOT NULL DEFAULT 0,
conn_chat_version INTEGER,
pq_support INTEGER NOT NULL DEFAULT 0,
pq_encryption INTEGER NOT NULL DEFAULT 0,
pq_snd_enabled INTEGER,

View file

@ -46,6 +46,7 @@ import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Chat.Call
import Simplex.Chat.Types
import Simplex.Chat.Types.Util
import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion)
import Simplex.Messaging.Compression (CompressCtx, compress, decompressBatch)
import Simplex.Messaging.Crypto.Ratchet (PQSupport (..), pattern PQSupportOn, pattern PQSupportOff)
import Simplex.Messaging.Encoding
@ -55,6 +56,15 @@ import Simplex.Messaging.Protocol (MsgBody)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Version hiding (version)
-- Chat version history:
-- 1 - support chat versions in connections (9/1/2023)
-- 2 - create contacts for group members only via x.grp.direct.inv (9/16/2023)
-- 3 - faster joining via group links without creating contact (10/30/2023)
-- 4 - group message forwarding (11/18/2023)
-- 5 - batch sending messages (12/23/2023)
-- 6 - send group welcome message after history (12/29/2023)
-- 7 - update member profiles (1/15/2024)
-- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig.
-- This indirection is needed for backward/forward compatibility testing.
-- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code.
@ -64,7 +74,7 @@ currentChatVersion = VersionChat 7
-- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above)
-- TODO remove parameterization in 5.7
supportedChatVRange :: PQSupport -> VersionRangeChat
supportedChatVRange pq = mkVersionRange (VersionChat 1) $ case pq of
supportedChatVRange pq = mkVersionRange initialChatVersion $ case pq of
PQSupportOn -> pqEncryptionCompressionVersion
PQSupportOff -> currentChatVersion
{-# INLINE supportedChatVRange #-}
@ -97,6 +107,11 @@ memberProfileUpdateVersion = VersionChat 7
pqEncryptionCompressionVersion :: VersionChat
pqEncryptionCompressionVersion = VersionChat 8
agentToChatVersion :: VersionSMPA -> VersionChat
agentToChatVersion v
| v < pqdrSMPAgentVersion = initialChatVersion
| otherwise = pqEncryptionCompressionVersion
data ConnectionEntity
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
| RcvGroupMsgConnection {entityConnection :: Connection, groupInfo :: GroupInfo, groupMember :: GroupMember}

View file

@ -61,7 +61,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id,
created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter,
peer_chat_min_version, peer_chat_max_version
conn_chat_version, peer_chat_min_version, peer_chat_max_version
FROM connections
WHERE user_id = ? AND agent_conn_id = ?
|]

View file

@ -125,14 +125,14 @@ deletePendingContactConnection db userId connId =
|]
(userId, connId, ConnContact)
createAddressContactConnection :: DB.Connection -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> PQSupport -> ExceptT StoreError IO Contact
createAddressContactConnection db user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode pqSup = do
PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode pqSup
createAddressContactConnection :: DB.Connection -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO Contact
createAddressContactConnection db user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode chatV pqSup = do
PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode chatV pqSup
liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId)
getContact db user contactId
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> PQSupport -> IO PendingContactConnection
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode pqSup = do
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup = do
createdAt <- getCurrentTime
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
let pccConnStatus = ConnJoined
@ -142,12 +142,12 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou
INSERT INTO connections (
user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated,
via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id,
created_at, updated_at, to_subscribe, pq_support, pq_encryption
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (userId, acId, pccConnStatus, ConnContact, True, cReqHash, xContactId)
:. (customUserProfileId, isJust groupLinkId, groupLinkId)
:. (createdAt, createdAt, subMode == SMOnlyCreate, pqSup, pqSup)
:. (createdAt, createdAt, subMode == SMOnlyCreate, chatV, pqSup, pqSup)
)
pccConnId <- insertedRowId db
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt}
@ -179,7 +179,7 @@ getContactByConnReqHash db user@User {userId} cReqHash =
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
JOIN connections c ON c.contact_id = ct.contact_id
@ -189,8 +189,8 @@ getContactByConnReqHash db user@User {userId} cReqHash =
|]
(userId, cReqHash, CSActive)
createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> PQSupport -> IO PendingContactConnection
createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode pqSup = do
createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection
createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode chatV pqSup = do
createdAt <- getCurrentTime
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
let contactConnInitiated = pccConnStatus == ConnNew
@ -199,11 +199,11 @@ createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile
[sql|
INSERT INTO connections
(user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id,
created_at, updated_at, to_subscribe, pq_support, pq_encryption)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (userId, acId, cReq, pccConnStatus, ConnContact, contactConnInitiated, customUserProfileId)
:. (createdAt, createdAt, subMode == SMOnlyCreate, pqSup, pqSup)
:. (createdAt, createdAt, subMode == SMOnlyCreate, chatV, pqSup, pqSup)
)
pccConnId <- insertedRowId db
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt}
@ -582,7 +582,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
@ -709,8 +709,8 @@ deleteContactRequest db User {userId} contactRequestId = do
(userId, userId, contactRequestId, userId)
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO Contact
createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do
createAcceptedContact :: DB.Connection -> User -> ConnId -> Maybe VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO Contact
createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
createdAt <- getCurrentTime
customUserProfileId <- forM incognitoProfile $ \case
@ -722,7 +722,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?)"
(userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId, contactUsed)
contactId <- insertedRowId db
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False}
@ -747,7 +747,7 @@ getContact_ db user@User {userId} contactId deleted =
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
@ -801,7 +801,7 @@ getContactConnections db userId Contact {contactId} =
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM connections c
JOIN contacts ct ON ct.contact_id = c.contact_id
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
@ -819,7 +819,7 @@ getConnectionById db User {userId} connId = ExceptT $ do
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id,
created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter,
peer_chat_min_version, peer_chat_max_version
conn_chat_version, peer_chat_min_version, peer_chat_max_version
FROM connections
WHERE user_id = ? AND connection_id = ?
|]

View file

@ -432,7 +432,8 @@ lookupChatRefByFileId db User {userId} fileId =
createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection
createSndFileConnection_ db userId fileId agentConnId subMode = do
currentTs <- getCurrentTime
createConnection_ db userId ConnSndFile (Just fileId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
-- TODO PQ use range from minVersion of the current range?
createConnection_ db userId ConnSndFile (Just fileId) agentConnId (Just initialChatVersion) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO ()
updateSndFileStatus db SndFileTransfer {fileId, connId} status = do

View file

@ -186,7 +186,7 @@ createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName}
"INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs)
userContactLinkId <- insertedRowId db
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId (Just initialChatVersion) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff
getGroupLinkConnection :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO Connection
getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
@ -197,7 +197,7 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ?
@ -283,7 +283,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr =
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
JOIN groups g ON g.group_id = m.group_id
@ -691,7 +691,7 @@ groupMemberQuery =
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
LEFT JOIN connections c ON c.connection_id = (
@ -785,11 +785,11 @@ getGroupInvitation db vr user groupId =
createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> SubscriptionMode -> ExceptT StoreError IO GroupMember
createNewContactMember _ _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ _ _ = throwError $ SEContactNotReady localDisplayName
createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile, activeConn = Just Connection {peerChatVRange}} memberRole agentConnId connRequest subMode =
createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile, activeConn = Just Connection {connChatVersion, peerChatVRange}} memberRole agentConnId connRequest subMode =
createWithRandomId gVar $ \memId -> do
createdAt <- liftIO getCurrentTime
member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt
void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt subMode
void $ createMemberConnection_ db userId groupMemberId agentConnId connChatVersion peerChatVRange Nothing 0 createdAt subMode
pure member
where
VersionRange minV maxV = peerChatVRange
@ -832,13 +832,13 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId,
:. (minV, maxV)
)
createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO ()
createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) peerChatVRange subMode =
createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> Maybe VersionChat -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO ()
createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) chatV peerChatVRange subMode =
createWithRandomId gVar $ \memId -> do
createdAt <- liftIO getCurrentTime
insertMember_ (MemberId memId) createdAt
groupMemberId <- liftIO $ insertedRowId db
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt subMode
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 createdAt subMode
setCommandConnId db user cmdId connId
where
VersionRange minV maxV = peerChatVRange
@ -889,16 +889,17 @@ createAcceptedMember
:. (minV, maxV)
)
createAcceptedMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO ()
createAcceptedMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> Maybe VersionChat -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO ()
createAcceptedMemberConnection
db
user@User {userId}
(cmdId, agentConnId)
chatV
UserContactRequest {cReqChatVRange, userContactLinkId}
groupMemberId
subMode = do
createdAt <- liftIO getCurrentTime
Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff
Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff
setCommandConnId db user cmdId connId
getContactViaMember :: DB.Connection -> User -> GroupMember -> ExceptT StoreError IO Contact
@ -928,15 +929,15 @@ getMemberInvitation db User {userId} groupMemberId =
fmap join . maybeFirstRow fromOnly $
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId)
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionRangeChat -> SubscriptionMode -> IO ()
createMemberConnection db userId GroupMember {groupMemberId} agentConnId peerChatVRange subMode = do
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> Maybe VersionChat -> VersionRangeChat -> SubscriptionMode -> IO ()
createMemberConnection db userId GroupMember {groupMemberId} agentConnId chatV peerChatVRange subMode = do
currentTs <- getCurrentTime
void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs subMode
void $ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionRangeChat -> SubscriptionMode -> IO ()
createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) peerChatVRange subMode = do
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> Maybe VersionChat -> VersionRangeChat -> SubscriptionMode -> IO ()
createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) chatV peerChatVRange subMode = do
currentTs <- getCurrentTime
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs subMode
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode
setCommandConnId db user cmdId connId
updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO ()
@ -1202,12 +1203,13 @@ getForwardInvitedMembers db user forwardMember highlyAvailable = do
WHERE re_group_member_id = ? AND intro_status NOT IN (?,?,?)
|]
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> Maybe MemberRestrictions -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> ExceptT StoreError IO GroupMember
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> Maybe VersionChat -> MemberInfo -> Maybe MemberRestrictions -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> ExceptT StoreError IO GroupMember
createIntroReMember
db
user@User {userId}
gInfo@GroupInfo {groupId}
_host@GroupMember {memberContactId, activeConn}
chatV
memInfo@(MemberInfo _ _ memChatVRange memberProfile)
memRestrictions_
(groupCmdId, groupAgentConnId)
@ -1220,7 +1222,7 @@ createIntroReMember
currentTs <- liftIO getCurrentTime
newMember <- case directConnIds of
Just (directCmdId, directAgentConnId) -> do
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId chatV mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff
liftIO $ setCommandConnId db user directCmdId directConnId
(localDisplayName, contactId, memProfileId) <- createContact_ db userId memberProfile "" (Just groupId) currentTs False
liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, directConnId)
@ -1230,18 +1232,18 @@ createIntroReMember
pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memRestriction, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId}
liftIO $ do
member <- createNewMember_ db user gInfo newMember currentTs
conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId mcvr memberContactId cLevel currentTs subMode
conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId chatV mcvr memberContactId cLevel currentTs subMode
liftIO $ setCommandConnId db user groupCmdId groupConnId
pure (member :: GroupMember) {activeConn = Just conn}
createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionRangeChat -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO ()
createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do
createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> Maybe VersionChat -> VersionRangeChat -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO ()
createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} chatV mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do
let cLevel = 1 + maybe 0 (\Connection {connLevel} -> connLevel) activeConn
currentTs <- getCurrentTime
Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId mcvr viaContactId cLevel currentTs subMode
Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId chatV mcvr viaContactId cLevel currentTs subMode
setCommandConnId db user groupCmdId groupConnId
forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do
Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff
Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId chatV mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff
setCommandConnId db user directCmdId directConnId
contactId <- createMemberContact_ directConnId currentTs
updateMember_ contactId currentTs
@ -1271,9 +1273,9 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId =
|]
[":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId]
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionRangeChat -> Maybe Int64 -> Int -> UTCTime -> SubscriptionMode -> IO Connection
createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange viaContact connLevel currentTs subMode =
createConnection_ db userId ConnMember (Just groupMemberId) agentConnId peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe VersionChat -> VersionRangeChat -> Maybe Int64 -> Int -> UTCTime -> SubscriptionMode -> IO Connection
createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange viaContact connLevel currentTs subMode =
createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff
getViaGroupMember :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember))
getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
@ -1297,7 +1299,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m
JOIN contacts ct ON ct.contact_id = m.contact_id
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@ -1882,7 +1884,7 @@ createMemberContact
cReq
gInfo
GroupMember {groupMemberId, localDisplayName, memberProfile, memberContactProfileId}
Connection {connLevel, peerChatVRange = peerChatVRange@(VersionRange minV maxV)}
Connection {connLevel, connChatVersion, peerChatVRange = peerChatVRange@(VersionRange minV maxV)}
subMode = do
currentTs <- getCurrentTime
let incognitoProfile = incognitoMembershipProfile gInfo
@ -1909,11 +1911,11 @@ createMemberContact
[sql|
INSERT INTO connections (
user_id, agent_conn_id, conn_req_inv, conn_level, conn_status, conn_type, contact_conn_initiated, contact_id, custom_user_profile_id,
peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
conn_chat_version, peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (userId, acId, cReq, connLevel, ConnNew, ConnContact, True, contactId, customUserProfileId)
:. (minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate)
:. (connChatVersion, minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate)
)
connId <- insertedRowId db
let ctConn =
@ -1921,6 +1923,7 @@ createMemberContact
{ connId,
agentConnId = AgentConnId acId,
peerChatVRange,
connChatVersion,
connType = ConnContact,
contactConnInitiated = True,
entityId = Just contactId,
@ -2030,7 +2033,7 @@ createMemberContactConn_
user@User {userId}
(cmdId, acId)
gInfo
_memberConn@Connection {connLevel, peerChatVRange = peerChatVRange@(VersionRange minV maxV)}
_memberConn@Connection {connLevel, connChatVersion, peerChatVRange = peerChatVRange@(VersionRange minV maxV)}
contactId
subMode = do
currentTs <- liftIO getCurrentTime
@ -2040,11 +2043,11 @@ createMemberContactConn_
[sql|
INSERT INTO connections (
user_id, agent_conn_id, conn_level, conn_status, conn_type, contact_id, custom_user_profile_id,
peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
conn_chat_version, peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (userId, acId, connLevel, ConnJoined, ConnContact, contactId, customUserProfileId)
:. (minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate)
:. (connChatVersion, minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate)
)
connId <- insertedRowId db
setCommandConnId db user cmdId connId
@ -2052,6 +2055,7 @@ createMemberContactConn_
Connection
{ connId,
agentConnId = AgentConnId acId,
connChatVersion,
peerChatVRange,
connType = ConnContact,
contactConnInitiated = False,

View file

@ -324,7 +324,7 @@ createUserContactLink db User {userId} agentConnId cReq subMode =
"INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)"
(userId, cReq, currentTs, currentTs)
userContactLinkId <- insertedRowId db
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId (Just initialChatVersion) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
getUserAddressConnections :: DB.Connection -> User -> ExceptT StoreError IO [Connection]
getUserAddressConnections db User {userId} = do
@ -340,7 +340,7 @@ getUserAddressConnections db User {userId} = do
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL
@ -356,7 +356,7 @@ getUserContactLinks db User {userId} =
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
c.peer_chat_min_version, c.peer_chat_max_version,
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version,
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id

View file

@ -151,15 +151,16 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, VersionChat, VersionChat)
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Maybe VersionChat, VersionChat, VersionChat)
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe VersionChat, Maybe VersionChat)
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat)
toConnection :: ConnectionRow -> Connection
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, minVer, maxVer)) =
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, connChatVersion, minVer, maxVer)) =
Connection
{ connId,
agentConnId = AgentConnId acId,
connChatVersion, -- TODO we could avoid maybe here by computing compatible version, but it would require passing current version range here as well
peerChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer,
connLevel,
viaContact,
@ -189,12 +190,12 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup
entityId_ ConnUserContact = userContactLinkId
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, Just minVer, Just maxVer)) =
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, minVer, maxVer))
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, connChatVersion, Just minVer, Just maxVer)) =
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, connChatVersion, minVer, maxVer))
toMaybeConnection _ = Nothing
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection
createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode pqSup = do
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe VersionChat -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection
createConnection_ db userId connType entityId acId connChatVersion peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode pqSup = do
viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId ->
maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId)
let viaGroupLink = isJust viaLinkGroupId
@ -204,18 +205,19 @@ createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange
INSERT INTO connections (
user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id, conn_status, conn_type,
contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at,
peer_chat_min_version, peer_chat_max_version, to_subscribe, pq_support, pq_encryption
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
conn_chat_version, peer_chat_min_version, peer_chat_max_version, to_subscribe, pq_support, pq_encryption
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType)
:. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs)
:. (minV, maxV, subMode == SMOnlyCreate, pqSup, pqSup)
:. (connChatVersion, minV, maxV, subMode == SMOnlyCreate, pqSup, pqSup)
)
connId <- insertedRowId db
pure
Connection
{ connId,
agentConnId = AgentConnId acId,
connChatVersion,
peerChatVRange,
connType,
contactConnInitiated = False,
@ -250,8 +252,8 @@ createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, imag
(displayName, fullName, image, userId, Just True, createdAt, createdAt)
insertedRowId db
updateConnSupportPQ :: DB.Connection -> Int64 -> PQSupport -> IO ()
updateConnSupportPQ db connId pqSup =
updateConnSupportPQ :: DB.Connection -> Int64 -> PQSupport -> PQEncryption -> IO ()
updateConnSupportPQ db connId pqSup pqEnc =
DB.execute
db
[sql|
@ -259,7 +261,7 @@ updateConnSupportPQ db connId pqSup =
SET pq_support = ?, pq_encryption = ?
WHERE connection_id = ?
|]
(pqSup, pqSup, connId)
(pqSup, pqEnc, connId)
updateConnPQSndEnabled :: DB.Connection -> Int64 -> PQEncryption -> IO ()
updateConnPQSndEnabled db connId pqSndEnabled =
@ -294,16 +296,16 @@ updateConnPQEnabledCON db connId pqEnabled =
|]
(pqEnabled, pqEnabled, connId)
setPeerChatVRange :: DB.Connection -> Int64 -> VersionRangeChat -> IO ()
setPeerChatVRange db connId (VersionRange minVer maxVer) =
setPeerChatVRange :: DB.Connection -> Int64 -> Maybe VersionChat -> VersionRangeChat -> IO ()
setPeerChatVRange db connId chatV (VersionRange minVer maxVer) =
DB.execute
db
[sql|
UPDATE connections
SET peer_chat_min_version = ?, peer_chat_max_version = ?
SET conn_chat_version = ?, peer_chat_min_version = ?, peer_chat_max_version = ?
WHERE connection_id = ?
|]
(minVer, maxVer, connId)
(chatV, minVer, maxVer, connId)
setMemberChatVRange :: DB.Connection -> GroupMemberId -> VersionRangeChat -> IO ()
setMemberChatVRange db mId (VersionRange minVer maxVer) =

View file

@ -23,7 +23,7 @@
module Simplex.Chat.Types where
import Crypto.Number.Serialize (os2ip)
import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.=))
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson as J
import qualified Data.Aeson.Encoding as JE
import qualified Data.Aeson.TH as JQ
@ -1291,6 +1291,7 @@ type ConnReqContact = ConnectionRequestUri 'CMContact
data Connection = Connection
{ connId :: Int64,
agentConnId :: AgentConnId,
connChatVersion :: Maybe VersionChat,
peerChatVRange :: VersionRangeChat,
connLevel :: Int,
viaContact :: Maybe Int64, -- group member contact ID, if not direct connection

View file

@ -342,8 +342,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} ->
["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName]
CRRemoteCtrlStopped {} -> ["remote controller stopped"]
CRContactPQEnabled u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": post-quantum encryption " <> (if pqOn then "enabled" else "disabled")]
CRContactPQAllowed u c -> ttyUser u [ttyContact' c <> ": post-quantum encryption allowed"]
CRContactPQAllowed u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": enable " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption"]
CRContactPQEnabled u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption enabled"]
CRSQLResult rows -> map plain rows
CRSlowSQLQueries {chatQueries, agentQueries} ->
let viewQuery SlowSQLQuery {query, queryStats = SlowQueryStats {count, timeMax, timeAvg}} =
@ -1177,7 +1177,7 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
incognitoProfile
<> ["alias: " <> plain localAlias | localAlias /= ""]
<> [viewConnectionVerified (contactSecurityCode ct)]
<> ["post-quantum encryption enabled" | contactPQEnabled ct == CR.PQEncOn]
<> ["quantum resistant end-to-end encryption" | contactPQEnabled ct == CR.PQEncOn]
<> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn
viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString]

View file

@ -21,11 +21,11 @@ import qualified Simplex.Chat.AppSettings as AS
import Simplex.Chat.Call
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Options (ChatOpts (..))
import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion, supportedChatVRange)
import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificationCode, verificationCode, pattern VersionChat)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), pattern PQSupportOff, pattern PQEncOn, pattern PQEncOff)
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), pattern PQSupportOn, pattern PQSupportOff, pattern PQEncOn, pattern PQEncOff)
import Simplex.Messaging.Util (safeDecodeUtf8)
import Simplex.Messaging.Version
import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
@ -131,7 +131,8 @@ chatDirectTests = do
describe "PQ tests" $ do
describe "enable PQ before connection, connect via invitation link" $ pqMatrix2 runTestPQConnectViaLink
describe "enable PQ before connection, connect via contact address" $ pqMatrix2 runTestPQConnectViaAddress
it "should enable PQ after several messages in connection without PQ" testPQAllowContact
it "should enable PQ after several messages in connection without PQ" testPQEnableContact
it "should enable PQ, reduce envelope size and enable compression" testPQEnableContactCompression
where
testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2
testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2
@ -2784,7 +2785,7 @@ runTestPQConnectViaLink (alice, aPQ) (bob, bPQ) = do
pqOn :: TestCC -> IO ()
pqOn cc = do
cc ##> "/_pq on"
cc ##> "/pq on"
cc <## "ok"
runTestPQConnectViaAddress :: HasCallStack => (TestCC, PQEnabled) -> (TestCC, PQEnabled) -> IO ()
@ -2820,8 +2821,8 @@ runTestPQConnectViaAddress (alice, aPQ) (bob, bPQ) = do
pqSend = if pqEnabled then (+#>) else (\#>)
e2eeInfo = if pqEnabled then e2eeInfoPQStr else e2eeInfoNoPQStr
testPQAllowContact :: HasCallStack => FilePath -> IO ()
testPQAllowContact =
testPQEnableContact :: HasCallStack => FilePath -> IO ()
testPQEnableContact =
testChat2 aliceProfile bobProfile $ \alice bob -> do
connectUsers alice bob
(alice, "hi") \#> bob
@ -2853,15 +2854,15 @@ testPQAllowContact =
PQEncOff <- bob `pqForContact` 2
-- if only one contact allows PQ, it's not enabled
alice ##> "/_pq allow 2"
alice <## "bob: post-quantum encryption allowed"
alice ##> "/pq @bob on"
alice <## "bob: enable quantum resistant end-to-end encryption"
sendMany PQEncOff alice bob
PQEncOff <- alice `pqForContact` 2
PQEncOff <- bob `pqForContact` 2
-- both contacts have to allow PQ to enable it
bob ##> "/_pq allow 2"
bob <## "alice: post-quantum encryption allowed"
bob ##> "/pq @alice on"
bob <## "alice: enable quantum resistant end-to-end encryption"
(alice, "1") \#> bob
(bob, "2") \#> alice
@ -2875,16 +2876,16 @@ testPQAllowContact =
(bob, "6") ++#> alice
-- equivalent to:
-- bob `send` "@alice 6"
-- bob <## "alice: post-quantum encryption enabled"
-- bob <## "alice: quantum resistant end-to-end encryption enabled"
-- bob <# "@alice 6"
-- alice <## "bob: post-quantum encryption enabled"
-- alice <## "bob: quantum resistant end-to-end encryption enabled"
-- alice <# "bob> 6"
PQEncOn <- alice `pqForContact` 2
alice #$> ("/_get chat @2 count=2", chat, [(0, "post-quantum encryption enabled"), (0, "6")])
alice #$> ("/_get chat @2 count=2", chat, [(0, e2eeInfoPQStr), (0, "6")])
PQEncOn <- bob `pqForContact` 2
bob #$> ("/_get chat @2 count=2", chat, [(1, "post-quantum encryption enabled"), (1, "6")])
bob #$> ("/_get chat @2 count=2", chat, [(1, e2eeInfoPQStr), (1, "6")])
(alice, "6") +#> bob
(bob, "7") +#> alice
@ -2894,8 +2895,43 @@ testPQAllowContact =
PQEncOn <- alice `pqForContact` 2
PQEncOn <- bob `pqForContact` 2
pure ()
sendMany :: PQEncryption -> TestCC -> TestCC -> IO ()
sendMany pqEnc alice bob =
forM_ [(1 :: Int) .. 10] $ \i -> do
sndRcv pqEnc False (alice, show i) bob
sndRcv pqEnc False (bob, show i) alice
testPQEnableContactCompression :: HasCallStack => FilePath -> IO ()
testPQEnableContactCompression =
testChat2 aliceProfile bobProfile $ \alice bob -> do
connectUsers alice bob
(alice, "hi") \#> bob
(bob, "hey") \#> alice
PQEncOff <- alice `pqForContact` 2
PQEncOff <- bob `pqForContact` 2
(alice, "lrg 1", v) \:#> (bob, v)
(bob, "lrg 2", v) \:#> (alice, v)
PQSupportOff <- alice `pqSupportForCt` 2
alice ##> "/pq @bob on"
alice <## "bob: enable quantum resistant end-to-end encryption"
PQSupportOn <- alice `pqSupportForCt` 2
(alice, "lrg 3", v) \:#> (bob, v)
(bob, "lrg 4", v) \:#> (alice, v)
PQSupportOff <- bob `pqSupportForCt` 2
bob ##> "/pq @alice on"
bob <## "alice: enable quantum resistant end-to-end encryption"
PQSupportOn <- bob `pqSupportForCt` 2
(alice, "lrg 1", v) \:#> (bob, v')
(bob, "lrg 2", v') \:#> (alice, v')
(alice, "lrg 3", v') \:#> (bob, v')
(bob, "lrg 4", v') \:#> (alice, v')
(alice, "lrg 5", v') +:#> (bob, v')
PQEncOff <- alice `pqForContact` 2
PQEncOff <- bob `pqForContact` 2
(bob, "lrg 6", v') ++:#> (alice, v')
(alice, "lrg 7", v') +:#> (bob, v')
(bob, "lrg 8", v') +:#> (alice, v')
where
sendMany pqEnc alice bob =
forM_ [(1 :: Int) .. 10] $ \i -> do
sndRcv pqEnc False (alice, show i) bob
sndRcv pqEnc False (bob, show i) alice
v = currentChatVersion
v' = pqEncryptionCompressionVersion

View file

@ -13,6 +13,7 @@ import Control.Concurrent.Async (concurrently_)
import Control.Concurrent.STM
import Control.Monad (unless, when)
import Control.Monad.Except (runExceptT)
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as B
import Data.Char (isDigit)
import Data.List (isPrefixOf, isSuffixOf)
@ -31,7 +32,8 @@ import Simplex.Chat.Types.Preferences
import Simplex.FileTransfer.Client.Main (xftpClientCLI)
import Simplex.Messaging.Agent.Store.SQLite (maybeFirstRow, withTransaction)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Version
import System.Directory (doesFileExist)
@ -201,13 +203,41 @@ sndRcv pqEnc enabled (cc1, msg) cc2 = do
name2 <- userName cc2
let cmd = "@" <> name2 <> " " <> msg
cc1 `send` cmd
when enabled $ cc1 <## (name2 <> ": post-quantum encryption enabled")
when enabled $ cc1 <## (name2 <> ": quantum resistant end-to-end encryption enabled")
cc1 <# cmd
cc1 `pqSndForContact` 2 `shouldReturn` pqEnc
when enabled $ cc2 <## (name1 <> ": post-quantum encryption enabled")
when enabled $ cc2 <## (name1 <> ": quantum resistant end-to-end encryption enabled")
cc2 <# (name1 <> "> " <> msg)
cc2 `pqRcvForContact` 2 `shouldReturn` pqEnc
(\:#>) :: HasCallStack => (TestCC, String, VersionChat) -> (TestCC, VersionChat) -> IO ()
(\:#>) = sndRcvImg PQEncOff False
(+:#>) :: HasCallStack => (TestCC, String, VersionChat) -> (TestCC, VersionChat) -> IO ()
(+:#>) = sndRcvImg PQEncOn False
(++:#>) :: HasCallStack => (TestCC, String, VersionChat) -> (TestCC, VersionChat) -> IO ()
(++:#>) = sndRcvImg PQEncOn True
sndRcvImg :: HasCallStack => PQEncryption -> Bool -> (TestCC, String, VersionChat) -> (TestCC, VersionChat) -> IO ()
sndRcvImg pqEnc enabled (cc1, msg, v1) (cc2, v2) = do
name1 <- userName cc1
name2 <- userName cc2
g <- C.newRandom
img <- atomically $ B64.encode <$> C.randomBytes lrgLen g
cc1 `send` ("/_send @2 json {\"msgContent\":{\"type\":\"image\",\"text\":\"" <> msg <> "\",\"image\":\"" <> B.unpack img <> "\"}}")
cc1 .<## "}}"
when enabled $ cc1 <## (name2 <> ": quantum resistant end-to-end encryption enabled")
cc1 <# ("@" <> name2 <> " " <> msg)
cc1 `pqSndForContact` 2 `shouldReturn` pqEnc
cc1 `pqVerForContact` 2 `shouldReturn` v1
when enabled $ cc2 <## (name1 <> ": quantum resistant end-to-end encryption enabled")
cc2 <# (name1 <> "> " <> msg)
cc2 `pqRcvForContact` 2 `shouldReturn` pqEnc
cc2 `pqVerForContact` 2 `shouldReturn` v2
where
lrgLen = maxEncodedMsgLength PQSupportOff * 3 `div` 4 - 98 -- this is max size for binary image preview given the rest of the message
-- PQ combinators /
chat :: String -> [(Int, String)]
@ -518,19 +548,25 @@ getProfilePictureByName cc displayName =
DB.query db "SELECT image FROM contact_profiles WHERE display_name = ? LIMIT 1" (Only displayName)
pqSndForContact :: TestCC -> ContactId -> IO PQEncryption
pqSndForContact = pqForContact_ pqSndEnabled
pqSndForContact = pqForContact_ pqSndEnabled PQEncOff
pqRcvForContact :: TestCC -> ContactId -> IO PQEncryption
pqRcvForContact = pqForContact_ pqRcvEnabled
pqRcvForContact = pqForContact_ pqRcvEnabled PQEncOff
pqForContact :: TestCC -> ContactId -> IO PQEncryption
pqForContact = pqForContact_ (Just . connPQEnabled)
pqForContact = pqForContact_ (Just . connPQEnabled) PQEncOff
pqForContact_ :: (Connection -> Maybe PQEncryption) -> TestCC -> ContactId -> IO PQEncryption
pqForContact_ pqSel cc contactId =
getTestCCContact cc contactId >>= \ct -> case contactConn ct of
Just conn -> pure $ fromMaybe PQEncOff $ pqSel conn
Nothing -> fail "no connection"
pqSupportForCt :: TestCC -> ContactId -> IO PQSupport
pqSupportForCt = pqForContact_ (\Connection {pqSupport} -> Just pqSupport) PQSupportOff
pqVerForContact :: TestCC -> ContactId -> IO VersionChat
pqVerForContact = pqForContact_ connChatVersion (VersionChat 0)
pqForContact_ :: (Connection -> Maybe a) -> a -> TestCC -> ContactId -> IO a
pqForContact_ pqSel def cc contactId = (fromMaybe def . pqSel) <$> getCtConn cc contactId
getCtConn :: TestCC -> ContactId -> IO Connection
getCtConn cc contactId = getTestCCContact cc contactId >>= maybe (fail "no connection") pure . contactConn
getTestCCContact :: TestCC -> ContactId -> IO Contact
getTestCCContact cc contactId =