From 152c127d622fc0af4aa9e4df228d6e362e5b0572 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 6 Aug 2019 12:23:29 -0400 Subject: [PATCH 1/3] Added SafeBrowsingHelper --- Client.xcodeproj/project.pbxproj | 4 + .../Frontend/Browser/SafeBrowsingHelper.swift | 270 ++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 Client/Frontend/Browser/SafeBrowsingHelper.swift diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index 55ffa07bc50..d0649b42ec0 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -518,6 +518,7 @@ 5E4845C022DE381200372022 /* WindowRenderHelper.js in Resources */ = {isa = PBXBuildFile; fileRef = 5E4845BF22DE381200372022 /* WindowRenderHelper.js */; }; 5E4845C222DE3DF800372022 /* WindowRenderHelperScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E4845C122DE3DF800372022 /* WindowRenderHelperScript.swift */; }; 5E9288CA22DF864C007BE7A6 /* TabSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9288C922DF864C007BE7A6 /* TabSessionTests.swift */; }; + 5EB57C0022F9E0EC00A07325 /* SafeBrowsingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EB57BFF22F9E0EC00A07325 /* SafeBrowsingHelper.swift */; }; 744B0FFE1B4F172E00100422 /* ToolbarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 744B0FFD1B4F172E00100422 /* ToolbarTests.swift */; }; 744ED5611DBFEB8D00A2B5BE /* MailtoLinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 744ED5601DBFEB8D00A2B5BE /* MailtoLinkHandler.swift */; }; 7479B4EF1C5306A200DF000B /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7479B4ED1C5306A200DF000B /* Reachability.swift */; }; @@ -1890,6 +1891,7 @@ 5E4845BF22DE381200372022 /* WindowRenderHelper.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = WindowRenderHelper.js; sourceTree = ""; }; 5E4845C122DE3DF800372022 /* WindowRenderHelperScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowRenderHelperScript.swift; sourceTree = ""; }; 5E9288C922DF864C007BE7A6 /* TabSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSessionTests.swift; sourceTree = ""; }; + 5EB57BFF22F9E0EC00A07325 /* SafeBrowsingHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeBrowsingHelper.swift; sourceTree = ""; }; 744B0FFD1B4F172E00100422 /* ToolbarTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarTests.swift; sourceTree = ""; }; 744ED5601DBFEB8D00A2B5BE /* MailtoLinkHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailtoLinkHandler.swift; sourceTree = ""; }; 7479B4ED1C5306A200DF000B /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Reachability.swift; path = ThirdParty/Reachability.swift; sourceTree = ""; }; @@ -3854,6 +3856,7 @@ 279C756A219DDE3B001CD1CB /* FingerprintingProtection.swift */, 5E34780F22D7A1D200B0D5F8 /* ResourceDownloadManager.swift */, 5E4845C122DE3DF800372022 /* WindowRenderHelperScript.swift */, + 5EB57BFF22F9E0EC00A07325 /* SafeBrowsingHelper.swift */, ); indentWidth = 4; path = Browser; @@ -5896,6 +5899,7 @@ 0A431D9721B1C54A0041625B /* BloomFilter.cpp in Sources */, C615FACF2129FBD000A8168C /* ImageCacheProtocol.swift in Sources */, 4422D4BB21BFFB7600BF1855 /* cache.cc in Sources */, + 5EB57C0022F9E0EC00A07325 /* SafeBrowsingHelper.swift in Sources */, 39455F771FC83F430088A22C /* TabEventHandler.swift in Sources */, 4422D4B721BFFB7600BF1855 /* filter_policy.cc in Sources */, 0AADC4D520D2A6A200FDE368 /* PreloadedFavorites.swift in Sources */, diff --git a/Client/Frontend/Browser/SafeBrowsingHelper.swift b/Client/Frontend/Browser/SafeBrowsingHelper.swift new file mode 100644 index 00000000000..518988ec8f8 --- /dev/null +++ b/Client/Frontend/Browser/SafeBrowsingHelper.swift @@ -0,0 +1,270 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import Foundation + +struct SafeBrowsingHelper { + /// Types of threats. + enum ThreatType: String, Codable { + case unspecified = "THREAT_TYPE_UNSPECIFIED" + case malware = "MALWARE" + case socialEngineering = "SOCIAL_ENGINEERING" + case unwantedSoftware = "UNWANTED_SOFTWARE" + case potentiallyHarmfulApplication = "POTENTIALLY_HARMFUL_APPLICATION" + } + + /// Types of platforms. + enum PlatformType: String, Codable { + /// Unknown platform. + case unknown = "PLATFORM_TYPE_UNSPECIFIED" + + /// Threat posed to Windows. + case windows = "WINDOWS" + + /// Threat posed to Linux. + case linux = "LINUX" + + /// Threat posed to Android. + case android = "ANDROID" + + /// Threat posed to OS X. + case osx = "OSX" + + /// Threat posed to iOS. + case ios = "IOS" + + /// Threat posed to at least one of the defined platforms. + case `any` = "ANY_PLATFORM" + + /// Threat posed to all defined platforms. + case all = "ALL_PLATFORMS" + + /// Threat posed to Chrome. + case chrome = "CHROME" + } + + /// Types of entries that pose threats. + /// Threat lists are collections of entries of a single type. + enum ThreatEntryType: String, Codable { + case unspecified = "THREAT_ENTRY_TYPE_UNSPECIFIED" + case url = "URL" + case exe = "EXECUTABLE" + } + + /// The client metadata associated with Safe Browsing API requests. + struct ClientInfo: Codable { + /// A client ID that (hopefully) uniquely identifies the client implementation of the Safe Browsing API. + let clientId: String + + /// The version of the client implementation. + let clientVersion: String + } + + /// An individual threat; for example, a malicious URL or its hash representation. + /// Only one of these fields should be set. + struct ThreatEntry: Codable { + /// A hash prefix, consisting of the most significant 4-32 bytes of a SHA256 hash. + /// This field is in binary format. For JSON requests, hashes are base64-encoded. + /// + /// A base64-encoded string. + let hash: String? + + /// A URL. + let url: String? + + /// The digest of an executable in SHA256 format. + /// The API supports both binary and hex digests. + /// For JSON requests, digests are base64-encoded. + /// + /// A base64-encoded string. + let digest: String? + } + + /// A single metadata entry. + struct MetadataEntry: Codable { + /// The metadata entry key. + /// For JSON requests, the key is base64-encoded. + /// + /// A base64-encoded string. + let key: String + + /// The metadata entry value. + /// For JSON requests, the value is base64-encoded. + /// + ///A base64-encoded string. + let value: String + } + + /// The metadata associated with a specific threat entry. + /// The client is expected to know the metadata key/value pairs + /// associated with each threat type. + struct ThreatEntryMetadata: Codable { + let entries: [MetadataEntry] + } + + /// The information regarding one or more threats that a client + /// submits when checking for matches in threat lists. + struct ThreatInfo: Codable { + /// The threat types to be checked. + let threatTypes: [ThreatType] + + /// The platform types to be checked. + let platformTypes: [PlatformType] + + /// The entry types to be checked. + let threatEntryTypes: [ThreatEntryType] + + /// The threat entries to be checked. + let threatEntries: [ThreatEntry] + } + + /// A match when checking a threat entry in the Safe Browsing threat lists. + struct ThreatMatch: Codable { + + /// The threat type matching this threat. + let threatType: ThreatType + + /// The platform type matching this threat. + let platformType: PlatformType + + /// The threat entry type matching this threat. + let threatEntryType: ThreatEntryType + + /// The threat matching this threat. + let threat: ThreatEntry + + /// Optional metadata associated with this threat. + let threatEntryMetadata: ThreatEntryMetadata? + + /// The cache lifetime for the returned match. + /// Clients must not cache this response for more than this duration to avoid false positives. + /// + /// A duration in seconds with up to nine fractional digits, terminated by 's'. + /// Example: "3.5s". + let cacheDuration: String + } + + + struct Request: Codable { + let client: ClientInfo + let threatInfo: ThreatInfo + } + + struct Response: Codable { + let matches: [ThreatMatch] + + init() { + matches = [] + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.matches = try container.decodeIfPresent([ThreatMatch].self, forKey: .matches) ?? [] + } + } + + struct ResponseError: Codable { + let code: Int + let message: String + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: NestedKeys.self) + let errorContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .error) + + self.code = try errorContainer.decode(Int.self, forKey: .code) + self.message = try errorContainer.decode(String.self, forKey: .message) + } + + private enum NestedKeys: String, CodingKey { + case error + } + } + + private class SafeBrowsingError: NSError { + public init(_ message: String, code: Int = -1) { + super.init(domain: "SafeBrowsingError", code: code, userInfo: [ + NSLocalizedDescriptionKey: message, + NSLocalizedFailureErrorKey: message + ]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError() + } + } + + @discardableResult + public static func threatMatches(urls: [URL], session: URLSession = .shared, _ completion: @escaping (Response, Error?) -> Void) throws -> URLSessionDataTask { + + let apiKey = "AIzaSyDeglae_dQQyQuRNk1jPq5R5--jBy21H5o" + + let threatTypes: [ThreatType] = [.malware, + .socialEngineering, + .unwantedSoftware, + .potentiallyHarmfulApplication] + + let platformTypes: [PlatformType] = [.any] + let threatEntryTypes: [ThreatEntryType] = [.url, .exe] + + let clientInfo = ClientInfo(clientId: "com.brave.safebrowsing", clientVersion: "1.0") + let threatInfo = ThreatInfo(threatTypes: threatTypes, + platformTypes: platformTypes, + threatEntryTypes: threatEntryTypes, + threatEntries: urls.map { + ThreatEntry(hash: nil, url: $0.absoluteString, digest: nil) + } + ) + + let requestInfo = Request(client: clientInfo, + threatInfo: threatInfo) + + let request = try { () throws -> URLRequest in + guard !apiKey.isEmpty else { + throw SafeBrowsingError("Invalid API Key") + } + + guard let url = URL(string: "https://safebrowsing.googleapis.com/v4/threatMatches:find?key=\(apiKey)") else { + throw SafeBrowsingError("Invalid Safe-Browsing URL") + } + + var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30) + request.httpMethod = "POST" + request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36", forHTTPHeaderField: "User-Agent") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.httpBody = try JSONEncoder().encode(requestInfo) + return request + }() + + let task = session.dataTask(with: request) { data, response, error in + if let error = error { + return completion(Response(), error) + } + + guard let data = data else { + return completion(Response(), SafeBrowsingError("Invalid Server Response: No Data")) + } + + if let response = response as? HTTPURLResponse { + if response.statusCode < 200 || response.statusCode > 299 { + do { + let error = try JSONDecoder().decode(ResponseError.self, from: data) + return completion(Response(), SafeBrowsingError(error.message, code: error.code)) + } catch { + return completion(Response(), error) + } + } + } + + do { + let response = try JSONDecoder().decode(Response.self, from: data) + completion(response, nil) + } catch { + completion(Response(), error) + } + } + task.resume() + return task + } +} From 71b1180bfe5575830af39822e9c0c1f53ade61c0 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 6 Aug 2019 13:34:13 -0400 Subject: [PATCH 2/3] Added SafeBrowsing implementation for Google Safe Browsing via threat-match request. --- ...rViewController+WKNavigationDelegate.swift | 32 +++++++++++++++++++ .../Frontend/Browser/SafeBrowsingHelper.swift | 1 - 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift index 54a5b60ba19..af86bae001a 100644 --- a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift +++ b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift @@ -107,6 +107,38 @@ extension BrowserViewController: WKNavigationDelegate { decisionHandler(.cancel) return } + + let semaphore = DispatchSemaphore(value: 0) + var isSafeURL: Bool = true + + DispatchQueue.global(qos: .background).async { + let task = try? SafeBrowsingHelper.threatMatches(urls: [url], { response, error in + + if !response.matches.isEmpty { + isSafeURL = false + } + + semaphore.signal() + }) + + if task == nil { + semaphore.signal() + } + } + + _ = semaphore.wait(timeout: .now() + .seconds(30)) + + if !isSafeURL { + let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let domain = Domain.getOrCreate(forUrl: url, persistent: !isPrivateBrowsing) + let isSafeBrowsingEnabled = domain.isShieldExpected(.SafeBrowsing) + + if isSafeBrowsingEnabled { + safeBrowsing?.showMalwareWarningPage(forUrl: url, inWebView: webView) + decisionHandler(.cancel) + return + } + } // First special case are some schemes that are about Calling. We prompt the user to confirm this action. This // gives us the exact same behaviour as Safari. diff --git a/Client/Frontend/Browser/SafeBrowsingHelper.swift b/Client/Frontend/Browser/SafeBrowsingHelper.swift index 518988ec8f8..b74dc345584 100644 --- a/Client/Frontend/Browser/SafeBrowsingHelper.swift +++ b/Client/Frontend/Browser/SafeBrowsingHelper.swift @@ -145,7 +145,6 @@ struct SafeBrowsingHelper { let cacheDuration: String } - struct Request: Codable { let client: ClientInfo let threatInfo: ThreatInfo From 88baea8a60f1503c98149cf8e33c668345d24c7f Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 6 Aug 2019 13:45:52 -0400 Subject: [PATCH 3/3] Moved things around. --- Client/Frontend/Browser/SafeBrowsingHelper.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Client/Frontend/Browser/SafeBrowsingHelper.swift b/Client/Frontend/Browser/SafeBrowsingHelper.swift index b74dc345584..a490f7c27b3 100644 --- a/Client/Frontend/Browser/SafeBrowsingHelper.swift +++ b/Client/Frontend/Browser/SafeBrowsingHelper.swift @@ -3,8 +3,12 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. import Foundation +import Shared struct SafeBrowsingHelper { + + private static let apiKey = "AIzaSyDeglae_dQQyQuRNk1jPq5R5--jBy21H5o" + /// Types of threats. enum ThreatType: String, Codable { case unspecified = "THREAT_TYPE_UNSPECIFIED" @@ -196,8 +200,6 @@ struct SafeBrowsingHelper { @discardableResult public static func threatMatches(urls: [URL], session: URLSession = .shared, _ completion: @escaping (Response, Error?) -> Void) throws -> URLSessionDataTask { - let apiKey = "AIzaSyDeglae_dQQyQuRNk1jPq5R5--jBy21H5o" - let threatTypes: [ThreatType] = [.malware, .socialEngineering, .unwantedSoftware, @@ -206,7 +208,9 @@ struct SafeBrowsingHelper { let platformTypes: [PlatformType] = [.any] let threatEntryTypes: [ThreatEntryType] = [.url, .exe] - let clientInfo = ClientInfo(clientId: "com.brave.safebrowsing", clientVersion: "1.0") + let clientInfo = ClientInfo(clientId: AppInfo.applicationBundle.bundleIdentifier ?? "com.brave.safebrowsing", + clientVersion: AppInfo.appVersion) + let threatInfo = ThreatInfo(threatTypes: threatTypes, platformTypes: platformTypes, threatEntryTypes: threatEntryTypes, @@ -219,17 +223,16 @@ struct SafeBrowsingHelper { threatInfo: threatInfo) let request = try { () throws -> URLRequest in - guard !apiKey.isEmpty else { + guard !SafeBrowsingHelper.apiKey.isEmpty else { throw SafeBrowsingError("Invalid API Key") } - guard let url = URL(string: "https://safebrowsing.googleapis.com/v4/threatMatches:find?key=\(apiKey)") else { + guard let url = URL(string: "https://safebrowsing.googleapis.com/v4/threatMatches:find?key=\(SafeBrowsingHelper.apiKey)") else { throw SafeBrowsingError("Invalid Safe-Browsing URL") } var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30) request.httpMethod = "POST" - request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36", forHTTPHeaderField: "User-Agent") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Accept") request.httpBody = try JSONEncoder().encode(requestInfo)