diff --git a/src/background/main.background.ts b/src/background/main.background.ts index e2a1e462c9..628a8342e5 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -249,7 +249,8 @@ export default class MainBackground { this.analytics, this.notificationsService, this.systemService, this.vaultTimeoutService, this.environmentService, this.policyService, this.userService, this.messagingService); this.nativeMessagingBackground = new NativeMessagingBackground(this.storageService, this.cryptoService, this.cryptoFunctionService, - this.vaultTimeoutService, this.runtimeBackground, this.i18nService, this.userService, this.messagingService, this.appIdService); + this.vaultTimeoutService, this.runtimeBackground, this.i18nService, this.userService, this.messagingService, this.appIdService, + this.platformUtilsService); this.commandsBackground = new CommandsBackground(this, this.passwordGenerationService, this.platformUtilsService, this.analytics, this.vaultTimeoutService); diff --git a/src/background/nativeMessaging.background.ts b/src/background/nativeMessaging.background.ts index 26020d400d..56e3509f76 100644 --- a/src/background/nativeMessaging.background.ts +++ b/src/background/nativeMessaging.background.ts @@ -3,6 +3,7 @@ import { CryptoService } from 'jslib/abstractions/crypto.service'; import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; import { MessagingService } from 'jslib/abstractions/messaging.service'; +import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { StorageService } from 'jslib/abstractions/storage.service'; import { UserService } from 'jslib/abstractions/user.service'; import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service'; @@ -33,7 +34,8 @@ export class NativeMessagingBackground { constructor(private storageService: StorageService, private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, private vaultTimeoutService: VaultTimeoutService, private runtimeBackground: RuntimeBackground, private i18nService: I18nService, private userService: UserService, - private messagingService: MessagingService, private appIdService: AppIdService) { + private messagingService: MessagingService, private appIdService: AppIdService, + private platformUtilsService: PlatformUtilsService) { this.storageService.save(ConstantsService.biometricFingerprintValidated, false); if (chrome?.permissions?.onAdded) { @@ -48,17 +50,27 @@ export class NativeMessagingBackground { this.appId = await this.appIdService.getAppId(); this.storageService.save(ConstantsService.biometricFingerprintValidated, false); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.port = BrowserApi.connectNative('com.8bit.bitwarden'); this.connecting = true; + const connectedCallback = () => { + this.connected = true; + this.connecting = false; + resolve(); + }; + + // Safari has a bundled native component which is always available, no need to + // check if the desktop app is running. + if (this.platformUtilsService.isSafari()) { + connectedCallback(); + } + this.port.onMessage.addListener(async (message: any) => { switch (message.command) { case 'connected': - this.connected = true; - this.connecting = false; - resolve(); + connectedCallback(); break; case 'disconnected': if (this.connecting) { @@ -114,15 +126,10 @@ export class NativeMessagingBackground { break; } case 'wrongUserId': - this.messagingService.send('showDialog', { - text: this.i18nService.t('nativeMessagingWrongUserDesc'), - title: this.i18nService.t('nativeMessagingWrongUserTitle'), - confirmText: this.i18nService.t('ok'), - type: 'error', - }); + this.showWrongUserDialog(); default: // Ignore since it belongs to another device - if (message.appId !== this.appId) { + if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) { return; } @@ -154,19 +161,35 @@ export class NativeMessagingBackground { }); } + showWrongUserDialog() { + this.messagingService.send('showDialog', { + text: this.i18nService.t('nativeMessagingWrongUserDesc'), + title: this.i18nService.t('nativeMessagingWrongUserTitle'), + confirmText: this.i18nService.t('ok'), + type: 'error', + }); + } + async send(message: any) { if (!this.connected) { await this.connect(); } + if (this.platformUtilsService.isSafari()) { + this.postMessage(message); + } else { + this.postMessage({appId: this.appId, message: await this.encryptMessage(message)}); + } + } + + async encryptMessage(message: any) { if (this.sharedSecret == null) { await this.secureCommunication(); } message.timestamp = Date.now(); - const encrypted = await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret); - this.postMessage({appId: this.appId, message: encrypted}); + return await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret); } getResponse(): Promise { @@ -197,7 +220,10 @@ export class NativeMessagingBackground { } private async onMessage(rawMessage: any) { - const message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret)); + let message = rawMessage; + if (!this.platformUtilsService.isSafari()) { + message = JSON.parse(await this.cryptoService.decryptToUtf8(rawMessage, this.sharedSecret)); + } if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) { // tslint:disable-next-line @@ -241,7 +267,21 @@ export class NativeMessagingBackground { } if (message.response === 'unlocked') { - this.cryptoService.setKey(new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64).buffer)); + await this.cryptoService.setKey(new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64).buffer)); + + // Verify key is correct by attempting to decrypt a secret + try { + await this.cryptoService.getFingerprint(await this.userService.getUserId()); + } catch (e) { + // tslint:disable-next-line + console.error('Unable to verify key:', e); + await this.cryptoService.clearKey(); + this.showWrongUserDialog(); + + message = false; + break; + } + this.vaultTimeoutService.biometricLocked = false; this.runtimeBackground.processMessage({command: 'unlocked'}, null, null); } diff --git a/src/safari/desktop.xcodeproj/project.pbxproj b/src/safari/desktop.xcodeproj/project.pbxproj index c3157a17bb..50c4059d07 100644 --- a/src/safari/desktop.xcodeproj/project.pbxproj +++ b/src/safari/desktop.xcodeproj/project.pbxproj @@ -31,7 +31,7 @@ containerPortal = 55E037402577FA6B00979016 /* Project object */; proxyType = 1; remoteGlobalIDString = 55E037592577FA6F00979016; - remoteInfo = "safari"; + remoteInfo = safari; }; /* End PBXContainerItemProxy section */ @@ -50,6 +50,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 5508DD7926051B5900A85C58 /* libswiftAppKit.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftAppKit.tbd; path = usr/lib/swift/libswiftAppKit.tbd; sourceTree = SDKROOT; }; 55E037482577FA6B00979016 /* desktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktop.app; sourceTree = BUILT_PRODUCTS_DIR; }; 55E0374B2577FA6B00979016 /* desktop.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = desktop.entitlements; sourceTree = ""; }; 55E0374C2577FA6B00979016 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -127,6 +128,7 @@ 55E0375E2577FA6F00979016 /* Frameworks */ = { isa = PBXGroup; children = ( + 5508DD7926051B5900A85C58 /* libswiftAppKit.tbd */, 55E0375F2577FA6F00979016 /* Cocoa.framework */, ); name = Frameworks; @@ -157,7 +159,7 @@ 55E037782577FA6F00979016 /* _locales */, ); name = Resources; - path = "safari"; + path = safari; sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ @@ -195,7 +197,7 @@ dependencies = ( ); name = safari; - productName = "safari"; + productName = safari; productReference = 55E0375A2577FA6F00979016 /* safari.appex */; productType = "com.apple.product-type.app-extension"; }; @@ -426,6 +428,53 @@ }; name = Release; }; + 55E037692577FA6F00979016 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_ENTITLEMENTS = safari/safari.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = HGT9YVMPAL; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = safari/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.14; + PRODUCT_BUNDLE_IDENTIFIER = com.bitwarden.desktop.safari; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 55E0376A2577FA6F00979016 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_ENTITLEMENTS = safari/safari.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = HGT9YVMPAL; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = safari/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.14; + PRODUCT_BUNDLE_IDENTIFIER = com.bitwarden.desktop.safari; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; 55E0376D2577FA6F00979016 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -435,7 +484,7 @@ CODE_SIGN_ENTITLEMENTS = desktop/desktop.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = LTZ2PFU5D6; + DEVELOPMENT_TEAM = HGT9YVMPAL; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = desktop/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -459,7 +508,7 @@ CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = LTZ2PFU5D6; + DEVELOPMENT_TEAM = HGT9YVMPAL; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = desktop/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -473,51 +522,6 @@ }; name = Release; }; - 55E037692577FA6F00979016 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_ENTITLEMENTS = "safari/safari.entitlements"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = LTZ2PFU5D6; - ENABLE_HARDENED_RUNTIME = YES; - INFOPLIST_FILE = "safari/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - PRODUCT_BUNDLE_IDENTIFIER = com.bitwarden.desktop.safari; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 55E0376A2577FA6F00979016 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_ENTITLEMENTS = "safari/safari.entitlements"; - CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = LTZ2PFU5D6; - ENABLE_HARDENED_RUNTIME = YES; - INFOPLIST_FILE = "safari/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.14; - PRODUCT_BUNDLE_IDENTIFIER = com.bitwarden.desktop.safari; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ diff --git a/src/safari/safari/SafariWebExtensionHandler.swift b/src/safari/safari/SafariWebExtensionHandler.swift index 35d7a498a2..fe241109e4 100644 --- a/src/safari/safari/SafariWebExtensionHandler.swift +++ b/src/safari/safari/SafariWebExtensionHandler.swift @@ -1,7 +1,9 @@ import SafariServices import os.log +import LocalAuthentication let SFExtensionMessageKey = "message" +let ServiceName = "Bitwarden" class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { @@ -78,14 +80,68 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { context.completeRequest(returningItems: [response], completionHandler: nil) } return + case "biometricUnlock": + + var error: NSError? + let laContext = LAContext() + + guard laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "biometricUnlock", + "response": "not supported", + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + ], + ], + ] + break; + } + laContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Bitwarden Safari Extension") { (success, error) in + if success { + let passwordName = "key" + var passwordLength: UInt32 = 0 + var passwordPtr: UnsafeMutableRawPointer? = nil + + let status = SecKeychainFindGenericPassword(nil, UInt32(ServiceName.utf8.count), ServiceName, UInt32(passwordName.utf8.count), passwordName, &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(obj: T?) -> String? { diff --git a/src/services/browserPlatformUtils.service.ts b/src/services/browserPlatformUtils.service.ts index b29aaff394..e93e50fc1e 100644 --- a/src/services/browserPlatformUtils.service.ts +++ b/src/services/browserPlatformUtils.service.ts @@ -289,8 +289,11 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService } async supportsBiometric() { - const isUnsuportedFirefox = this.isFirefox() && parseInt((await browser.runtime.getBrowserInfo()).version.split('.')[0], 10) < 87; - return !isUnsuportedFirefox && !this.isSafari(); + if (this.isFirefox()) { + return parseInt((await browser.runtime.getBrowserInfo()).version.split('.')[0], 10) >= 87; + } + + return true; } authenticateBiometric() {