From 61ba9692bc7c9db486cccd41b540961ff05d7e8c Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Wed, 14 Jun 2023 14:59:53 -0400 Subject: [PATCH] migrate native messaging for biometrics to use new key model - support backwards compatibility - update safari web extension to send user key - add error handling --- apps/browser/src/_locales/en/messages.json | 6 +++ .../background/nativeMessaging.background.ts | 40 +++++++++++++++++-- .../services/browser-crypto.service.ts | 15 ++++++- .../safari/SafariWebExtensionHandler.swift | 28 ++++++------- .../src/services/native-messaging.service.ts | 14 +++++-- .../src/platform/services/crypto.service.ts | 6 ++- 6 files changed, 85 insertions(+), 24 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 27c985453e9..dea0d27aa1c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1599,6 +1599,12 @@ "biometricsNotSupportedDesc": { "message": "Browser biometrics is not supported on this device." }, + "biometricsFailedTitle": { + "message": "Biometrics failed" + }, + "biometricsFailedDesc": { + "message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support." + }, "nativeMessaginPermissionErrorTitle": { "message": "Permission not provided" }, diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index eb3936c192c..e1b7fdf4231 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -13,6 +13,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, SymmetricCryptoKey, + UserSymKey, } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { BrowserApi } from "../platform/browser/browser-api"; @@ -45,6 +46,7 @@ type ReceiveMessage = { // Unlock key keyB64?: string; + userKeyB64?: string; }; type ReceiveMessageOuter = { @@ -323,9 +325,41 @@ export class NativeMessagingBackground { } if (message.response === "unlocked") { - await this.cryptoService.setMasterKey( - new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64).buffer) as MasterKey - ); + try { + if (message.userKeyB64) { + const userKey = new SymmetricCryptoKey( + Utils.fromB64ToArray(message.userKeyB64).buffer + ) as UserSymKey; + await this.cryptoService.setUserKey(userKey); + } else if (message.keyB64) { + // backwards compatibility + let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); + encUserKey ||= await this.stateService.getUserSymKeyMasterKey(); + if (!encUserKey) { + throw new Error("No encrypted user key found"); + } + const masterKey = new SymmetricCryptoKey( + Utils.fromB64ToArray(message.keyB64).buffer + ) as MasterKey; + const userKey = await this.cryptoService.decryptUserSymKeyWithMasterKey( + masterKey, + new EncString(encUserKey) + ); + await this.cryptoService.setMasterKey(masterKey); + await this.cryptoService.setUserKey(userKey); + } else { + throw new Error("No key received"); + } + } catch (e) { + this.logService.error("Unable to set key: " + e); + this.messagingService.send("showDialog", { + title: { key: "biometricsFailedTitle" }, + content: { key: "biometricsFailedDesc" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "danger", + }); + } // Verify key is correct by attempting to decrypt a secret try { diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index 75558436b26..14b60e9cacf 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -1,11 +1,22 @@ import { KeySuffixOptions } from "@bitwarden/common/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + SymmetricCryptoKey, + UserSymKey, +} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; export class BrowserCryptoService extends CryptoService { - protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) { + protected override async retrieveUserKeyFromStorage( + keySuffix: KeySuffixOptions, + userId?: string + ): Promise { if (keySuffix === KeySuffixOptions.Biometric) { await this.platformUtilService.authenticateBiometric(); - return (await this.getUserKeyFromMemory())?.keyB64; + const userKey = await this.getUserKeyFromMemory(); + if (userKey) { + return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey.keyB64).buffer) as UserSymKey; + } } return await super.retrieveUserKeyFromStorage(keySuffix); diff --git a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift index 80ea214b4b0..b0688e3bebb 100644 --- a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift +++ b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift @@ -19,11 +19,11 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { 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 @@ -59,12 +59,12 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { guard let data = blobData else { return } - + let panel = NSSavePanel() panel.canCreateDirectories = true panel.nameFieldStringValue = dlMsg.fileName let response = panel.runModal(); - + if response == NSApplication.ModalResponse.OK { if let url = panel.url { do { @@ -87,12 +87,12 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } return case "biometricUnlock": - + var error: NSError? let laContext = LAContext() - + laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) - + if let e = error, e.code != kLAErrorBiometryLockout { response.userInfo = [ SFExtensionMessageKey: [ @@ -123,26 +123,26 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { guard let userId = message?["userId"] as? String else { return } - let passwordName = userId + "_masterkey_biometric" + let passwordName = userId + "_user_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 { 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: ""), + "userKeyB64": result!.replacingOccurrences(of: "\"", with: ""), ], ]] } else { @@ -157,10 +157,10 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { ] } } - + context.completeRequest(returningItems: [response], completionHandler: nil) } - + return default: return diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index c566dcb7061..2b4950f7e03 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -136,14 +136,22 @@ export class NativeMessagingService { }); } - const key = await this.cryptoService.getUserKeyFromStorage( + const userKey = await this.cryptoService.getUserKeyFromStorage( KeySuffixOptions.Biometric, message.userId ); + const masterKey = await this.cryptoService.getMasterKey(message.userId); - if (key != null) { + if (userKey != null) { + // we send the master key still for backwards compatibility + // with older browser extensions this.send( - { command: "biometricUnlock", response: "unlocked", keyB64: key.keyB64 }, + { + command: "biometricUnlock", + response: "unlocked", + keyB64: masterKey.keyB64, + userKeyB64: userKey.keyB64, + }, appId ); } else { diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index d7bd999ee75..dae4a195881 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -739,10 +739,12 @@ export class CryptoService implements CryptoServiceAbstraction { keySuffix: KeySuffixOptions, userId?: string ): Promise { - if (keySuffix === KeySuffixOptions.Pin) { + if (keySuffix === KeySuffixOptions.Auto) { await this.migrateAutoKeyIfNeeded(userId); const userKey = await this.stateService.getUserSymKeyAuto({ userId: userId }); - return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey).buffer) as UserSymKey; + if (userKey) { + return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey).buffer) as UserSymKey; + } } return null; }