Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Add support for SafeBrowsing via request #1332

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Client.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1890,6 +1891,7 @@
5E4845BF22DE381200372022 /* WindowRenderHelper.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = WindowRenderHelper.js; sourceTree = "<group>"; };
5E4845C122DE3DF800372022 /* WindowRenderHelperScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowRenderHelperScript.swift; sourceTree = "<group>"; };
5E9288C922DF864C007BE7A6 /* TabSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSessionTests.swift; sourceTree = "<group>"; };
5EB57BFF22F9E0EC00A07325 /* SafeBrowsingHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeBrowsingHelper.swift; sourceTree = "<group>"; };
744B0FFD1B4F172E00100422 /* ToolbarTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarTests.swift; sourceTree = "<group>"; };
744ED5601DBFEB8D00A2B5BE /* MailtoLinkHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailtoLinkHandler.swift; sourceTree = "<group>"; };
7479B4ED1C5306A200DF000B /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Reachability.swift; path = ThirdParty/Reachability.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3854,6 +3856,7 @@
279C756A219DDE3B001CD1CB /* FingerprintingProtection.swift */,
5E34780F22D7A1D200B0D5F8 /* ResourceDownloadManager.swift */,
5E4845C122DE3DF800372022 /* WindowRenderHelperScript.swift */,
5EB57BFF22F9E0EC00A07325 /* SafeBrowsingHelper.swift */,
);
indentWidth = 4;
path = Browser;
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
272 changes: 272 additions & 0 deletions Client/Frontend/Browser/SafeBrowsingHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
// 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
import Shared

struct SafeBrowsingHelper {

private static let apiKey = "AIzaSyDeglae_dQQyQuRNk1jPq5R5--jBy21H5o"

/// 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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need other platforms here? Just iOS should work.


/// 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 {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is using the Lookup API, we should use the recommended Update API to reduce the performance impact and not sending hash for every URL to the google servers: https://developers.google.com/safe-browsing/v4/update-api

let threatTypes: [ThreatType] = [.malware,
.socialEngineering,
.unwantedSoftware,
.potentiallyHarmfulApplication]

let platformTypes: [PlatformType] = [.any]
let threatEntryTypes: [ThreatEntryType] = [.url, .exe]

let clientInfo = ClientInfo(clientId: AppInfo.applicationBundle.bundleIdentifier ?? "com.brave.safebrowsing",
clientVersion: AppInfo.appVersion)

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 !SafeBrowsingHelper.apiKey.isEmpty else {
throw SafeBrowsingError("Invalid API Key")
}

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("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
}
}