android: pq support; ios: fixes (#3878)

This commit is contained in:
spaced4ndy authored and GitHub committed 2024-03-08 16:39:15 +04:00
1 parent 109b6e0cff
commit 405348732b
9 files changed
+151 -6

No files matched your search

@@ -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."))
}
}
+5 -3
View File
@@ -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
)
}
@@ -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<MsgContent> {
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)
}
}
}
@@ -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<UpMigration>, val agentMigrations: List<UpMigration>): 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 })}"
@@ -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)
@@ -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 = {},
)
}
@@ -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)
}
}
@@ -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()
}
@@ -54,6 +54,9 @@
<string name="decryption_error">Decryption error</string>
<string name="encryption_renegotiation_error">Encryption re-negotiation error</string>
<string name="e2ee_info_no_pq">This conversation is protected by end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery.</string>
<string name="e2ee_info_pq">This conversation is protected by quantum resistant end-to-end encryption. It has perfect forward secrecy, repudiation and quantum resistant break-in recovery.</string>
<!-- NoteFolder - ChatModel.kt -->
<string name="note_folder_local_display_name">Private notes</string>
@@ -1239,6 +1242,8 @@
<string name="snd_conn_event_ratchet_sync_started">agreeing encryption for %s…</string>
<string name="snd_conn_event_ratchet_sync_agreed">encryption agreed for %s</string>
<string name="rcv_conn_event_verification_code_reset">security code changed</string>
<string name="conn_event_enabled_pq">enabled post-quantum encryption</string>
<string name="conn_event_disabled_pq">disabled post-quantum encryption</string>
<!-- GroupMemberRole -->
<string name="group_member_role_observer">observer</string>