diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 07d83ac475..bc4b6947ab 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -172,13 +172,13 @@ struct ChatInfoView: View { let conn = contact.activeConn { Section { infoRow(Text(String("PQ E2E encryption")), conn.connPQEnabled ? "Enabled" : "Disabled") - if !conn.enablePQ { + if !conn.pqSupport { allowPQButton() } } header: { Text(String("Post-quantum E2E encryption")) } footer: { - if !conn.enablePQ { + if !conn.pqSupport { Text(String("After allowing post-quantum encryption, it will be enabled after several messages if your contact also allows it.")) } } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 0125973d14..267c254be1 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1532,14 +1532,15 @@ public struct Connection: Decodable { public var viaGroupLink: Bool public var customUserProfileId: Int64? public var connectionCode: SecurityCode? - public var enablePQ: Bool + public var pqSupport: Bool + public var pqEncryption: Bool public var pqSndEnabled: Bool? public var pqRcvEnabled: Bool? public var connectionStats: ConnectionStats? = nil private enum CodingKeys: String, CodingKey { - case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, enablePQ, pqSndEnabled, pqRcvEnabled + case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled } public var id: ChatId { get { ":\(connId)" } } @@ -1555,7 +1556,8 @@ public struct Connection: Decodable { connStatus: .ready, connLevel: 0, viaGroupLink: false, - enablePQ: false + pqSupport: false, + pqEncryption: false ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 21bfb1daa3..df1dec330d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1123,11 +1123,19 @@ data class Connection( val viaGroupLink: Boolean, val customUserProfileId: Long? = null, val connectionCode: SecurityCode? = null, + val pqSupport: Boolean, + val pqEncryption: Boolean, + val pqSndEnabled: Boolean? = null, + val pqRcvEnabled: Boolean? = null, val connectionStats: ConnectionStats? = null ) { val id: ChatId get() = ":$connId" + + val connPQEnabled: Boolean + get() = pqSndEnabled == true && pqRcvEnabled == true + companion object { - val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null) + val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false) } } @@ -1853,6 +1861,10 @@ data class ChatItem ( is CIContent.SndModerated -> false is CIContent.RcvModerated -> false is CIContent.RcvBlocked -> false + is CIContent.SndDirectE2EEInfo -> false + is CIContent.RcvDirectE2EEInfo -> false + is CIContent.SndGroupE2EEInfo -> false + is CIContent.RcvGroupE2EEInfo -> false is CIContent.InvalidJSON -> false } @@ -2283,6 +2295,10 @@ sealed class CIContent: ItemContent { @Serializable @SerialName("sndModerated") object SndModerated: CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvModerated") object RcvModerated: CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvBlocked") object RcvBlocked: CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("sndDirectE2EEInfo") class SndDirectE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvDirectE2EEInfo") class RcvDirectE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("sndGroupE2EEInfo") class SndGroupE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvGroupE2EEInfo") class RcvGroupE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null } override val text: String get() = when (this) { @@ -2312,6 +2328,10 @@ sealed class CIContent: ItemContent { is SndModerated -> generalGetString(MR.strings.moderated_description) is RcvModerated -> generalGetString(MR.strings.moderated_description) is RcvBlocked -> generalGetString(MR.strings.blocked_by_admin_item_description) + is SndDirectE2EEInfo -> directE2EEInfoToText(e2eeInfo) + is RcvDirectE2EEInfo -> directE2EEInfoToText(e2eeInfo) + is SndGroupE2EEInfo -> e2eeInfoNoPQText + is RcvGroupE2EEInfo -> e2eeInfoNoPQText is InvalidJSON -> "invalid data" } @@ -2330,6 +2350,15 @@ sealed class CIContent: ItemContent { } companion object { + fun directE2EEInfoToText(e2EEInfo: E2EEInfo): String = + if (e2EEInfo.pqEnabled) { + generalGetString(MR.strings.e2ee_info_pq) + } else { + e2eeInfoNoPQText + } + + private val e2eeInfoNoPQText: String = generalGetString(MR.strings.e2ee_info_no_pq) + fun featureText(feature: Feature, enabled: String, param: Int?): String = if (feature.hasParam) { "${feature.text}: ${timeText(param)}" @@ -2744,6 +2773,9 @@ enum class CIGroupInvitationStatus { @SerialName("expired") Expired; } +@Serializable +class E2EEInfo (val pqEnabled: Boolean) {} + object MsgContentSerializer : KSerializer { override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) { element("MCText", buildClassSerialDescriptor("MCText") { @@ -3097,6 +3129,7 @@ sealed class RcvConnEvent { @Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent() @Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState): RcvConnEvent() @Serializable @SerialName("verificationCodeReset") object VerificationCodeReset: RcvConnEvent() + @Serializable @SerialName("pqEnabled") class PQEnabled(val enabled: Boolean): RcvConnEvent() val text: String get() = when (this) { is SwitchQueue -> when (phase) { @@ -3105,6 +3138,11 @@ sealed class RcvConnEvent { } is RatchetSync -> ratchetSyncStatusToText(syncStatus) is VerificationCodeReset -> generalGetString(MR.strings.rcv_conn_event_verification_code_reset) + is PQEnabled -> if (enabled) { + generalGetString(MR.strings.conn_event_enabled_pq) + } else { + generalGetString(MR.strings.conn_event_disabled_pq) + } } } @@ -3122,6 +3160,7 @@ fun ratchetSyncStatusToText(ratchetSyncStatus: RatchetSyncState): String { sealed class SndConnEvent { @Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent() @Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState, val member: GroupMemberRef? = null): SndConnEvent() + @Serializable @SerialName("pqEnabled") class PQEnabled(val enabled: Boolean): SndConnEvent() val text: String get() = when (this) { @@ -3150,6 +3189,12 @@ sealed class SndConnEvent { } ratchetSyncStatusToText(syncStatus) } + + is PQEnabled -> if (enabled) { + generalGetString(MR.strings.conn_event_enabled_pq) + } else { + generalGetString(MR.strings.conn_event_disabled_pq) + } } } 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 1a20449695..9e3ce480df 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 @@ -156,6 +156,7 @@ class AppPreferences { val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false) val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false) val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null) + val pqExperimentalEnabled = mkBoolPreference(SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED, false) val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name) val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name) @@ -312,6 +313,7 @@ class AppPreferences { private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades" private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct" private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName" + private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme" private const val SHARED_PREFS_THEMES = "Themes" @@ -633,6 +635,15 @@ object ChatController { suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable)) + suspend fun apiSetPQEnabled(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetPQEnabled(enable)) + + suspend fun apiAllowContactPQ(rh: Long?, contactId: Long): Contact? { + val r = sendCmd(rh, CC.ApiAllowContactPQ(contactId)) + if (r is CR.ContactPQAllowed) return r.contact + apiErrorAlert("apiAllowContactPQ", "Error allowing contact PQ", r) + return null + } + suspend fun apiExportArchive(config: ArchiveConfig) { val r = sendCmd(null, CC.ApiExportArchive(config)) if (r is CR.CmdOk) return @@ -2016,6 +2027,10 @@ object ChatController { } } } + is CR.ContactPQEnabled -> + if (active(r.user)) { + chatModel.updateContact(rhId, r.contact) + } is CR.ChatCmdError -> when { r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> { chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart) @@ -2274,6 +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 ApiExportArchive(val config: ArchiveConfig): CC() class ApiImportArchive(val config: ArchiveConfig): CC() class ApiDeleteStorage: CC() @@ -2403,6 +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 ApiExportArchive -> "/_db export ${json.encodeToString(config)}" is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiDeleteStorage -> "/_db delete" @@ -2537,6 +2556,8 @@ sealed class CC { is SetFilesFolder -> "setFilesFolder" is SetRemoteHostsFolder -> "setRemoteHostsFolder" is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles" + is ApiSetPQEnabled -> "apiSetPQEnabled" + is ApiAllowContactPQ -> "apiAllowContactPQ" is ApiExportArchive -> "apiExportArchive" is ApiImportArchive -> "apiImportArchive" is ApiDeleteStorage -> "apiDeleteStorage" @@ -4002,6 +4023,10 @@ sealed class CR { @Serializable @SerialName("remoteCtrlSessionCode") class RemoteCtrlSessionCode(val remoteCtrl_: RemoteCtrlInfo?, val sessionCode: String): 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("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() @Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR() @Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR() @@ -4151,6 +4176,8 @@ sealed class CR { is RemoteCtrlSessionCode -> "remoteCtrlSessionCode" is RemoteCtrlConnected -> "remoteCtrlConnected" is RemoteCtrlStopped -> "remoteCtrlStopped" + is ContactPQAllowed -> "contactPQAllowed" + is ContactPQEnabled -> "contactPQEnabled" is VersionInfo -> "versionInfo" is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" @@ -4315,6 +4342,8 @@ sealed class CR { "\nsessionCode: $sessionCode" is RemoteCtrlConnected -> json.encodeToString(remoteCtrl) is RemoteCtrlStopped -> noDetails() + is ContactPQAllowed -> withUser(user, "contact: ${contact.id}") + 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" + "agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}" 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 7a7c2d7f24..6e30a89810 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,6 +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()) // 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 f195c723f6..97ec502f35 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 @@ -58,6 +58,7 @@ fun ChatInfoView( val currentUser = remember { chatModel.currentUser }.value val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() + val pqExperimentalEnabled = chatModel.controller.appPrefs.pqExperimentalEnabled.get() if (chat != null && currentUser != null) { val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) { mutableStateOf(chatModel.contactNetworkStatus(contact)) @@ -80,6 +81,7 @@ fun ChatInfoView( localAlias, connectionCode, developerTools, + pqExperimentalEnabled, onLocalAliasChanged = { setContactAlias(chat, it, chatModel) }, @@ -138,6 +140,17 @@ fun ChatInfoView( } }) }, + allowContactPQ = { + showAllowContactPQAlert(allowContactPQ = { + withBGApi { + val ct = chatModel.controller.apiAllowContactPQ(chatRh, contact.contactId) + if (ct != null) { + chatModel.updateContact(chatRh, contact) + } + close.invoke() + } + }) + }, verifyClicked = { ModalManager.end.showModalCloseable { close -> remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct -> @@ -288,6 +301,7 @@ fun ChatInfoLayout( localAlias: String, connectionCode: String?, developerTools: Boolean, + pqExperimentalEnabled: Boolean, onLocalAliasChanged: (String) -> Unit, openPreferences: () -> Unit, deleteContact: () -> Unit, @@ -296,6 +310,7 @@ fun ChatInfoLayout( abortSwitchContactAddress: () -> Unit, syncContactConnection: () -> Unit, syncContactConnectionForce: () -> Unit, + allowContactPQ: () -> Unit, verifyClicked: () -> Unit, ) { val cStats = connStats.value @@ -345,6 +360,18 @@ fun ChatInfoLayout( SectionDividerSpaced() } + 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) { + AllowContactPQButton(allowContactPQ) + SectionTextFooter("After allowing post-quantum encryption, it will be enabled after several messages if your contact also allows it.") + } + SectionDividerSpaced() + } + } + if (contact.contactLink != null) { SectionView(stringResource(MR.strings.address_section_title).uppercase()) { SimpleXLinkQRCode(contact.contactLink) @@ -601,6 +628,17 @@ fun SynchronizeConnectionButtonForce(syncConnectionForce: () -> Unit) { ) } +@Composable +fun AllowContactPQButton(allowContactPQ: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_warning), + "Allow PQ encryption", + click = allowContactPQ, + textColor = WarningOrange, + iconColor = WarningOrange + ) +} + @Composable fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) { SettingsActionItem( @@ -704,6 +742,16 @@ 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!", + confirmText = "Allow", + onConfirm = allowContactPQ, + destructive = true, + ) +} + @Preview @Composable fun PreviewChatInfoLayout() { @@ -721,6 +769,7 @@ fun PreviewChatInfoLayout() { localAlias = "", connectionCode = "123", developerTools = false, + pqExperimentalEnabled = false, connStats = remember { mutableStateOf(null) }, contactNetworkStatus = NetworkStatus.Connected(), onLocalAliasChanged = {}, @@ -732,6 +781,7 @@ fun PreviewChatInfoLayout() { abortSwitchContactAddress = {}, syncContactConnection = {}, syncContactConnectionForce = {}, + allowContactPQ = {}, verifyClicked = {}, ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index ccb9683240..cce4307d1f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -452,6 +452,11 @@ fun ChatItemView( is CIContent.SndModerated -> DeletedItem() is CIContent.RcvModerated -> DeletedItem() is CIContent.RcvBlocked -> DeletedItem() + // TODO proper items + is CIContent.SndDirectE2EEInfo -> CIEventView(buildAnnotatedString { append(cItem.content.text) }) + is CIContent.RcvDirectE2EEInfo -> CIEventView(buildAnnotatedString { append(cItem.content.text) }) + is CIContent.SndGroupE2EEInfo -> CIEventView(buildAnnotatedString { append(cItem.content.text) }) + is CIContent.RcvGroupE2EEInfo -> CIEventView(buildAnnotatedString { append(cItem.content.text) }) is CIContent.InvalidJSON -> CIInvalidJSONView(c.json) } } 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 cc268e9a9d..5dca1527f2 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 @@ -58,6 +58,14 @@ fun DeveloperView( SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors) SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls) } + + 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) } + }) + SectionTextFooter("In this version applies only to new contacts.") + } } SectionBottomSpacer() } 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 7288d88431..7abd5c8a49 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -54,6 +54,9 @@ Decryption error Encryption re-negotiation error + This conversation is protected by end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery. + This conversation is protected by quantum resistant end-to-end encryption. It has perfect forward secrecy, repudiation and quantum resistant break-in recovery. + Private notes @@ -1239,6 +1242,8 @@ agreeing encryption for %s… encryption agreed for %s security code changed + enabled post-quantum encryption + disabled post-quantum encryption observer