diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 986287fca0..a5a07a8722 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 7645a94035..57dab12a87 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -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 { diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index bc4b6947ab..86532605db 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -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() ) diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index 816b46c54f..9b11c6d0f7 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -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)") } } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 6bb3fbb3c2..4df419ffef 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -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 diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 267c254be1..3463bfca18 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -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") } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 9e3ce480df..d695b2c608 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -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, val agentMigrations: List): 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" + diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 6e30a89810..7e2ba462c9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -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) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 97ec502f35..bbd5d93018 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -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, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index 5dca1527f2..421d4feec3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -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.") } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 7abd5c8a49..49552a592d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1242,8 +1242,8 @@ agreeing encryption for %s… encryption agreed for %s security code changed - enabled post-quantum encryption - disabled post-quantum encryption + quantum resistant e2e encryption + standard end-to-end encryption observer diff --git a/cabal.project b/cabal.project index 9f72b40aea..05e9af91ff 100644 --- a/cabal.project +++ b/cabal.project @@ -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 diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 21de83d0ab..7ca6c8c074 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -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"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 27a38cb292..068d3197fa 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -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), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 691fe64623..b2d82a0243 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -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]} diff --git a/src/Simplex/Chat/Help.hs b/src/Simplex/Chat/Help.hs index ac93e05533..adb77b9557 100644 --- a/src/Simplex/Chat/Help.hs +++ b/src/Simplex/Chat/Help.hs @@ -185,6 +185,8 @@ contactsHelpInfo = indent <> highlight "/verify @ " <> " - clear security code verification", indent <> highlight "/info @ " <> " - info about contact connection", indent <> highlight "/switch @ " <> " - switch receiving messages to another SMP relay", + indent <> highlight "/pq @ 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 @ 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", diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 8640cf23d3..0e95570b85 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -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_ diff --git a/src/Simplex/Chat/Migrations/M20240228_pq.hs b/src/Simplex/Chat/Migrations/M20240228_pq.hs index 1b1c173faa..c496d33b4b 100644 --- a/src/Simplex/Chat/Migrations/M20240228_pq.hs +++ b/src/Simplex/Chat/Migrations/M20240228_pq.hs @@ -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; diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index ea59a94d1f..19c6ba24d0 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -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, diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 703850c85d..1af6d676ab 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -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} diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 311dba6579..3bb4f3bf45 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -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 = ? |] diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 3128908225..d3806fe34c 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -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 = ? |] diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 6192f5eda4..0965150605 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -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 diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 4a87893e58..3b81d5b242 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -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, diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 06029b913d..0d47982aca 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -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 diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 1a5b41be68..0650dd23de 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -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) = diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index d4a3a8029e..18fe5ad1ac 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -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 diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index d953b9183f..8aa917f044 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -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] diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index cc1a7791b0..f50c82878f 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -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 diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 387dd95dd9..21993d11ef 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -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 =