From e2bce84de3416b21322b908cb080cc57e1c3f820 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Mon, 4 Nov 2024 02:37:33 -0600 Subject: [PATCH 1/2] #2548 Add warning for suspicious message (#2626) * feat: added warning for suspicious message * fix: machine type * fix: fastlane file * fix: fastlane file * fix: fastlane file * fix: semaphoreci * fix: use xcode 15 image * fix: pr reviews * fix: removed unused test --- .semaphore/semaphore.yml | 2 +- FlowCrypt.xcodeproj/project.pbxproj | 4 + ...hreadDetailsViewController+TableView.swift | 15 +++- .../Mail Provider/Gmail/GmailService.swift | 1 + .../Gmail+MessageExtension.swift | 5 +- .../MessagesList Provider/Model/Message.swift | 5 +- .../Resources/en.lproj/Localizable.strings | 3 + .../Cell Nodes/SecurityWarningNode.swift | 43 +++++++++++ .../message-export-1922cd70bf323ea6.json | 74 +++++++++++++++++++ .../api-mocks/apis/google/google-messages.ts | 1 + appium/tests/screenobjects/email.screen.ts | 20 +++++ .../CheckMessageProcessingErrors.spec.ts | 7 ++ 12 files changed, 173 insertions(+), 7 deletions(-) create mode 100644 FlowCryptUI/Cell Nodes/SecurityWarningNode.swift create mode 100644 appium/api-mocks/apis/google/exported-messages/message-export-1922cd70bf323ea6.json diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 6c65d4e83..93aca560e 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -114,4 +114,4 @@ after_pipeline: jobs: - name: Publish Results commands: - - test-results gen-pipeline-report + - test-results gen-pipeline-report \ No newline at end of file diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 06f1b2b1a..2f867087b 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -420,6 +420,7 @@ D2FD0F692453245E00259FF0 /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FD0F682453245E00259FF0 /* Either.swift */; }; D2FF6966243115EC007182F0 /* SetupImapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FF6965243115EC007182F0 /* SetupImapViewController.swift */; }; D2FF6968243115F9007182F0 /* SetupImapViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FF6967243115F9007182F0 /* SetupImapViewDecorator.swift */; }; + D741F9B22CA5661C00E1CAFF /* SecurityWarningNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D741F9B12CA5661400E1CAFF /* SecurityWarningNode.swift */; }; F191F621272511790053833E /* BlurViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F191F620272511790053833E /* BlurViewController.swift */; }; F8678DCC2722143300BB1710 /* GmailService+draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8678DCB2722143300BB1710 /* GmailService+draft.swift */; }; F8A72FA12729F82800E4BCAB /* DraftGatewayMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A72FA02729F82800E4BCAB /* DraftGatewayMock.swift */; }; @@ -886,6 +887,7 @@ D2FD0F682453245E00259FF0 /* Either.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Either.swift; sourceTree = ""; }; D2FF6965243115EC007182F0 /* SetupImapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupImapViewController.swift; sourceTree = ""; }; D2FF6967243115F9007182F0 /* SetupImapViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupImapViewDecorator.swift; sourceTree = ""; }; + D741F9B12CA5661400E1CAFF /* SecurityWarningNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityWarningNode.swift; sourceTree = ""; }; E26D5E20275AA417007B8802 /* BundleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = ""; }; F191F620272511790053833E /* BlurViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurViewController.swift; sourceTree = ""; }; F8678DCB2722143300BB1710 /* GmailService+draft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GmailService+draft.swift"; sourceTree = ""; }; @@ -2212,6 +2214,7 @@ D2A1D3D123FD9A9E00D626D6 /* Cell Nodes */ = { isa = PBXGroup; children = ( + D741F9B12CA5661400E1CAFF /* SecurityWarningNode.swift */, 9F8D5E61236B04E300186E43 /* CellNode.swift */, 9F1797652368EE50002BF770 /* SetupTitleNode.swift */, 5A39F433239EC61C001F4607 /* TitleCellNode.swift */, @@ -2904,6 +2907,7 @@ D2CDC3D72404704D002B045F /* RecipientEmailsCellNode.swift in Sources */, 5165ABCC27B526D100CCC379 /* RecipientEmailTextFieldNode.swift in Sources */, 51C56BE82901867D00610D12 /* ENSideMenu.swift in Sources */, + D741F9B22CA5661C00E1CAFF /* SecurityWarningNode.swift in Sources */, 955475FB2B0650AC00F52076 /* WebNode.swift in Sources */, D2717752242567EB00BDA9A9 /* KeyTextCellNode.swift in Sources */, 511D07E12769FBBA0050417B /* MessageActionCellNode.swift in Sources */, diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TableView.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TableView.swift index 2e2b2479d..a8ea7e780 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TableView.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TableView.swift @@ -27,7 +27,8 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { let processedMessage = input[section - 1].processedMessage let attachmentsCount = processedMessage?.attachments.count ?? 0 let pubkeysCount = processedMessage?.keyDetails.count ?? 0 - return Parts.allCases.count + attachmentsCount + pubkeysCount + let securityWarningBlockCount = processedMessage?.message.isSuspicious == true ? 1 : 0 + return Parts.allCases.count + attachmentsCount + pubkeysCount + securityWarningBlockCount } // swiftlint:disable cyclomatic_complexity function_body_length @@ -65,10 +66,16 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } } + if message.rawMessage.isSuspicious, row == 1 { + return SecurityWarningNode() + } + guard message.isExpanded, let processedMessage = message.processedMessage else { return self.dividerNode(indexPath: indexPath) } - guard row > 1 else { + let securityWarningBlockCount = message.rawMessage.isSuspicious == true ? 1 : 0 + + guard row > 1 + securityWarningBlockCount else { if processedMessage.text.isHTMLString { return ThreadDetailWebNode( input: .init(message: processedMessage.text, quote: processedMessage.quote, index: messageIndex) @@ -85,7 +92,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } let keyCount = processedMessage.keyDetails.count - let keyIndex = row - 2 + let keyIndex = row - 2 - securityWarningBlockCount if keyIndex < keyCount { let keyDetails = processedMessage.keyDetails[keyIndex] let node = PublicKeyDetailNode( @@ -97,7 +104,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { return node } - let attachmentIndex = row - 2 - keyCount + let attachmentIndex = row - 2 - keyCount - securityWarningBlockCount if let attachment = processedMessage.attachments[safe: attachmentIndex] { return AttachmentNode( input: .init( diff --git a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift index 23ec145e5..ed40e5c2c 100644 --- a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift @@ -60,5 +60,6 @@ extension String { static let bcc = "bcc" static let replyTo = "reply-to" static let inReplyTo = "in-reply-to" + static let receivedSPF = "received-spf" static let identifier = "message-id" } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift index 78c25099d..0b7d6a6de 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+MessageExtension.swift @@ -44,6 +44,7 @@ extension Message { var replyTo: String? var inReplyTo: String? var rfc822MsgId: String? + var isSuspicious = false for messageHeader in messageHeaders.compactMap({ $0 }) { guard let name = messageHeader.name?.lowercased(), @@ -58,6 +59,7 @@ extension Message { case .bcc: bcc = value case .replyTo: replyTo = value case .inReplyTo: inReplyTo = value + case .receivedSPF: isSuspicious = value.contains("softfail") case .identifier: rfc822MsgId = value default: break } @@ -81,7 +83,8 @@ extension Message { cc: cc, bcc: bcc, replyTo: replyTo, - inReplyTo: inReplyTo + inReplyTo: inReplyTo, + isSuspicious: isSuspicious ) } } diff --git a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift index d16a2586e..a0c795e8b 100644 --- a/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/MessagesList Provider/Model/Message.swift @@ -28,6 +28,7 @@ struct Message: Hashable { let body: MessageBody let inReplyTo: String? let replyToMsgId: String? + let isSuspicious: Bool private(set) var labels: [MessageLabel] var isRead: Bool { @@ -83,7 +84,8 @@ struct Message: Hashable { bcc: String? = nil, replyTo: String? = nil, inReplyTo: String? = nil, - replyToMsgId: String? = nil + replyToMsgId: String? = nil, + isSuspicious: Bool = false ) { self.identifier = identifier self.date = date @@ -104,6 +106,7 @@ struct Message: Hashable { self.replyTo = Self.parseRecipients(replyTo) self.inReplyTo = inReplyTo self.replyToMsgId = replyToMsgId + self.isSuspicious = isSuspicious } } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 44b652fa0..4a1f4f3e0 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -72,6 +72,9 @@ "message_mark_read_error" = "Could not mark message as read: %@"; "message_reply_all" = "Reply all"; "message_not_found_in_folder" = "Message not found in folder: "; +"message_security_warning_subject" = "Potentially suspicious message"; +"message_security_warning_message" = "It wasn't properly verified by the sender, so its authenticity can't be confirmed. +Be careful - avoid clicking links and downloading attachments, or sharing personal info."; // Passphrase Anti BruteForce Protection "passphrase_anti_brute_force_protection_hint" = "To protect you and your data, the next attempt will only be possible after the timer below finishes. Please wait until then before trying again."; diff --git a/FlowCryptUI/Cell Nodes/SecurityWarningNode.swift b/FlowCryptUI/Cell Nodes/SecurityWarningNode.swift new file mode 100644 index 000000000..7a9b88812 --- /dev/null +++ b/FlowCryptUI/Cell Nodes/SecurityWarningNode.swift @@ -0,0 +1,43 @@ +// +// SecurityWarningNode.swift +// FlowCrypt +// +// Created by Ioan Moldovan on 9/26/24 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +public final class SecurityWarningNode: CellNode { + + private lazy var subjectNode: ASTextNode2 = { + let textNode = ASTextNode2() + textNode.attributedText = "message_security_warning_subject".localized.attributed(.bold(18), color: .black, alignment: .left) + textNode.accessibilityIdentifier = "aid-security-warning-subject-node" + return textNode + }() + + private lazy var messageNode: ASTextNode2 = { + let textNode = ASTextNode2() + textNode.attributedText = "message_security_warning_message".localized.attributed(.regular(15), color: .black, alignment: .left) + textNode.accessibilityIdentifier = "aid-security-warning-message-node" + return textNode + }() + + override public init() { + super.init() + + backgroundColor = UIColor(hex: "FABD03") + } + + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + let stack = ASStackLayoutSpec.vertical() + stack.children = [subjectNode, messageNode] + stack.spacing = 10 + + return ASInsetLayoutSpec( + insets: .deviceSpecificTextInsets(top: 16, bottom: 16), + child: stack + ) + } +} diff --git a/appium/api-mocks/apis/google/exported-messages/message-export-1922cd70bf323ea6.json b/appium/api-mocks/apis/google/exported-messages/message-export-1922cd70bf323ea6.json new file mode 100644 index 000000000..b00b39cf7 --- /dev/null +++ b/appium/api-mocks/apis/google/exported-messages/message-export-1922cd70bf323ea6.json @@ -0,0 +1,74 @@ +{ + "acctEmail": "e2e.enterprise.test@flowcrypt.com", + "full": { + "id": "1922cd70bf323ea6", + "threadId": "1922cd70bf323ea6", + "labelIds": ["CATEGORY_PERSONAL", "INBOX"], + "snippet": "test -Mart at FlowCrypt", + "payload": { + "partId": "", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { + "name": "To", + "value": "flowcrypt.compatibility@gmail.com" + }, + { + "name": "Subject", + "value": "Test Spoofed email by Mart" + }, + { + "name": "From", + "value": "sender@domain.com" + }, + { + "name": "X-Priority", + "value": "3 (Normal)" + }, + { + "name": "Importance", + "value": "Normal" + }, + { + "name": "Received-SPF", + "value": "softfail (google.com: domain of transitioning hacker@gmail.com does not designate 0.0.0.0 as permitted sender) client-ip=0.0.0.0;" + }, + { + "name": "Errors-To", + "value": "hacker@gmail.com" + }, + { + "name": "Reply-To", + "value": "hacker@gmail.com" + }, + { + "name": "Content-Type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "Date", + "value": "Thu, 26 Sep 2024 07:39:02 +0200 (CEST)" + } + ], + "body": { + "size": 28, + "data": "dGVzdA0KDQotTWFydCBhdCBGbG93Q3J5cHQNCg==" + } + }, + "sizeEstimate": 3183, + "historyId": "298166", + "internalDate": "1727329142000" + }, + "attachments": {}, + "raw": { + "id": "1922cd70bf323ea6", + "threadId": "1922cd70bf323ea6", + "labelIds": ["CATEGORY_PERSONAL", "SPAM"], + "snippet": "test -Mart at FlowCrypt", + "sizeEstimate": 3183, + "raw": "RGVsaXZlcmVkLVRvOiBpb2FuQGZsb3djcnlwdC5jb20NClJlY2VpdmVkOiBieSAyMDAyOmEwNTo2MzU5OjQ1OWE6YjA6MWJlOmI4N2E6NDZhNCB3aXRoIFNNVFAgaWQgbm8yNmNzcDE1MDk5MXJ3YjsNCiAgICAgICAgV2VkLCAyNSBTZXAgMjAyNCAyMjozOTowMyAtMDcwMCAoUERUKQ0KWC1Hb29nbGUtU210cC1Tb3VyY2U6IEFHSFQrSUhocFVnem42dE4ycnAyUDc0NkNKQ1JDZ1NXZXEyYndWdVFDNWFpcXFFSThOdGhhSTlER0JJb2V5V3VRTWFQeXVCOUV2WmoNClgtUmVjZWl2ZWQ6IGJ5IDIwMDI6YTE3OjkwYjozMTQ0OmIwOjJkODo3N2NjOjg1ZSB3aXRoIFNNVFAgaWQgOThlNjdlZDU5ZTFkMS0yZTA2YWZlYjc4ZW1yNjMzMTEzMGE5MS4zNy4xNzI3MzI5MTQzNTQ1Ow0KICAgICAgICBXZWQsIDI1IFNlcCAyMDI0IDIyOjM5OjAzIC0wNzAwIChQRFQpDQpBUkMtU2VhbDogaT0xOyBhPXJzYS1zaGEyNTY7IHQ9MTcyNzMyOTE0MzsgY3Y9bm9uZTsNCiAgICAgICAgZD1nb29nbGUuY29tOyBzPWFyYy0yMDI0MDYwNTsNCiAgICAgICAgYj1TOVRyZzluaUsyQURsQWQrWnYwZE9CbnhBVStvYlN5RkUyUkgvVFFucS9ZL2M2VmNCd3pkcmM4d3A4Y3BJL1ZUUWsNCiAgICAgICAgIDJjc3F6TEIzczRCQjBTMFB0U0pFd0VJTUUvd2J1SWtTUWl5RkpjOURsY3R2VFBST1RhSmNHc0xKTmQvWlBvRzhMQTJCDQogICAgICAgICBpZVJ6YlpsbWs2TmpKMFFPVURNZlVzYU8reXp0dzcwSmU1OWgwbjZQVUoyWTloNW5UdXFpYzFzbmpqZVc5b0dPcDF3Mg0KICAgICAgICAgWWJGQzFrK09GbjkyZElxeFZkNHZsNnp5blBrZytxSFoyQi9HSTE1ZHRjaFIyZFpXOEJlcWh3cU00TXIvdFFpVE9TQmgNCiAgICAgICAgIDRSdjBzTm1yam5LSVJCR2JPRWVBMHNMTndWam5kTWJjaDMwVGdIb1Y1NEoxakcyQVZnY1F0aDFTTjVuc0ovOVJoL0d3DQogICAgICAgICB6VWp3PT0NCkFSQy1NZXNzYWdlLVNpZ25hdHVyZTogaT0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOyBkPWdvb2dsZS5jb207IHM9YXJjLTIwMjQwNjA1Ow0KICAgICAgICBoPWRhdGU6bWVzc2FnZS1pZDpyZXBseS10bzplcnJvcnMtdG86aW1wb3J0YW5jZTpmcm9tOnN1YmplY3Q6dG87DQogICAgICAgIGJoPWNXY3VTNmxuVTE4VTU4YTUwT3dqelFzMGEwK3FsNGxZbnBDSEZoZ3Yxalk9Ow0KICAgICAgICBmaD1yVjZaTXJhY21nOVg1TUdtam9YdVJ6YmZ4dGpEUldSZkEzWkZLQTNRZC9jPTsNCiAgICAgICAgYj1MaTRxMjRuNXUxajFPWEFqUG1JN0RDNlFXOVJTd2tSOUFjaHd6bEJkb0ZLbXYwdmpXclo3dVFQS2UyMUVGZnlJUVQNCiAgICAgICAgIFk5M3pQbWg5U1RacVFaV3FGTS9SaXJGTFlRRjV2UGpZTzdVRWpiSkoyS05lYlFtS3lNT1ZDN2UydlNxdzRsWVQ1bUh6DQogICAgICAgICAwUEpmbzNUK2cvWUE2UU0zbldmbjIzU1pONHRTS3hIU1c2VXh6N3F0S3FCNXFtQjJSalFrUFZod0FGMEw3SFEva0RWNA0KICAgICAgICAgWk1QaDVuMUR2SHdqWDlxRURWUmk1d3NQdGg4Y1NRbEY2dnFSdXZjZzhyVjdGajZnRzA1QVFyeFp2bTQ2TVVMU0t0ckINCiAgICAgICAgIG5NYjhQYmpreEdJUHdBQ1gyODNzbDRKWEh3Q1ZiTVdYRHEzeDZPck01a2tKdmxJcGdLd2l6UEhOOGs2cW9Ud1pmbHV2DQogICAgICAgICBvMjNnPT07DQogICAgICAgIGRhcmE9Z29vZ2xlLmNvbQ0KQVJDLUF1dGhlbnRpY2F0aW9uLVJlc3VsdHM6IGk9MTsgbXguZ29vZ2xlLmNvbTsNCiAgICAgICBzcGY9c29mdGZhaWwgKGdvb2dsZS5jb206IGRvbWFpbiBvZiB0cmFuc2l0aW9uaW5nIGhhY2tlckBnbWFpbC5jb20gZG9lcyBub3QgZGVzaWduYXRlIDExNC4yOS4yMzYuMjQ3IGFzIHBlcm1pdHRlZCBzZW5kZXIpIHNtdHAubWFpbGZyb209aGFja2VyQGdtYWlsLmNvbTsNCiAgICAgICBkbWFyYz1mYWlsIChwPU5PTkUgc3A9UVVBUkFOVElORSBkaXM9Tk9ORSkgaGVhZGVyLmZyb209Z21haWwuY29tDQpSZXR1cm4tUGF0aDogPGhhY2tlckBnbWFpbC5jb20-DQpSZWNlaXZlZDogZnJvbSBlbWtlaS5jeiAoZW1rZWkuY3ouIFsxMTQuMjkuMjM2LjI0N10pDQogICAgICAgIGJ5IG14Lmdvb2dsZS5jb20gd2l0aCBFU01UUFMgaWQgOThlNjdlZDU5ZTFkMS0yZTA2ZTJmMjQ2Y3NpMzAzNTc1NWE5MS4xNTkuMjAyNC4wOS4yNS4yMi4zOS4wMw0KICAgICAgICBmb3IgPGlvYW5AZmxvd2NyeXB0LmNvbT4NCiAgICAgICAgKHZlcnNpb249VExTMV8zIGNpcGhlcj1UTFNfQUVTXzI1Nl9HQ01fU0hBMzg0IGJpdHM9MjU2LzI1Nik7DQogICAgICAgIFdlZCwgMjUgU2VwIDIwMjQgMjI6Mzk6MDMgLTA3MDAgKFBEVCkNClJlY2VpdmVkLVNQRjogc29mdGZhaWwgKGdvb2dsZS5jb206IGRvbWFpbiBvZiB0cmFuc2l0aW9uaW5nIGhhY2tlckBnbWFpbC5jb20gZG9lcyBub3QgZGVzaWduYXRlIDExNC4yOS4yMzYuMjQ3IGFzIHBlcm1pdHRlZCBzZW5kZXIpIGNsaWVudC1pcD0xMTQuMjkuMjM2LjI0NzsNCkF1dGhlbnRpY2F0aW9uLVJlc3VsdHM6IG14Lmdvb2dsZS5jb207DQogICAgICAgc3BmPXNvZnRmYWlsIChnb29nbGUuY29tOiBkb21haW4gb2YgdHJhbnNpdGlvbmluZyBoYWNrZXJAZ21haWwuY29tIGRvZXMgbm90IGRlc2lnbmF0ZSAxMTQuMjkuMjM2LjI0NyBhcyBwZXJtaXR0ZWQgc2VuZGVyKSBzbXRwLm1haWxmcm9tPWhhY2tlckBnbWFpbC5jb207DQogICAgICAgZG1hcmM9ZmFpbCAocD1OT05FIHNwPVFVQVJBTlRJTkUgZGlzPU5PTkUpIGhlYWRlci5mcm9tPWdtYWlsLmNvbQ0KUmVjZWl2ZWQ6IGJ5IGVta2VpLmN6IChQb3N0Zml4LCBmcm9tIHVzZXJpZCAzMykNCglpZCA0NzU5QzFGMDk7IFRodSwgMjYgU2VwIDIwMjQgMDc6Mzk6MDIgKzAyMDAgKENFU1QpDQpUbzogaW9hbkBmbG93Y3J5cHQuY29tDQpTdWJqZWN0OiBUZXN0IFNwb29mZWQgZW1haWwgYnkgTWFydA0KRnJvbTogIkFkbWluaXN0cmF0b3IiIDxoYWNrZXJAZ21haWwuY29tPg0KWC1Qcmlvcml0eTogMyAoTm9ybWFsKQ0KSW1wb3J0YW5jZTogTm9ybWFsDQpFcnJvcnMtVG86IGhhY2tlckBnbWFpbC5jb20NClJlcGx5LVRvOiBoYWNrZXJAZ21haWwuY29tDQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9dXRmLTgNCk1lc3NhZ2UtSWQ6IDwyMDI0MDkyNjA1MzkwMi40NzU5QzFGMDlAZW1rZWkuY3o-DQpEYXRlOiBUaHUsIDI2IFNlcCAyMDI0IDA3OjM5OjAyICswMjAwIChDRVNUKQ0KDQp0ZXN0DQoNCi1NYXJ0IGF0IEZsb3dDcnlwdA0K", + "historyId": "298166", + "internalDate": "1727329142000" + } +} diff --git a/appium/api-mocks/apis/google/google-messages.ts b/appium/api-mocks/apis/google/google-messages.ts index 011b62476..db113ce5a 100644 --- a/appium/api-mocks/apis/google/google-messages.ts +++ b/appium/api-mocks/apis/google/google-messages.ts @@ -31,4 +31,5 @@ export type GoogleMockMessage = | 'Encrypted email with public key attached' | 'Email with another user public key attached' | 'Test remote images #2414' + | 'Test Spoofed email by Mart' | 'Test encrypted message sent via encrypted contact page with attachment'; diff --git a/appium/tests/screenobjects/email.screen.ts b/appium/tests/screenobjects/email.screen.ts index de1cdf295..86bd949c6 100644 --- a/appium/tests/screenobjects/email.screen.ts +++ b/appium/tests/screenobjects/email.screen.ts @@ -38,6 +38,8 @@ const SELECTORS = { TOGGLE_PUBLIC_KEY_NODE: '~aid-toggle-public-key-node', PUBLIC_KEY_VALUE: '~aid-public-key-value', IMPORT_PUBLIC_KEY_BUTTON: '~aid-import-key-button', + SECURITY_WARNING_SUBJECT: '~aid-security-warning-subject-node', + SECURITY_WARNING_MESSSAGE: '~aid-security-warning-message-node', }; class EmailScreen extends BaseScreen { @@ -173,6 +175,14 @@ class EmailScreen extends BaseScreen { return $(SELECTORS.ATTACHMENT_TEXT_VIEW); } + get securityWarningSubjectLabel() { + return $(SELECTORS.SECURITY_WARNING_SUBJECT); + } + + get securityWarningMessageLabel() { + return $(SELECTORS.SECURITY_WARNING_MESSSAGE); + } + checkEmailSender = async (sender: string, index = 0) => { const element = await this.senderEmail(index); await ElementHelper.waitElementVisible(element); @@ -204,6 +214,16 @@ class EmailScreen extends BaseScreen { } }; + checkSecurityWarningBlock = async () => { + await ElementHelper.waitForText(await this.securityWarningSubjectLabel, 'Potentially suspicious message'); + await ElementHelper.waitForText( + await this.securityWarningMessageLabel, + "It wasn't properly verified by the sender, so its authenticity can't be confirmed.", + 15000, + true, + ); + }; + checkOpenedEmail = async (email: string, subject: string, text: string, isHtml = false) => { await this.checkEmailSender(email); await this.checkEmailSubject(subject); diff --git a/appium/tests/specs/mock/inbox/CheckMessageProcessingErrors.spec.ts b/appium/tests/specs/mock/inbox/CheckMessageProcessingErrors.spec.ts index 9a4e6455d..23569697b 100644 --- a/appium/tests/specs/mock/inbox/CheckMessageProcessingErrors.spec.ts +++ b/appium/tests/specs/mock/inbox/CheckMessageProcessingErrors.spec.ts @@ -29,6 +29,7 @@ describe('INBOX: ', () => { const notIntegrityProtectedSender = CommonData.notIntegrityProtected.senderName; const notIntegrityProtectedText = CommonData.notIntegrityProtected.message; + const spoofedEmailSubject = 'Test Spoofed email by Mart'; const keyMismatchSubject = CommonData.keyMismatch.subject; const keyMismatchName = CommonData.keyMismatch.senderName; const keyMismatchText = CommonData.keyMismatch.message; @@ -49,6 +50,7 @@ describe('INBOX: ', () => { 'wrong checksum', 'not integrity protected - should show a warning and not decrypt automatically', 'key mismatch unexpectedly produces a modal', + spoofedEmailSubject, ], }); mockApi.attesterConfig = { @@ -83,6 +85,11 @@ describe('INBOX: ', () => { await EmailScreen.clickBackButton(); await MailFolderScreen.checkInboxScreen(); + // Check spoofed email to see if security warning block exists + await MailFolderScreen.clickOnEmailBySubject(spoofedEmailSubject); + await EmailScreen.checkSecurityWarningBlock(); + await EmailScreen.clickBackButton(); + // Checking error for wrong checksum message await MailFolderScreen.clickOnEmailBySubject(wrongChecksumSubject); await EmailScreen.checkOpenedEmail(wrongChecksumName, wrongChecksumSubject, wrongChecksumText); From 1482265280254169196f7e19dcf8d01d9366fcbf Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Mon, 4 Nov 2024 02:44:05 -0600 Subject: [PATCH 2/2] #2573 Upgrade GTMAppAuth to v4 (#2633) * feat: upgraded GTMAppAuth to v4 * fix: unit test --- FlowCrypt.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 10 ++--- .../Compose/ComposeViewController.swift | 2 +- .../ComposeViewController+Contacts.swift | 2 +- .../ContactsProviderType.swift | 2 +- .../GoogleContactsProvider.swift | 4 +- .../Mail Provider/Gmail/GmailService.swift | 2 +- .../GoogleAuthManager.swift | 38 ++++++++++--------- .../Mail Provider/GmailServiceTest.swift | 2 +- 9 files changed, 34 insertions(+), 30 deletions(-) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 2f867087b..d629b643e 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -3887,7 +3887,7 @@ repositoryURL = "https://github.com/google/GTMAppAuth"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.0.0; + minimumVersion = 4.0.0; }; }; 51A1A12629070CDF007F1188 /* XCRemoteSwiftPackageReference "Texture" */ = { diff --git a/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e2ab48bfe..6029b7795 100644 --- a/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "672e29829148463333cfe67147f4218035ae9dc45722e5edd9d0f3ce2cf783a7", + "originHash" : "05430bf1fc71f707204c86e319466dfc225d7e8224d4328e53f7059acda7dbd3", "pins" : [ { "identity" : "appauth-ios", @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleSignIn-iOS", "state" : { - "revision" : "7932d33686c1dc4d7df7a919aae47361d1cdfda4", - "version" : "7.0.0" + "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", + "version" : "7.1.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GTMAppAuth.git", "state" : { - "revision" : "cee3c709307912d040bd1e06ca919875a92339c6", - "version" : "2.0.0" + "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", + "version" : "4.1.1" } }, { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 0d458269c..f6236facd 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -121,7 +121,7 @@ final class ComposeViewController: TableNodeViewController { self.googleAuthManager = GoogleAuthManager( appDelegateGoogleSessionContainer: UIApplication.shared.delegate as? AppDelegate ) - self.contactsProvider = GoogleContactsProvider(authorization: self.googleAuthManager.authorization(for: appContext.user.email)) + self.contactsProvider = try GoogleContactsProvider(authorization: self.googleAuthManager.authorization(for: appContext.user.email)) let draftsApiClient = try appContext.getRequiredMailProvider().draftsApiClient diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift index 41a1d76bd..cdff39d11 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+Contacts.swift @@ -18,7 +18,7 @@ extension ComposeViewController { for: .gmailLogin(self), appContext: appContext ) - contactsProvider.authorization = googleAuthManager.authorization(for: appContext.user.email) + contactsProvider.authorization = try googleAuthManager.authorization(for: appContext.user.email) shouldEvaluateRecipientInput = true reload(sections: [.contacts]) } catch { diff --git a/FlowCrypt/Functionality/Mail Provider/Contacts Provider/ContactsProviderType.swift b/FlowCrypt/Functionality/Mail Provider/Contacts Provider/ContactsProviderType.swift index c3ec41054..bc96cb6e2 100644 --- a/FlowCrypt/Functionality/Mail Provider/Contacts Provider/ContactsProviderType.swift +++ b/FlowCrypt/Functionality/Mail Provider/Contacts Provider/ContactsProviderType.swift @@ -10,7 +10,7 @@ import Foundation import GTMAppAuth protocol ContactsProviderType { - var authorization: GTMAppAuthFetcherAuthorization? { get set } + var authorization: GTMAppAuth.AuthSession? { get set } var isContactsScopeEnabled: Bool { get } func searchContacts(query: String) async throws -> [Recipient] } diff --git a/FlowCrypt/Functionality/Mail Provider/Contacts Provider/GoogleContactsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Contacts Provider/GoogleContactsProvider.swift index b88715714..e2eac5393 100644 --- a/FlowCrypt/Functionality/Mail Provider/Contacts Provider/GoogleContactsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Contacts Provider/GoogleContactsProvider.swift @@ -18,7 +18,7 @@ enum ContactsProviderError: Error { } class GoogleContactsProvider: ContactsProviderType { - var authorization: GTMAppAuthFetcherAuthorization? { + var authorization: GTMAppAuth.AuthSession? { didSet { guard isContactsScopeEnabled else { return } runWarmupQuery() @@ -68,7 +68,7 @@ class GoogleContactsProvider: ContactsProviderType { } } - init(authorization: GTMAppAuthFetcherAuthorization?) { + init(authorization: GTMAppAuth.AuthSession?) { self.authorization = authorization } diff --git a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift index ed40e5c2c..a199f359b 100644 --- a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift @@ -29,7 +29,7 @@ class GmailService: MailServiceProvider { self?.progressHandler?(progress) } - guard let authorization = googleAuthManager.authorization(for: currentUserEmail) else { + guard let authorization = try? googleAuthManager.authorization(for: currentUserEmail) else { logger.logWarning("authorization for current user is nil") return service } diff --git a/FlowCrypt/Functionality/Services/Google User Service/GoogleAuthManager.swift b/FlowCrypt/Functionality/Services/Google User Service/GoogleAuthManager.swift index 5c2600088..e1722d419 100644 --- a/FlowCrypt/Functionality/Services/Google User Service/GoogleAuthManager.swift +++ b/FlowCrypt/Functionality/Services/Google User Service/GoogleAuthManager.swift @@ -39,7 +39,7 @@ enum GoogleAuthManagerError: Error, CustomStringConvertible { } protocol GoogleAuthManagerType { - func authorization(for email: String?) -> GTMAppAuthFetcherAuthorization? + func authorization(for email: String?) throws -> GTMAppAuth.AuthSession? } // this is here so that we don't have to include AppDelegate in test target @@ -64,16 +64,17 @@ final class GoogleAuthManager: NSObject, GoogleAuthManagerType { lazy var logger = Logger.nested(in: Self.self, with: .userAppStart) - private func idToken(for email: String?) -> String? { - return authorization(for: email)?.authState.lastTokenResponse?.idToken + private func idToken(for email: String?) throws -> String? { + return try authorization(for: email)?.authState.lastTokenResponse?.idToken } - func authorization(for email: String?) -> GTMAppAuthFetcherAuthorization? { + func authorization(for email: String?) throws -> GTMAppAuth.AuthSession? { guard let email else { return nil } + let keychainStore = GTMAppAuth.KeychainStore(itemName: Constants.index + email) // get authorization from keychain - return GTMAppAuthFetcherAuthorization(fromKeychainForName: Constants.index + email) + return try keychainStore.retrieveAuthSession() } private var authorizationConfiguration: OIDServiceConfiguration { @@ -83,7 +84,7 @@ final class GoogleAuthManager: NSObject, GoogleAuthManagerType { tokenEndpoint: URL(string: "\(GeneralConstants.Mock.backendUrl)/token")! ) } else { - return GTMAppAuthFetcherAuthorization.configurationForGoogle() + return GTMAppAuth.AuthSession.configurationForGoogle() } } } @@ -129,7 +130,8 @@ extension GoogleAuthManager { func signOut(user email: String) { DispatchQueue.main.async { self.appDelegateGoogleSessionContainer?.googleAuthSession = nil - GTMAppAuthFetcherAuthorization.removeFromKeychain(forName: Constants.index + email) + let keychainStore = GTMAppAuth.KeychainStore(itemName: Constants.index + email) + try? keychainStore.removeAuthSession() } } @@ -163,7 +165,7 @@ extension GoogleAuthManager { scopes: [GoogleScope], userEmail: String? ) async throws -> SessionType { - let authorization = GTMAppAuthFetcherAuthorization(authState: authState) + let authorization = GTMAppAuth.AuthSession(authState: authState) guard let email = authorization.userEmail else { throw GoogleAuthManagerError.inconsistentState("Missing email") @@ -178,7 +180,7 @@ extension GoogleAuthManager { email: authorization.userEmail ) } - saveAuth(state: authState, for: email) + try saveAuth(state: authState, for: email) guard let token = authState.lastTokenResponse?.accessToken else { throw GoogleAuthManagerError.inconsistentState("Missing token") } @@ -210,14 +212,16 @@ extension GoogleAuthManager { } // save auth session to keychain - private func saveAuth(state: OIDAuthState, for email: String) { + private func saveAuth(state: OIDAuthState, for email: String) throws { state.stateChangeDelegate = self - let authorization = GTMAppAuthFetcherAuthorization(authState: state) - GTMAppAuthFetcherAuthorization.save(authorization, toKeychainForName: Constants.index + email) + let authorization = GTMAppAuth.AuthSession(authState: state) + let keychainStore = GTMAppAuth.KeychainStore(itemName: Constants.index + email) + + try keychainStore.save(authSession: authorization) } private func fetchGoogleUser( - with authorization: GTMAppAuthFetcherAuthorization + with authorization: GTMAppAuth.AuthSession ) async throws -> GTLROauth2_Userinfo { return try await withCheckedThrowingContinuation { continuation in let query = GTLROauth2Query_UserinfoGet.query() @@ -249,7 +253,7 @@ extension GoogleAuthManager { // MARK: - Tokens extension GoogleAuthManager { func getCachedOrRefreshedIdToken(minExpiryDuration: Double = 0, email: String?) async throws -> String { - guard let idToken = idToken(for: email) else { throw (IdTokenError.missingToken) } + guard let idToken = try idToken(for: email) else { throw (IdTokenError.missingToken) } let decodedToken = try decode(idToken: idToken) @@ -282,7 +286,7 @@ extension GoogleAuthManager { private func performTokenRefresh(email: String?) async throws -> (accessToken: String, idToken: String) { return try await withCheckedThrowingContinuation { continuation in - let authorization = authorization(for: email) + let authorization = try? authorization(for: email) authorization?.authState.setNeedsTokenRefresh() authorization?.authState.performAction { accessToken, idToken, error in guard let accessToken, let idToken else { @@ -299,10 +303,10 @@ extension GoogleAuthManager { // MARK: - OIDAuthStateChangeDelegate extension GoogleAuthManager: OIDAuthStateChangeDelegate { func didChange(_ state: OIDAuthState) { - let authorization = GTMAppAuthFetcherAuthorization(authState: state) + let authorization = GTMAppAuth.AuthSession(authState: state) guard let email = authorization.userEmail else { return } - saveAuth(state: state, for: email) + try? saveAuth(state: state, for: email) } } diff --git a/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift b/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift index ff5b3cf1d..71616c390 100644 --- a/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift +++ b/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift @@ -38,7 +38,7 @@ class GmailServiceTest: XCTestCase { // MARK: - Mock class GoogleAuthManagerMock: GoogleAuthManagerType { - func authorization(for email: String?) -> GTMAppAuthFetcherAuthorization? { + func authorization(for email: String?) -> GTMAppAuth.AuthSession? { return nil }