Skip to content

Commit

Permalink
feat: message broker for user scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
Shane Osbourne committed May 5, 2023
1 parent 6c2dd17 commit b80a0a6
Show file tree
Hide file tree
Showing 7 changed files with 1,031 additions and 62 deletions.
7 changes: 5 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ let package = Package(
.define("TERMINATE_WITH_REASON_ENABLED", .when(platforms: [.macOS])),
]),
.target(
name: "UserScript"
name: "UserScript",
dependencies: [
"Common"
]
),
.target(
name: "PrivacyDashboard",
Expand Down Expand Up @@ -160,7 +163,7 @@ let package = Package(
dependencies: [
"Networking"
]),

// MARK: - Test targets
.testTarget(
name: "BrowserServicesKitTests",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import WebKit
import Combine
import ContentScopeScripts
import UserScript
import Common
import os.log

public final class ContentScopeProperties: Encodable {
public let globalPrivacyControlValue: Bool
Expand All @@ -38,9 +40,9 @@ public final class ContentScopeProperties: Encodable {
}
}
public struct ContentScopeFeature: Encodable {

public let settings: [String: ContentScopeFeatureToggles]

public init(featureToggles: ContentScopeFeatureToggles) {
self.settings = ["featureToggles": featureToggles]
}
Expand All @@ -49,18 +51,18 @@ public struct ContentScopeFeature: Encodable {
public struct ContentScopeFeatureToggles: Encodable {

public let emailProtection: Bool

public let credentialsAutofill: Bool
public let identitiesAutofill: Bool
public let creditCardsAutofill: Bool

public let credentialsSaving: Bool

public let passwordGeneration: Bool

public let inlineIconCredentials: Bool
public let thirdPartyCredentialsProvider: Bool

// Explicitly defined memberwise init only so it can be public
public init(emailProtection: Bool,
credentialsAutofill: Bool,
Expand All @@ -70,7 +72,7 @@ public struct ContentScopeFeatureToggles: Encodable {
passwordGeneration: Bool,
inlineIconCredentials: Bool,
thirdPartyCredentialsProvider: Bool) {

self.emailProtection = emailProtection
self.credentialsAutofill = credentialsAutofill
self.identitiesAutofill = identitiesAutofill
Expand All @@ -80,18 +82,18 @@ public struct ContentScopeFeatureToggles: Encodable {
self.inlineIconCredentials = inlineIconCredentials
self.thirdPartyCredentialsProvider = thirdPartyCredentialsProvider
}

enum CodingKeys: String, CodingKey {
case emailProtection = "emailProtection"

case credentialsAutofill = "inputType_credentials"
case identitiesAutofill = "inputType_identities"
case creditCardsAutofill = "inputType_creditCards"

case credentialsSaving = "credentials_saving"

case passwordGeneration = "password_generation"

case inlineIconCredentials = "inlineIcon_credentials"
case thirdPartyCredentialsProvider = "third_party_credentials_provider"
}
Expand All @@ -107,38 +109,114 @@ public struct ContentScopePlatform: Encodable {
#endif
}

public final class ContentScopeUserScript: NSObject, UserScript {
public let messageNames: [String] = []
public final class ContentScopeUserScript: NSObject, UserScript, UserScriptMessaging {

public var broker: UserScriptMessageBroker;
public let isolated: Bool
public var messageNames: [String] = []

public init(_ privacyConfigManager: PrivacyConfigurationManaging, properties: ContentScopeProperties) {
source = ContentScopeUserScript.generateSource(privacyConfigManager, properties: properties)
public init(_ privacyConfigManager: PrivacyConfigurationManaging,
properties: ContentScopeProperties,
isolated: Bool = false
) {
self.isolated = isolated
let contextName = self.isolated ? "contentScopeScriptsIsolated" : "contentScopeScripts";
self.broker = UserScriptMessageBroker(context: contextName)

source = ContentScopeUserScript.generateSource(
privacyConfigManager,
properties: properties,
isolated: self.isolated,
config: self.broker.messagingConfig()
)
self.messageNames = [contextName]
}

public static func generateSource(_ privacyConfigurationManager: PrivacyConfigurationManaging, properties: ContentScopeProperties) -> String {
public static func generateSource(_ privacyConfigurationManager: PrivacyConfigurationManaging,
properties: ContentScopeProperties,
isolated: Bool,
config: WebkitMessagingConfig
) -> String {

guard let privacyConfigJson = String(data: privacyConfigurationManager.currentConfig, encoding: .utf8),
let userUnprotectedDomains = try? JSONEncoder().encode(privacyConfigurationManager.privacyConfig.userUnprotectedDomains),
let userUnprotectedDomainsString = String(data: userUnprotectedDomains, encoding: .utf8),
let jsonProperties = try? JSONEncoder().encode(properties),
let jsonPropertiesString = String(data: jsonProperties, encoding: .utf8)
let jsonPropertiesString = String(data: jsonProperties, encoding: .utf8),
let jsonConfig = try? JSONEncoder().encode(config),
let jsonConfigString = String(data: jsonConfig, encoding: .utf8)
else {
return ""
}

return loadJS("contentScope", from: ContentScopeScripts.Bundle, withReplacements: [

let jsInclude = isolated ? "contentScopeIsolated" : "contentScope"

return loadJS(jsInclude, from: ContentScopeScripts.Bundle, withReplacements: [
"$CONTENT_SCOPE$": privacyConfigJson,
"$USER_UNPROTECTED_DOMAINS$": userUnprotectedDomainsString,
"$USER_PREFERENCES$": jsonPropertiesString
"$USER_PREFERENCES$": jsonPropertiesString,
"$WEBKIT_MESSAGING_CONFIG$": jsonConfigString
])
}

public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
}

public let source: String

public let injectionTime: WKUserScriptInjectionTime = .atDocumentStart
public let forMainFrameOnly: Bool = false
public let requiresRunInPageContentWorld: Bool = true
public var requiresRunInPageContentWorld: Bool {
if #available(macOS 11.0, *) {
if self.isolated { return false }
}
return true
}
}

@available(macOS 11, *)
extension ContentScopeUserScript: WKScriptMessageHandlerWithReply {
public func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage,
replyHandler: @escaping (Any?, String?) -> Void) {
let action = broker.messageHandlerFor(message)
do {
try broker.execute(action: action, original: message) { json in
replyHandler(json, nil)
}
} catch let error {
// forward uncaught errors to the client
replyHandler(nil, error.localizedDescription)
}
}
}

// MARK: - Fallback for macOS 10.15
extension ContentScopeUserScript: WKScriptMessageHandler {
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
let action = broker.messageHandlerFor(message);

if case .notify = action {
do {
try broker.execute(action: action, original: message) { _ in
message.webView?.evaluateJavaScript("console.log('🍎 doing nothing with notification')")
}
} catch let error {
// there's no way of communicating this
message.webView?.evaluateJavaScript("console.log('error \(error.localizedDescription)')")
}
}

// Accept a message that requires an encrypted response
guard case .respondEncrypted(_, _, let params) = action else {
message.webView?.evaluateJavaScript("console.log('not something to response to \(String(describing: message.messageBody))')")
return
}

do {
try broker.execute(action: action, original: message) { [weak broker] json in
broker?.encryptMessageResponse(response: json, params: params, message: message)
message.webView?.evaluateJavaScript("console.log('all good')")
}
} catch let error {
// there's no way of communicating this
message.webView?.evaluateJavaScript("console.log('cannot communicate this error: \(error.localizedDescription)')")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// Created by shane osbourne on 22/04/2023.
//

import Foundation
import WebKit
import Combine
import ContentScopeScripts
import UserScript
import Common
import os.log

public final class SpecialPagesUserScript: NSObject, UserScript, UserScriptMessaging {
public var source: String = "";

public static let context = "specialPages"

// special pages messaging cannot be isolated as we'll want regular page-scripts to be able to communicate
public let broker = UserScriptMessageBroker(context: SpecialPagesUserScript.context, requiresRunInPageContentWorld: true );

public let messageNames: [String] = [
SpecialPagesUserScript.context
]

public let injectionTime: WKUserScriptInjectionTime = .atDocumentStart
public let forMainFrameOnly: Bool = true
public private(set) var requiresRunInPageContentWorld: Bool = true
}

@available(macOS 11, *)
extension SpecialPagesUserScript: WKScriptMessageHandlerWithReply {
public func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage,
replyHandler: @escaping (Any?, String?) -> Void) {
let action = broker.messageHandlerFor(message)
do {
try broker.execute(action: action, original: message) { json in
replyHandler(json, nil)
}
} catch let error {
// forward uncaught errors to the client
replyHandler(nil, error.localizedDescription)
}
}
}

// MARK: - Fallback for macOS 10.15
extension SpecialPagesUserScript: WKScriptMessageHandler {
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
let action = broker.messageHandlerFor(message);

// special pages only support notifications on Catalina
if case .notify = action {
do {
try broker.execute(action: action, original: message) { _ in
message.webView?.evaluateJavaScript("console.log('🍎 doing nothing with notification')")
}
} catch let error {
// there's no way of communicating this
message.webView?.evaluateJavaScript("console.log('error \(error.localizedDescription)')")
}
}
}
}
87 changes: 54 additions & 33 deletions Sources/UserScript/UserScriptMessageEncryption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@

import Foundation
import WebKit
import Common

public struct SecureMessagingParams: Codable {
let iv: [UInt8]
let key: [UInt8]
let methodName: String
let secret: String
}

public protocol UserScriptMessageEncryption {

Expand All @@ -41,41 +49,54 @@ extension UserScriptMessageEncryption {
return
}

guard let body = message.messageBody as? [String: Any],
let messageHandling = body["messageHandling"] as? [String: Any],
let secret = messageHandling["secret"] as? String,
// If this does not match the page is playing shenanigans.
secret == generatedSecret
else { return }
guard let params = accept(encrypted: message) else { return }

messageHandler(message) { reply in
guard let reply = reply,
let messageHandling = body["messageHandling"] as? [String: Any],
let key = messageHandling["key"] as? [UInt8],
let iv = messageHandling["iv"] as? [UInt8],
let methodName = messageHandling["methodName"] as? String,
let encryption = try? self.encrypter.encryptReply(reply, key: key, iv: iv) else { return }

let ciphertext = encryption.ciphertext.withUnsafeBytes { bytes in
return bytes.map { String($0) }
}.joined(separator: ",")

let tag = encryption.tag.withUnsafeBytes { bytes in
return bytes.map { String($0) }
}.joined(separator: ",")

let script = """
(() => {
window.\(methodName) && window.\(methodName)({
ciphertext: [\(ciphertext)],
tag: [\(tag)]
});
})();
"""

assert(message.messageWebView != nil)
dispatchPrecondition(condition: .onQueue(DispatchQueue.main))
message.messageWebView?.evaluateJavaScript(script)
guard let reply = reply else { return }
encrypt(reply: reply, with: params, into: message.messageWebView)
}
}

public func messageHandlerFor(_ messageName: String) -> MessageHandler? {
return nil
}

public func accept(encrypted message: UserScriptMessage) -> SecureMessagingParams? {
guard let body = message.messageBody as? [String: Any],
let messageHandling = body["messageHandling"],
let params: SecureMessagingParams = DecodableHelper.decode(from: messageHandling),
params.secret == generatedSecret
else {
print("doing nothing")
return nil
}

return params
}

public func encrypt(reply: String, with params: SecureMessagingParams, into webView: WKWebView?) {

guard let encryption = try? encrypter.encryptReply(reply, key: params.key, iv: params.iv) else { return }

let ciphertext = encryption.ciphertext.withUnsafeBytes { bytes in
return bytes.map { String($0) }
}.joined(separator: ",")

let tag = encryption.tag.withUnsafeBytes { bytes in
return bytes.map { String($0) }
}.joined(separator: ",")

let script = """
(() => {
window.\(params.methodName) && window.\(params.methodName)({
ciphertext: [\(ciphertext)],
tag: [\(tag)]
});
})();
"""

assert(webView != nil)
dispatchPrecondition(condition: .onQueue(DispatchQueue.main))
webView?.evaluateJavaScript(script)
}
}
Loading

0 comments on commit b80a0a6

Please sign in to comment.