bitwarden-estensione-browser/src/safari/safari/SafariWebExtensionHandler.s...

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

201 lines
7.9 KiB
Swift
Raw Normal View History

import SafariServices
import os.log
import LocalAuthentication
let SFExtensionMessageKey = "message"
let ServiceName = "Bitwarden"
let ServiceNameBiometric = ServiceName + "_biometric"
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
let item = context.inputItems[0] as! NSExtensionItem
let message = item.userInfo?[SFExtensionMessageKey] as AnyObject?
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)
let response = NSExtensionItem()
guard let command = message?["command"] as? String else {
return
}
switch (command) {
case "readFromClipboard":
let pasteboard = NSPasteboard.general
response.userInfo = [ SFExtensionMessageKey: pasteboard.pasteboardItems?.first?.string(forType: .string) as Any ]
break
case "copyToClipboard":
guard let msg = message?["data"] as? String else {
return
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(msg, forType: .string)
case "showPopover":
SFSafariApplication.getActiveWindow { win in
win?.getToolbarItem(completionHandler: { item in
item?.showPopover()
})
}
break
case "downloadFile":
guard let jsonData = message?["data"] as? String else {
return
}
guard let dlMsg: DownloadFileMessage = jsonDeserialize(json: jsonData) else {
return
}
var blobData: Data?
if dlMsg.blobOptions?.type == "text/plain" {
blobData = dlMsg.blobData?.data(using: .utf8)
} else if let blob = dlMsg.blobData {
blobData = Data(base64Encoded: blob)
}
guard let data = blobData else {
return
}
let panel = NSSavePanel()
panel.isFloatingPanel = true
panel.canCreateDirectories = true
panel.nameFieldStringValue = dlMsg.fileName
panel.begin { response in
if response == NSApplication.ModalResponse.OK {
if let url = panel.url {
do {
let fileManager = FileManager.default
if !fileManager.fileExists(atPath: url.absoluteString) {
fileManager.createFile(atPath: url.absoluteString, contents: Data(),
attributes: nil)
}
try data.write(to: url)
} catch {
print(error)
NSLog("ERROR in downloadFile, \(error)")
}
}
}
}
break
case "sleep":
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
context.completeRequest(returningItems: [response], completionHandler: nil)
}
return
case "biometricUnlock":
var error: NSError?
let laContext = LAContext()
laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
if let e = error, e.code != kLAErrorBiometryLockout {
response.userInfo = [
SFExtensionMessageKey: [
"message": [
"command": "biometricUnlock",
"response": "not supported",
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
],
],
]
break
}
guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .userPresence], nil) else {
response.userInfo = [
SFExtensionMessageKey: [
"message": [
"command": "biometricUnlock",
"response": "not supported",
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
],
],
]
break
}
laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "Bitwarden Safari Extension") { (success, error) in
if success {
[Account Switching] Base changes for account switching (#2250) * Pull in jslib * Create new state models * Create browser specific stateService * Remove registration deprecated services, register stateService * Replace usage of deprecated services (user, constants) * Add missing properties to BrowserGroupingsComponentState * Remove StorageService from initFactory * Clear the correct state * Add null check when restoring send-grouping state * add remember email * Initialize stateservice in services.module * Fix 'lock now' not working * Comment to remove setting defaults on install * Pull jslib * Remove setting defaults on install * Bump jslib * Pass the current userId to services when logging out * Bump jslib * Override vaultTimeout default on account addition * Pull latest jslib * Retrieve vaultTimeout from stateService * Record activity per Account * Add userId to logout and add fallback if not present * Register AccountFactory * Pass userId in messages * Base changes for account switching di fixes (#2280) * [bug] Null checks on Account init * [bug] Use same stateService instance for all operations We override the stateService in browser, but currently don't pull the background service into popup and allow jslib to create its own instance of the base StateService for jslib services. This causes a split in in memory state between the three isntances that results in many errors, namely locking not working. * [chore] Update jslib * Pull in jslib * Pull in jslib * Pull in latest jslib to multiple stateservice inits * Check vault states before executing processReload * Adjust iterator * Update native messaging to include the userId (#2290) * Re-Add UserVerificationService * Fix email not being remembered by base component * Improve readability of reloadProcess * Removed unneeded null check * Fix constructor dependency (stateService) * Added missing await * Simplify dependency registration * Fixed typos * Reverted back to simple loop * Use vaultTimeoutService to retrieve Timeout Co-authored-by: Addison Beck <abeck@bitwarden.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
2022-01-27 22:22:51 +01:00
guard let userId = message?["userId"] as? String else {
return
}
let passwordName = userId + "_masterkey_biometric"
var passwordLength: UInt32 = 0
var passwordPtr: UnsafeMutableRawPointer? = nil
var status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(passwordName.utf8.count), passwordName, &passwordLength, &passwordPtr, nil)
if status != errSecSuccess {
[Account Switching] Base changes for account switching (#2250) * Pull in jslib * Create new state models * Create browser specific stateService * Remove registration deprecated services, register stateService * Replace usage of deprecated services (user, constants) * Add missing properties to BrowserGroupingsComponentState * Remove StorageService from initFactory * Clear the correct state * Add null check when restoring send-grouping state * add remember email * Initialize stateservice in services.module * Fix 'lock now' not working * Comment to remove setting defaults on install * Pull jslib * Remove setting defaults on install * Bump jslib * Pass the current userId to services when logging out * Bump jslib * Override vaultTimeout default on account addition * Pull latest jslib * Retrieve vaultTimeout from stateService * Record activity per Account * Add userId to logout and add fallback if not present * Register AccountFactory * Pass userId in messages * Base changes for account switching di fixes (#2280) * [bug] Null checks on Account init * [bug] Use same stateService instance for all operations We override the stateService in browser, but currently don't pull the background service into popup and allow jslib to create its own instance of the base StateService for jslib services. This causes a split in in memory state between the three isntances that results in many errors, namely locking not working. * [chore] Update jslib * Pull in jslib * Pull in jslib * Pull in latest jslib to multiple stateservice inits * Check vault states before executing processReload * Adjust iterator * Update native messaging to include the userId (#2290) * Re-Add UserVerificationService * Fix email not being remembered by base component * Improve readability of reloadProcess * Removed unneeded null check * Fix constructor dependency (stateService) * Added missing await * Simplify dependency registration * Fixed typos * Reverted back to simple loop * Use vaultTimeoutService to retrieve Timeout Co-authored-by: Addison Beck <abeck@bitwarden.com> Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
2022-01-27 22:22:51 +01:00
let fallbackName = "key"
status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil)
}
if status == errSecSuccess {
let result = NSString(bytes: passwordPtr!, length: Int(passwordLength), encoding: String.Encoding.utf8.rawValue) as String?
SecKeychainItemFreeContent(nil, passwordPtr)
response.userInfo = [ SFExtensionMessageKey: [
"message": [
"command": "biometricUnlock",
"response": "unlocked",
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"keyB64": result!.replacingOccurrences(of: "\"", with: ""),
],
]]
} else {
response.userInfo = [
SFExtensionMessageKey: [
"message": [
"command": "biometricUnlock",
"response": "not enabled",
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
],
],
]
}
}
context.completeRequest(returningItems: [response], completionHandler: nil)
}
return
default:
return
}
context.completeRequest(returningItems: [response], completionHandler: nil)
}
}
func jsonSerialize<T: Encodable>(obj: T?) -> String? {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(obj)
return String(data: data, encoding: .utf8) ?? "null"
} catch _ {
return "null"
}
}
func jsonDeserialize<T: Decodable>(json: String?) -> T? {
if json == nil {
return nil
}
let decoder = JSONDecoder()
do {
let obj = try decoder.decode(T.self, from: json!.data(using: .utf8)!)
return obj
} catch _ {
return nil
}
}
class DownloadFileMessage: Decodable, Encodable {
var fileName: String
var blobData: String?
var blobOptions: DownloadFileMessageBlobOptions?
}
class DownloadFileMessageBlobOptions: Decodable, Encodable {
var type: String?
}