From ce9b909495f7b7a9d2ca69e79c56a8fe65eff049 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 7 Mar 2024 16:43:10 +0400 Subject: [PATCH] ios: pq support (#3870) * ios: pq support * fix * fix * update * text * rename --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Model/SimpleXAPI.swift | 19 +++++++ apps/ios/Shared/Views/Chat/ChatInfoView.swift | 57 +++++++++++++++++++ apps/ios/Shared/Views/Chat/ChatItemView.swift | 5 ++ .../Views/UserSettings/DeveloperView.swift | 25 ++++++++ apps/ios/SimpleXChat/APITypes.swift | 13 +++++ apps/ios/SimpleXChat/AppGroup.swift | 4 ++ apps/ios/SimpleXChat/ChatTypes.swift | 54 +++++++++++++++++- 7 files changed, 174 insertions(+), 3 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 1fc6b54390..20975dfe3c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -258,6 +258,18 @@ func apiSetEncryptLocalFiles(_ enable: Bool) throws { throw r } +func apiSetPQEnabled(_ enable: Bool) throws { + let r = chatSendCmdSync(.apiSetPQEnabled(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 } + throw r +} + func apiExportArchive(config: ArchiveConfig) async throws { try await sendCommandOkResp(.apiExportArchive(config: config)) } @@ -1244,6 +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()) m.chatInitialized = true m.currentUser = try apiGetActiveUser() if m.currentUser == nil { @@ -1818,6 +1831,12 @@ func processReceivedMsg(_ res: ChatResponse) async { } } } + case let .contactPQEnabled(user, contact, _): + if active(user) { + await MainActor.run { + m.updateContact(contact) // or updateContactConnectionStats? + } + } default: logger.debug("unsupported event: \(res.responseType)") } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index b702c2cc23..07d83ac475 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -103,6 +103,7 @@ struct ChatInfoView: View { @State private var sendReceipts = SendReceipts.userDefault(true) @State private var sendReceiptsUserDefault = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @AppStorage(GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED, store: groupDefaults) private var pqExperimentalEnabled = false enum ChatInfoViewAlert: Identifiable { case clearChatAlert @@ -110,6 +111,7 @@ struct ChatInfoView: View { case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert + case allowContactPQEncryptionAlert case error(title: LocalizedStringKey, error: LocalizedStringKey = "") var id: String { @@ -119,6 +121,7 @@ struct ChatInfoView: View { case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" + case .allowContactPQEncryptionAlert: return "allowContactPQEncryptionAlert" case let .error(title, _): return "error \(title)" } } @@ -165,6 +168,22 @@ struct ChatInfoView: View { } .disabled(!contact.ready || !contact.active) + if pqExperimentalEnabled, + let conn = contact.activeConn { + Section { + infoRow(Text(String("PQ E2E encryption")), conn.connPQEnabled ? "Enabled" : "Disabled") + if !conn.enablePQ { + allowPQButton() + } + } header: { + Text(String("Post-quantum E2E encryption")) + } footer: { + if !conn.enablePQ { + Text(String("After allowing post-quantum encryption, it will be enabled after several messages if your contact also allows it.")) + } + } + } + if let contactLink = contact.contactLink { Section { SimpleXLinkQRCode(uri: contactLink) @@ -237,6 +256,7 @@ struct ChatInfoView: View { case .switchAddressAlert: return switchAddressAlert(switchContactAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) }) + case .allowContactPQEncryptionAlert: return allowContactPQEncryptionAlert() case let .error(title, error): return mkAlert(title: title, message: error) } } @@ -410,6 +430,15 @@ struct ChatInfoView: View { } } + private func allowPQButton() -> some View { + Button { + alert = .allowContactPQEncryptionAlert + } label: { + Label(String("Allow PQ encryption"), systemImage: "exclamationmark.triangle") + .foregroundColor(.orange) + } + } + private func networkStatusRow() -> some View { HStack { Text("Network status") @@ -543,6 +572,34 @@ struct ChatInfoView: View { } } } + + private func allowContactPQEncryption() { + Task { + do { + let ct = try await apiAllowContactPQ(contact.apiId) + contact = ct + await MainActor.run { + chatModel.updateContact(contact) + dismiss() + } + } catch let error { + logger.error("allowContactPQEncryption apiAllowContactPQ error: \(responseError(error))") + let a = getErrorAlert(error, "Error allowing contact PQ encryption") + await MainActor.run { + alert = .error(title: a.title, error: a.message) + } + } + } + } + + 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!")), + primaryButton: .destructive(Text(String("Allow")), action: allowContactPQEncryption), + secondaryButton: .cancel() + ) + } } func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert { diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 8f67a8f737..d9404547e2 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -111,6 +111,11 @@ struct ChatItemContentView: View { case .rcvModerated: deletedItemView() case .rcvBlocked: deletedItemView() case let .invalidJSON(json): CIInvalidJSONView(json: json) + // TODO proper items + case .sndDirectE2EEInfo: CIEventView(eventText: Text(chatItem.content.text)) + case .rcvDirectE2EEInfo: CIEventView(eventText: Text(chatItem.content.text)) + case .sndGroupE2EEInfo: CIEventView(eventText: Text(chatItem.content.text)) + case .rcvGroupE2EEInfo: CIEventView(eventText: Text(chatItem.content.text)) } } diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index 3bbfbfe33e..816b46c54f 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -12,6 +12,7 @@ import SimpleXChat struct DeveloperView: View { @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false + @AppStorage(GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED, store: groupDefaults) private var pqExperimentalEnabled = false @Environment(\.colorScheme) var colorScheme var body: some View { @@ -42,9 +43,33 @@ struct DeveloperView: View { } footer: { (developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.") } + + if developerTools { + Section { + settingsRow("key") { + Toggle("Post-quantum E2EE", isOn: $pqExperimentalEnabled) + .onChange(of: pqExperimentalEnabled) { + setPQExperimentalEnabled($0) + } + } + } header: { + Text(String("Experimental")) + } footer: { + Text(String("In this version applies only to new contacts.")) + } + } } } } + + private func setPQExperimentalEnabled(_ enable: Bool) { + do { + try apiSetPQEnabled(enable) + } catch let error { + let err = responseError(error) + logger.error("apiSetPQEnabled \(err)") + } + } } struct DeveloperView_Previews: PreviewProvider { diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 88bb8910dd..6bb3fbb3c2 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -32,6 +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 apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) case apiDeleteStorage @@ -162,6 +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 .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" case .apiDeleteStorage: return "/_db delete" @@ -306,6 +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 .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" case .apiDeleteStorage: return "apiDeleteStorage" @@ -617,6 +623,9 @@ public enum ChatResponse: Decodable, Error { case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String) case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo) case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason) + // pq + case contactPQAllowed(user: UserRef, contact: Contact) + case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool) // misc case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) case cmdOk(user: UserRef?) @@ -765,6 +774,8 @@ public enum ChatResponse: Decodable, Error { case .remoteCtrlSessionCode: return "remoteCtrlSessionCode" case .remoteCtrlConnected: return "remoteCtrlConnected" case .remoteCtrlStopped: return "remoteCtrlStopped" + case .contactPQAllowed: return "contactPQAllowed" + case .contactPQEnabled: return "contactPQAllowed" case .versionInfo: return "versionInfo" case .cmdOk: return "cmdOk" case .chatCmdError: return "chatCmdError" @@ -915,6 +926,8 @@ 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 .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 case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index ceb7d9d7db..47e250b7e9 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -39,6 +39,7 @@ let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase" let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase" public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades" public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled" +public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled" public let APP_GROUP_NAME = "group.chat.simplex.app" @@ -67,6 +68,7 @@ public func registerGroupDefaults() { GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false, GROUP_DEFAULT_CALL_KIT_ENABLED: true, + GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false, ]) } @@ -193,6 +195,8 @@ public let confirmDBUpgradesGroupDefault = BoolDefault(defaults: groupDefaults, public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED) +public let pqExperimentalEnabledDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES) + public class DateDefault { var defaults: UserDefaults var key: String diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 997f6e3537..0125973d14 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1532,22 +1532,30 @@ public struct Connection: Decodable { public var viaGroupLink: Bool public var customUserProfileId: Int64? public var connectionCode: SecurityCode? + public var enablePQ: 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 + case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, enablePQ, pqSndEnabled, pqRcvEnabled } public var id: ChatId { get { ":\(connId)" } } + public var connPQEnabled: Bool { + pqSndEnabled == true && pqRcvEnabled == true + } + static let sampleData = Connection( connId: 1, agentConnId: "abc", peerChatVRange: VersionRange(minVersion: 1, maxVersion: 1), connStatus: .ready, connLevel: 0, - viaGroupLink: false + viaGroupLink: false, + enablePQ: false ) } @@ -2300,6 +2308,10 @@ public struct ChatItem: Identifiable, Decodable { case .sndModerated: return false case .rcvModerated: return false case .rcvBlocked: return false + case .sndDirectE2EEInfo: return false + case .rcvDirectE2EEInfo: return false + case .sndGroupE2EEInfo: return false + case .rcvGroupE2EEInfo: return false case .invalidJSON: return false } } @@ -2735,6 +2747,10 @@ public enum CIContent: Decodable, ItemContent { case sndModerated case rcvModerated case rcvBlocked + case sndDirectE2EEInfo(e2eeInfo: E2EEInfo) + case rcvDirectE2EEInfo(e2eeInfo: E2EEInfo) + case sndGroupE2EEInfo(e2eeInfo: E2EEInfo) + case rcvGroupE2EEInfo(e2eeInfo: E2EEInfo) case invalidJSON(json: String) public var text: String { @@ -2766,11 +2782,25 @@ public enum CIContent: Decodable, ItemContent { case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item") case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item") case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item") + case let .sndDirectE2EEInfo(e2eeInfo): return directE2EEInfoToText(e2eeInfo) + case let .rcvDirectE2EEInfo(e2eeInfo): return directE2EEInfoToText(e2eeInfo) + case .sndGroupE2EEInfo: return e2eeInfoNoPQText + case .rcvGroupE2EEInfo: return e2eeInfoNoPQText case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item") } } } + private func directE2EEInfoToText(_ e2eeInfo: E2EEInfo) -> String { + e2eeInfo.pqEnabled + ? NSLocalizedString("This conversation is protected by quantum resistant end-to-end encryption. It has perfect forward secrecy, repudiation and quantum resistant break-in recovery.", comment: "E2EE info chat item") + : e2eeInfoNoPQText + } + + private var e2eeInfoNoPQText: String { + NSLocalizedString("This conversation is protected by end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery.", comment: "E2EE info chat item") + } + static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String { feature.hasParam ? "\(feature.text): \(timeText(param))" @@ -3457,6 +3487,10 @@ public enum CIGroupInvitationStatus: String, Decodable { case expired } +public struct E2EEInfo: Decodable { + public var pqEnabled: Bool +} + public enum RcvDirectEvent: Decodable { case contactDeleted case profileUpdated(fromProfile: Profile, toProfile: Profile) @@ -3574,7 +3608,8 @@ public enum RcvConnEvent: Decodable { case switchQueue(phase: SwitchPhase) case ratchetSync(syncStatus: RatchetSyncState) case verificationCodeReset - + case pqEnabled(enabled: Bool) + var text: String { switch self { case let .switchQueue(phase): @@ -3586,6 +3621,12 @@ public enum RcvConnEvent: Decodable { return ratchetSyncStatusToText(syncStatus) case .verificationCodeReset: 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") + } else { + return NSLocalizedString("disabled post-quantum encryption", comment: "chat item text") + } } } } @@ -3603,6 +3644,7 @@ func ratchetSyncStatusToText(_ ratchetSyncStatus: RatchetSyncState) -> String { public enum SndConnEvent: Decodable { case switchQueue(phase: SwitchPhase, member: GroupMemberRef?) case ratchetSync(syncStatus: RatchetSyncState, member: GroupMemberRef?) + case pqEnabled(enabled: Bool) var text: String { switch self { @@ -3626,6 +3668,12 @@ public enum SndConnEvent: Decodable { } } return ratchetSyncStatusToText(syncStatus) + case let .pqEnabled(enabled): + if enabled { + return NSLocalizedString("enabled post-quantum encryption", comment: "chat item text") + } else { + return NSLocalizedString("disabled post-quantum encryption", comment: "chat item text") + } } } }