1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

migrate native messaging for biometrics to use new key model

- support backwards compatibility
- update safari web extension to send user key
- add error handling
This commit is contained in:
Jacob Fink
2023-06-14 14:59:53 -04:00
parent 9c6739f40a
commit 61ba9692bc
6 changed files with 85 additions and 24 deletions

View File

@@ -1599,6 +1599,12 @@
"biometricsNotSupportedDesc": { "biometricsNotSupportedDesc": {
"message": "Browser biometrics is not supported on this device." "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": { "nativeMessaginPermissionErrorTitle": {
"message": "Permission not provided" "message": "Permission not provided"
}, },

View File

@@ -13,6 +13,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { import {
MasterKey, MasterKey,
SymmetricCryptoKey, SymmetricCryptoKey,
UserSymKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserApi } from "../platform/browser/browser-api";
@@ -45,6 +46,7 @@ type ReceiveMessage = {
// Unlock key // Unlock key
keyB64?: string; keyB64?: string;
userKeyB64?: string;
}; };
type ReceiveMessageOuter = { type ReceiveMessageOuter = {
@@ -323,9 +325,41 @@ export class NativeMessagingBackground {
} }
if (message.response === "unlocked") { if (message.response === "unlocked") {
await this.cryptoService.setMasterKey( try {
new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64).buffer) as MasterKey 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 // Verify key is correct by attempting to decrypt a secret
try { try {

View File

@@ -1,11 +1,22 @@
import { KeySuffixOptions } from "@bitwarden/common/enums"; 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"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
export class BrowserCryptoService extends CryptoService { export class BrowserCryptoService extends CryptoService {
protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) { protected override async retrieveUserKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: string
): Promise<UserSymKey> {
if (keySuffix === KeySuffixOptions.Biometric) { if (keySuffix === KeySuffixOptions.Biometric) {
await this.platformUtilService.authenticateBiometric(); 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); return await super.retrieveUserKeyFromStorage(keySuffix);

View File

@@ -19,11 +19,11 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg) os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)
let response = NSExtensionItem() let response = NSExtensionItem()
guard let command = message?["command"] as? String else { guard let command = message?["command"] as? String else {
return return
} }
switch (command) { switch (command) {
case "readFromClipboard": case "readFromClipboard":
let pasteboard = NSPasteboard.general let pasteboard = NSPasteboard.general
@@ -59,12 +59,12 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
guard let data = blobData else { guard let data = blobData else {
return return
} }
let panel = NSSavePanel() let panel = NSSavePanel()
panel.canCreateDirectories = true panel.canCreateDirectories = true
panel.nameFieldStringValue = dlMsg.fileName panel.nameFieldStringValue = dlMsg.fileName
let response = panel.runModal(); let response = panel.runModal();
if response == NSApplication.ModalResponse.OK { if response == NSApplication.ModalResponse.OK {
if let url = panel.url { if let url = panel.url {
do { do {
@@ -87,12 +87,12 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
} }
return return
case "biometricUnlock": case "biometricUnlock":
var error: NSError? var error: NSError?
let laContext = LAContext() let laContext = LAContext()
laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
if let e = error, e.code != kLAErrorBiometryLockout { if let e = error, e.code != kLAErrorBiometryLockout {
response.userInfo = [ response.userInfo = [
SFExtensionMessageKey: [ SFExtensionMessageKey: [
@@ -123,26 +123,26 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
guard let userId = message?["userId"] as? String else { guard let userId = message?["userId"] as? String else {
return return
} }
let passwordName = userId + "_masterkey_biometric" let passwordName = userId + "_user_biometric"
var passwordLength: UInt32 = 0 var passwordLength: UInt32 = 0
var passwordPtr: UnsafeMutableRawPointer? = nil var passwordPtr: UnsafeMutableRawPointer? = nil
var status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(passwordName.utf8.count), passwordName, &passwordLength, &passwordPtr, nil) var status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(passwordName.utf8.count), passwordName, &passwordLength, &passwordPtr, nil)
if status != errSecSuccess { if status != errSecSuccess {
let fallbackName = "key" let fallbackName = "key"
status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil) status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil)
} }
if status == errSecSuccess { if status == errSecSuccess {
let result = NSString(bytes: passwordPtr!, length: Int(passwordLength), encoding: String.Encoding.utf8.rawValue) as String? let result = NSString(bytes: passwordPtr!, length: Int(passwordLength), encoding: String.Encoding.utf8.rawValue) as String?
SecKeychainItemFreeContent(nil, passwordPtr) SecKeychainItemFreeContent(nil, passwordPtr)
response.userInfo = [ SFExtensionMessageKey: [ response.userInfo = [ SFExtensionMessageKey: [
"message": [ "message": [
"command": "biometricUnlock", "command": "biometricUnlock",
"response": "unlocked", "response": "unlocked",
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"keyB64": result!.replacingOccurrences(of: "\"", with: ""), "userKeyB64": result!.replacingOccurrences(of: "\"", with: ""),
], ],
]] ]]
} else { } else {
@@ -157,10 +157,10 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
] ]
} }
} }
context.completeRequest(returningItems: [response], completionHandler: nil) context.completeRequest(returningItems: [response], completionHandler: nil)
} }
return return
default: default:
return return

View File

@@ -136,14 +136,22 @@ export class NativeMessagingService {
}); });
} }
const key = await this.cryptoService.getUserKeyFromStorage( const userKey = await this.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Biometric, KeySuffixOptions.Biometric,
message.userId 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( this.send(
{ command: "biometricUnlock", response: "unlocked", keyB64: key.keyB64 }, {
command: "biometricUnlock",
response: "unlocked",
keyB64: masterKey.keyB64,
userKeyB64: userKey.keyB64,
},
appId appId
); );
} else { } else {

View File

@@ -739,10 +739,12 @@ export class CryptoService implements CryptoServiceAbstraction {
keySuffix: KeySuffixOptions, keySuffix: KeySuffixOptions,
userId?: string userId?: string
): Promise<UserSymKey> { ): Promise<UserSymKey> {
if (keySuffix === KeySuffixOptions.Pin) { if (keySuffix === KeySuffixOptions.Auto) {
await this.migrateAutoKeyIfNeeded(userId); await this.migrateAutoKeyIfNeeded(userId);
const userKey = await this.stateService.getUserSymKeyAuto({ userId: 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; return null;
} }