1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-07 19:13:39 +00:00

[PM-10741] Refactor biometrics interface & add dynamic status (#10973)

This commit is contained in:
Bernd Schoolmann
2025-01-08 10:46:00 +01:00
committed by GitHub
parent 0bd988dac8
commit 72121cda94
66 changed files with 1840 additions and 1459 deletions

View File

@@ -4656,6 +4656,33 @@
"noEditPermissions": {
"message": "You don't have permission to edit this item"
},
"biometricsStatusHelptextUnlockNeeded": {
"message": "Biometric unlock is unavailable because PIN or password unlock is required first."
},
"biometricsStatusHelptextHardwareUnavailable": {
"message": "Biometric unlock is currently unavailable."
},
"biometricsStatusHelptextAutoSetupNeeded": {
"message": "Biometric unlock is unavailable due to misconfigured system files."
},
"biometricsStatusHelptextManualSetupNeeded": {
"message": "Biometric unlock is unavailable due to misconfigured system files."
},
"biometricsStatusHelptextDesktopDisconnected": {
"message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed."
},
"biometricsStatusHelptextNotEnabledInDesktop": {
"message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.",
"placeholders": {
"email": {
"content": "$1",
"example": "mail@example.com"
}
}
},
"biometricsStatusHelptextUnavailableReasonUnknown": {
"message": "Biometric unlock is currently unavailable for an unknown reason."
},
"authenticating": {
"message": "Authenticating"
},

View File

@@ -8,6 +8,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { AvatarModule, ItemModule } from "@bitwarden/components";
import { BiometricsService } from "@bitwarden/key-management";
import { AccountSwitcherService, AvailableAccount } from "./services/account-switcher.service";
@@ -26,6 +27,7 @@ export class AccountComponent {
private location: Location,
private i18nService: I18nService,
private logService: LogService,
private biometricsService: BiometricsService,
) {}
get specialAccountAddId() {
@@ -45,6 +47,9 @@ export class AccountComponent {
// locked or logged out account statuses are handled by background and app.component
if (result?.status === AuthenticationStatus.Unlocked) {
this.location.back();
await this.biometricsService.setShouldAutopromptNow(false);
} else {
await this.biometricsService.setShouldAutopromptNow(true);
}
this.loading.emit(false);
}

View File

@@ -11,13 +11,16 @@
<h2 bitTypography="h6">{{ "unlockMethods" | i18n }}</h2>
</bit-section-header>
<bit-card>
<bit-form-control *ngIf="supportsBiometric">
<bit-form-control>
<input bitCheckbox id="biometric" type="checkbox" formControlName="biometric" />
<bit-label for="biometric" class="tw-whitespace-normal">{{
"unlockWithBiometrics" | i18n
}}</bit-label>
<bit-hint *ngIf="biometricUnavailabilityReason">
{{ biometricUnavailabilityReason }}
</bit-hint>
</bit-form-control>
<bit-form-control class="tw-pl-5" *ngIf="supportsBiometric && this.form.value.biometric">
<bit-form-control class="tw-pl-5" *ngIf="this.form.value.biometric">
<input
bitCheckbox
id="autoBiometricsPrompt"

View File

@@ -17,6 +17,7 @@ import {
Subject,
switchMap,
takeUntil,
timer,
} from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -53,7 +54,12 @@ import {
TypographyModule,
ToastService,
} from "@bitwarden/components";
import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management";
import {
KeyService,
BiometricsService,
BiometricStateService,
BiometricsStatus,
} from "@bitwarden/key-management";
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
import { BrowserApi } from "../../../platform/browser/browser-api";
@@ -99,7 +105,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
vaultTimeoutOptions: VaultTimeoutOption[] = [];
hasVaultTimeoutPolicy = false;
supportsBiometric: boolean;
biometricUnavailabilityReason: string;
showChangeMasterPass = true;
accountSwitcherEnabled = false;
@@ -199,7 +205,40 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
};
this.form.patchValue(initialValues, { emitEvent: false });
this.supportsBiometric = await this.biometricsService.supportsBiometric();
timer(0, 1000)
.pipe(
switchMap(async () => {
const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id);
const biometricSettingAvailable =
(status !== BiometricsStatus.DesktopDisconnected &&
status !== BiometricsStatus.NotEnabledInConnectedDesktopApp) ||
(await this.vaultTimeoutSettingsService.isBiometricLockSet());
if (!biometricSettingAvailable) {
this.form.controls.biometric.disable({ emitEvent: false });
} else {
this.form.controls.biometric.enable({ emitEvent: false });
}
if (status === BiometricsStatus.DesktopDisconnected && !biometricSettingAvailable) {
this.biometricUnavailabilityReason = this.i18nService.t(
"biometricsStatusHelptextDesktopDisconnected",
);
} else if (
status === BiometricsStatus.NotEnabledInConnectedDesktopApp &&
!biometricSettingAvailable
) {
this.biometricUnavailabilityReason = this.i18nService.t(
"biometricsStatusHelptextNotEnabledInDesktop",
activeAccount.email,
);
} else {
this.biometricUnavailabilityReason = "";
}
}),
takeUntil(this.destroy$),
)
.subscribe();
this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword();
this.form.controls.vaultTimeout.valueChanges
@@ -399,7 +438,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
}
async updateBiometric(enabled: boolean) {
if (enabled && this.supportsBiometric) {
if (enabled) {
let granted;
try {
granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] });
@@ -471,7 +510,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
const biometricsPromise = async () => {
try {
const result = await this.biometricsService.authenticateBiometric();
const result = await this.biometricsService.authenticateWithBiometrics();
// prevent duplicate dialog
biometricsResponseReceived = true;

View File

@@ -204,6 +204,7 @@ import {
BiometricStateService,
BiometricsService,
DefaultBiometricStateService,
DefaultKeyService,
DefaultKdfConfigService,
KdfConfigService,
KeyService as KeyServiceAbstraction,
@@ -241,7 +242,6 @@ import AutofillService from "../autofill/services/autofill.service";
import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service";
import { SafariApp } from "../browser/safariApp";
import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service";
import { BrowserKeyService } from "../key-management/browser-key.service";
import { BrowserApi } from "../platform/browser/browser-api";
import { flagEnabled } from "../platform/flags";
import { UpdateBadge } from "../platform/listeners/update-badge";
@@ -416,6 +416,7 @@ export default class MainBackground {
await this.refreshMenu(true);
if (this.systemService != null) {
await this.systemService.clearPendingClipboard();
await this.biometricsService.setShouldAutopromptNow(false);
await this.processReloadService.startProcessReload(this.authService);
}
};
@@ -633,6 +634,7 @@ export default class MainBackground {
this.biometricsService = new BackgroundBrowserBiometricsService(
runtimeNativeMessagingBackground,
this.logService,
);
this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider);
@@ -649,7 +651,7 @@ export default class MainBackground {
this.stateService,
);
this.keyService = new BrowserKeyService(
this.keyService = new DefaultKeyService(
this.pinService,
this.masterPasswordService,
this.keyGenerationService,
@@ -660,8 +662,6 @@ export default class MainBackground {
this.stateService,
this.accountService,
this.stateProvider,
this.biometricStateService,
this.biometricsService,
this.kdfConfigService,
);
@@ -857,10 +857,8 @@ export default class MainBackground {
this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinService,
this.logService,
this.vaultTimeoutSettingsService,
this.platformUtilsService,
this.kdfConfigService,
this.biometricsService,
);
this.vaultFilterService = new VaultFilterService(
@@ -890,6 +888,7 @@ export default class MainBackground {
this.stateEventRunnerService,
this.taskSchedulerService,
this.logService,
this.biometricsService,
lockedCallback,
logoutCallback,
);
@@ -1081,6 +1080,7 @@ export default class MainBackground {
this.vaultTimeoutSettingsService,
this.biometricStateService,
this.accountService,
this.logService,
);
// Other fields

View File

@@ -1,10 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, map } from "rxjs";
import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@@ -14,18 +13,19 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserKey } from "@bitwarden/common/types/key";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
import { KeyService, BiometricStateService, BiometricsCommands } from "@bitwarden/key-management";
import { BrowserApi } from "../platform/browser/browser-api";
import RuntimeBackground from "./runtime.background";
const MessageValidTimeout = 10 * 1000;
const MessageNoResponseTimeout = 60 * 1000;
const HashAlgorithmForEncryption = "sha1";
type Message = {
command: string;
messageId?: number;
// Filled in by this service
userId?: string;
@@ -43,6 +43,7 @@ type OuterMessage = {
type ReceiveMessage = {
timestamp: number;
command: string;
messageId: number;
response?: any;
// Unlock key
@@ -53,19 +54,23 @@ type ReceiveMessage = {
type ReceiveMessageOuter = {
command: string;
appId: string;
messageId?: number;
// Should only have one of these.
message?: EncString;
sharedSecret?: string;
};
type Callback = {
resolver: any;
rejecter: any;
};
export class NativeMessagingBackground {
private connected = false;
connected = false;
private connecting: boolean;
private port: browser.runtime.Port | chrome.runtime.Port;
private resolver: any = null;
private rejecter: any = null;
private privateKey: Uint8Array = null;
private publicKey: Uint8Array = null;
private secureSetupResolve: any = null;
@@ -73,6 +78,11 @@ export class NativeMessagingBackground {
private appId: string;
private validatingFingerprint: boolean;
private messageId = 0;
private callbacks = new Map<number, Callback>();
isConnectedToOutdatedDesktopClient = true;
constructor(
private keyService: KeyService,
private encryptService: EncryptService,
@@ -97,6 +107,7 @@ export class NativeMessagingBackground {
}
async connect() {
this.logService.info("[Native Messaging IPC] Connecting to Bitwarden Desktop app...");
this.appId = await this.appIdService.getAppId();
await this.biometricStateService.setFingerprintValidated(false);
@@ -106,6 +117,9 @@ export class NativeMessagingBackground {
this.connecting = true;
const connectedCallback = () => {
this.logService.info(
"[Native Messaging IPC] Connection to Bitwarden Desktop app established!",
);
this.connected = true;
this.connecting = false;
resolve();
@@ -123,11 +137,17 @@ export class NativeMessagingBackground {
connectedCallback();
break;
case "disconnected":
this.logService.info("[Native Messaging IPC] Disconnected from Bitwarden Desktop app.");
if (this.connecting) {
reject(new Error("startDesktop"));
}
this.connected = false;
this.port.disconnect();
// reject all
for (const callback of this.callbacks.values()) {
callback.rejecter("disconnected");
}
this.callbacks.clear();
break;
case "setupEncryption": {
// Ignore since it belongs to another device
@@ -147,6 +167,16 @@ export class NativeMessagingBackground {
await this.biometricStateService.setFingerprintValidated(true);
}
this.sharedSecret = new SymmetricCryptoKey(decrypted);
this.logService.info("[Native Messaging IPC] Secure channel established");
if ("messageId" in message) {
this.logService.info("[Native Messaging IPC] Non-legacy desktop client");
this.isConnectedToOutdatedDesktopClient = false;
} else {
this.logService.info("[Native Messaging IPC] Legacy desktop client");
this.isConnectedToOutdatedDesktopClient = true;
}
this.secureSetupResolve();
break;
}
@@ -155,17 +185,25 @@ export class NativeMessagingBackground {
if (message.appId !== this.appId) {
return;
}
this.logService.warning(
"[Native Messaging IPC] Secure channel encountered an error; disconnecting and wiping keys...",
);
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
this.rejecter({
message: "invalidateEncryption",
});
if (this.callbacks.has(message.messageId)) {
this.callbacks.get(message.messageId).rejecter({
message: "invalidateEncryption",
});
}
return;
case "verifyFingerprint": {
if (this.sharedSecret == null) {
this.logService.info(
"[Native Messaging IPC] Desktop app requested trust verification by fingerprint.",
);
this.validatingFingerprint = true;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -174,9 +212,11 @@ export class NativeMessagingBackground {
break;
}
case "wrongUserId":
this.rejecter({
message: "wrongUserId",
});
if (this.callbacks.has(message.messageId)) {
this.callbacks.get(message.messageId).rejecter({
message: "wrongUserId",
});
}
return;
default:
// Ignore since it belongs to another device
@@ -210,6 +250,60 @@ export class NativeMessagingBackground {
});
}
async callCommand(message: Message): Promise<any> {
const messageId = this.messageId++;
if (
message.command == BiometricsCommands.Unlock ||
message.command == BiometricsCommands.IsAvailable
) {
// TODO remove after 2025.01
// wait until there is no other callbacks, or timeout
const call = await firstValueFrom(
race(
from([false]).pipe(delay(5000)),
timer(0, 100).pipe(
filter(() => this.callbacks.size === 0),
map(() => true),
),
),
);
if (!call) {
this.logService.info(
`[Native Messaging IPC] Message of type ${message.command} did not get a response before timing out`,
);
return;
}
}
const callback = new Promise((resolver, rejecter) => {
this.callbacks.set(messageId, { resolver, rejecter });
});
message.messageId = messageId;
try {
await this.send(message);
} catch (e) {
this.logService.info(
`[Native Messaging IPC] Error sending message of type ${message.command} to Bitwarden Desktop app. Error: ${e}`,
);
const callback = this.callbacks.get(messageId);
this.callbacks.delete(messageId);
callback.rejecter("errorConnecting");
}
setTimeout(() => {
if (this.callbacks.has(messageId)) {
this.logService.info("[Native Messaging IPC] Message timed out and received no response");
this.callbacks.get(messageId).rejecter({
message: "timeout",
});
this.callbacks.delete(messageId);
}
}, MessageNoResponseTimeout);
return callback;
}
async send(message: Message) {
if (!this.connected) {
await this.connect();
@@ -233,20 +327,7 @@ export class NativeMessagingBackground {
return await this.encryptService.encrypt(JSON.stringify(message), this.sharedSecret);
}
getResponse(): Promise<any> {
return new Promise((resolve, reject) => {
this.resolver = function (response: any) {
resolve(response);
};
this.rejecter = function (resp: any) {
reject({
message: resp,
});
};
});
}
private postMessage(message: OuterMessage) {
private postMessage(message: OuterMessage, messageId?: number) {
// Wrap in try-catch to when the port disconnected without triggering `onDisconnect`.
try {
const msg: any = message;
@@ -262,13 +343,17 @@ export class NativeMessagingBackground {
}
this.port.postMessage(msg);
} catch (e) {
this.logService.error("NativeMessaging port disconnected, disconnecting.");
this.logService.info(
"[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.",
);
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
this.rejecter("invalidateEncryption");
if (this.callbacks.has(messageId)) {
this.callbacks.get(messageId).rejecter("invalidateEncryption");
}
}
}
@@ -285,90 +370,30 @@ export class NativeMessagingBackground {
}
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
this.logService.error("NativeMessage is to old, ignoring.");
this.logService.info("[Native Messaging IPC] Received an old native message, ignoring...");
return;
}
switch (message.command) {
case "biometricUnlock": {
if (
["not available", "not enabled", "not supported", "not unlocked", "canceled"].includes(
message.response,
)
) {
this.rejecter(message.response);
return;
}
const messageId = message.messageId;
// Check for initial setup of biometric unlock
const enabled = await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$);
if (enabled === null || enabled === false) {
if (message.response === "unlocked") {
await this.biometricStateService.setBiometricUnlockEnabled(true);
}
break;
}
// Ignore unlock if already unlocked
if ((await this.authService.getAuthStatus()) === AuthenticationStatus.Unlocked) {
break;
}
if (message.response === "unlocked") {
try {
if (message.userKeyB64) {
const userKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(message.userKeyB64),
) as UserKey;
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const isUserKeyValid = await this.keyService.validateUserKey(userKey, activeUserId);
if (isUserKeyValid) {
await this.keyService.setUserKey(userKey, activeUserId);
} else {
this.logService.error("Unable to verify biometric unlocked userkey");
await this.keyService.clearKeys(activeUserId);
this.rejecter("userkey wrong");
return;
}
} else {
throw new Error("No key received");
}
} catch (e) {
this.logService.error("Unable to set key: " + e);
this.rejecter("userkey wrong");
return;
}
// Verify key is correct by attempting to decrypt a secret
try {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.keyService.getFingerprint(userId);
} catch (e) {
this.logService.error("Unable to verify key: " + e);
await this.keyService.clearKeys();
this.rejecter("userkey wrong");
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.runtimeBackground.processMessage({ command: "unlocked" });
}
break;
}
case "biometricUnlockAvailable": {
this.resolver(message);
break;
}
default:
this.logService.error("NativeMessage, got unknown command: " + message.command);
break;
if (
message.command == BiometricsCommands.Unlock ||
message.command == BiometricsCommands.IsAvailable
) {
this.logService.info(
`[Native Messaging IPC] Received legacy message of type ${message.command}`,
);
const messageId = this.callbacks.keys().next().value;
const resolver = this.callbacks.get(messageId);
this.callbacks.delete(messageId);
resolver.resolver(message);
return;
}
if (this.resolver) {
this.resolver(message);
if (this.callbacks.has(messageId)) {
this.callbacks.get(messageId).resolver(message);
} else {
this.logService.info("[Native Messaging IPC] Received message without a callback", message);
}
}
@@ -384,6 +409,7 @@ export class NativeMessagingBackground {
command: "setupEncryption",
publicKey: Utils.fromBufferToB64(publicKey),
userId: userId,
messageId: this.messageId++,
});
return new Promise((resolve, reject) => (this.secureSetupResolve = resolve));

View File

@@ -16,6 +16,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherType } from "@bitwarden/common/vault/enums";
import { BiometricsCommands } from "@bitwarden/key-management";
import { MessageListener, isExternalMessage } from "../../../../libs/common/src/platform/messaging";
import {
@@ -71,8 +72,10 @@ export default class RuntimeBackground {
sendResponse: (response: any) => void,
) => {
const messagesWithResponse = [
"biometricUnlock",
"biometricUnlockAvailable",
BiometricsCommands.AuthenticateWithBiometrics,
BiometricsCommands.GetBiometricsStatus,
BiometricsCommands.UnlockWithBiometricsForUser,
BiometricsCommands.GetBiometricsStatusForUser,
"getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag",
"getInlineMenuFieldQualificationFeatureFlag",
"getInlineMenuTotpFeatureFlag",
@@ -185,13 +188,17 @@ export default class RuntimeBackground {
break;
}
break;
case "biometricUnlock": {
const result = await this.main.biometricsService.authenticateBiometric();
return result;
case BiometricsCommands.AuthenticateWithBiometrics: {
return await this.main.biometricsService.authenticateWithBiometrics();
}
case "biometricUnlockAvailable": {
const result = await this.main.biometricsService.isBiometricUnlockAvailable();
return result;
case BiometricsCommands.GetBiometricsStatus: {
return await this.main.biometricsService.getBiometricsStatus();
}
case BiometricsCommands.UnlockWithBiometricsForUser: {
return await this.main.biometricsService.unlockWithBiometricsForUser(msg.userId);
}
case BiometricsCommands.GetBiometricsStatusForUser: {
return await this.main.biometricsService.getBiometricsStatusForUser(msg.userId);
}
case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": {
return await this.configService.getFeatureFlag(

View File

@@ -1,36 +1,136 @@
import { Injectable } from "@angular/core";
import { NativeMessagingBackground } from "../../background/nativeMessaging.background";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { BiometricsService, BiometricsCommands, BiometricsStatus } from "@bitwarden/key-management";
import { BrowserBiometricsService } from "./browser-biometrics.service";
import { NativeMessagingBackground } from "../../background/nativeMessaging.background";
import { BrowserApi } from "../../platform/browser/browser-api";
@Injectable()
export class BackgroundBrowserBiometricsService extends BrowserBiometricsService {
constructor(private nativeMessagingBackground: () => NativeMessagingBackground) {
export class BackgroundBrowserBiometricsService extends BiometricsService {
constructor(
private nativeMessagingBackground: () => NativeMessagingBackground,
private logService: LogService,
) {
super();
}
async authenticateBiometric(): Promise<boolean> {
const responsePromise = this.nativeMessagingBackground().getResponse();
await this.nativeMessagingBackground().send({ command: "biometricUnlock" });
const response = await responsePromise;
return response.response === "unlocked";
async authenticateWithBiometrics(): Promise<boolean> {
try {
await this.ensureConnected();
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
const response = await this.nativeMessagingBackground().callCommand({
command: BiometricsCommands.Unlock,
});
return response.response == "unlocked";
} else {
const response = await this.nativeMessagingBackground().callCommand({
command: BiometricsCommands.AuthenticateWithBiometrics,
});
return response.response;
}
} catch (e) {
this.logService.info("Biometric authentication failed", e);
return false;
}
}
async isBiometricUnlockAvailable(): Promise<boolean> {
const responsePromise = this.nativeMessagingBackground().getResponse();
await this.nativeMessagingBackground().send({ command: "biometricUnlockAvailable" });
const response = await responsePromise;
return response.response === "available";
async getBiometricsStatus(): Promise<BiometricsStatus> {
if (!(await BrowserApi.permissionsGranted(["nativeMessaging"]))) {
return BiometricsStatus.NativeMessagingPermissionMissing;
}
try {
await this.ensureConnected();
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
const response = await this.nativeMessagingBackground().callCommand({
command: BiometricsCommands.IsAvailable,
});
const resp =
response.response == "available"
? BiometricsStatus.Available
: BiometricsStatus.HardwareUnavailable;
return resp;
} else {
const response = await this.nativeMessagingBackground().callCommand({
command: BiometricsCommands.GetBiometricsStatus,
});
if (response.response) {
return response.response;
}
}
return BiometricsStatus.Available;
} catch (e) {
return BiometricsStatus.DesktopDisconnected;
}
}
async biometricsNeedsSetup(): Promise<boolean> {
async unlockWithBiometricsForUser(userId: UserId): Promise<UserKey | null> {
try {
await this.ensureConnected();
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
const response = await this.nativeMessagingBackground().callCommand({
command: BiometricsCommands.Unlock,
});
if (response.response == "unlocked") {
return response.userKeyB64;
} else {
return null;
}
} else {
const response = await this.nativeMessagingBackground().callCommand({
command: BiometricsCommands.UnlockWithBiometricsForUser,
userId: userId,
});
if (response.response) {
return response.userKeyB64;
} else {
return null;
}
}
} catch (e) {
this.logService.info("Biometric unlock for user failed", e);
throw new Error("Biometric unlock failed");
}
}
async getBiometricsStatusForUser(id: UserId): Promise<BiometricsStatus> {
try {
await this.ensureConnected();
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
return await this.getBiometricsStatus();
}
return (
await this.nativeMessagingBackground().callCommand({
command: BiometricsCommands.GetBiometricsStatusForUser,
userId: id,
})
).response;
} catch (e) {
return BiometricsStatus.DesktopDisconnected;
}
}
// the first time we call, this might use an outdated version of the protocol, so we drop the response
private async ensureConnected() {
if (!this.nativeMessagingBackground().connected) {
await this.nativeMessagingBackground().callCommand({
command: BiometricsCommands.IsAvailable,
});
}
}
async getShouldAutopromptNow(): Promise<boolean> {
return false;
}
async biometricsSupportsAutoSetup(): Promise<boolean> {
return false;
}
async biometricsSetup(): Promise<void> {}
async setShouldAutopromptNow(value: boolean): Promise<void> {}
}

View File

@@ -1,19 +0,0 @@
import { Injectable } from "@angular/core";
import { BiometricsService } from "@bitwarden/key-management";
import { BrowserApi } from "../../platform/browser/browser-api";
@Injectable()
export abstract class BrowserBiometricsService extends BiometricsService {
async supportsBiometric() {
const platformInfo = await BrowserApi.getPlatformInfo();
if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") {
return true;
}
return false;
}
abstract authenticateBiometric(): Promise<boolean>;
abstract isBiometricUnlockAvailable(): Promise<boolean>;
}

View File

@@ -1,34 +1,55 @@
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { BiometricsCommands, BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
import { BrowserApi } from "../../platform/browser/browser-api";
import { BrowserBiometricsService } from "./browser-biometrics.service";
export class ForegroundBrowserBiometricsService extends BiometricsService {
shouldAutopromptNow = true;
export class ForegroundBrowserBiometricsService extends BrowserBiometricsService {
async authenticateBiometric(): Promise<boolean> {
async authenticateWithBiometrics(): Promise<boolean> {
const response = await BrowserApi.sendMessageWithResponse<{
result: boolean;
error: string;
}>("biometricUnlock");
}>(BiometricsCommands.AuthenticateWithBiometrics);
if (!response.result) {
throw response.error;
}
return response.result;
}
async isBiometricUnlockAvailable(): Promise<boolean> {
async getBiometricsStatus(): Promise<BiometricsStatus> {
const response = await BrowserApi.sendMessageWithResponse<{
result: boolean;
result: BiometricsStatus;
error: string;
}>("biometricUnlockAvailable");
return response.result && response.result === true;
}>(BiometricsCommands.GetBiometricsStatus);
return response.result;
}
async biometricsNeedsSetup(): Promise<boolean> {
return false;
async unlockWithBiometricsForUser(userId: UserId): Promise<UserKey | null> {
const response = await BrowserApi.sendMessageWithResponse<{
result: string;
error: string;
}>(BiometricsCommands.UnlockWithBiometricsForUser, { userId });
if (!response.result) {
return null;
}
return SymmetricCryptoKey.fromString(response.result) as UserKey;
}
async biometricsSupportsAutoSetup(): Promise<boolean> {
return false;
async getBiometricsStatusForUser(id: UserId): Promise<BiometricsStatus> {
const response = await BrowserApi.sendMessageWithResponse<{
result: BiometricsStatus;
error: string;
}>(BiometricsCommands.GetBiometricsStatusForUser, { userId: id });
return response.result;
}
async biometricsSetup(): Promise<void> {}
async getShouldAutopromptNow(): Promise<boolean> {
return this.shouldAutopromptNow;
}
async setShouldAutopromptNow(value: boolean): Promise<void> {
this.shouldAutopromptNow = value;
}
}

View File

@@ -1,91 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state";
import { StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import {
KdfConfigService,
DefaultKeyService,
BiometricsService,
BiometricStateService,
} from "@bitwarden/key-management";
export class BrowserKeyService extends DefaultKeyService {
constructor(
pinService: PinServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService,
encryptService: EncryptService,
platformUtilService: PlatformUtilsService,
logService: LogService,
stateService: StateService,
accountService: AccountService,
stateProvider: StateProvider,
private biometricStateService: BiometricStateService,
private biometricsService: BiometricsService,
kdfConfigService: KdfConfigService,
) {
super(
pinService,
masterPasswordService,
keyGenerationService,
cryptoFunctionService,
encryptService,
platformUtilService,
logService,
stateService,
accountService,
stateProvider,
kdfConfigService,
);
}
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
if (keySuffix === KeySuffixOptions.Biometric) {
const biometricUnlockPromise =
userId == null
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
: this.biometricStateService.getBiometricUnlockEnabled(userId);
return await biometricUnlockPromise;
}
return super.hasUserKeyStored(keySuffix, userId);
}
/**
* Browser doesn't store biometric keys, so we retrieve them from the desktop and return
* if we successfully saved it into memory as the User Key
* @returns the `UserKey` if the user passes a biometrics prompt, otherwise return `null`.
*/
protected override async getKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: UserId,
): Promise<UserKey> {
if (keySuffix === KeySuffixOptions.Biometric) {
const biometricsResult = await this.biometricsService.authenticateBiometric();
if (!biometricsResult) {
return null;
}
const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId));
if (userKey) {
return userKey;
}
}
return await super.getKeyFromStorage(keySuffix, userId);
}
}

View File

@@ -9,8 +9,8 @@ import {
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { KeyService, BiometricsService } from "@bitwarden/key-management";
import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/key-management/angular";
import { KeyService, BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
import { UnlockOptions } from "@bitwarden/key-management/angular";
import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service";
@@ -121,8 +121,7 @@ describe("ExtensionLockComponentService", () => {
describe("getAvailableUnlockOptions$", () => {
interface MockInputs {
hasMasterPassword: boolean;
osSupportsBiometric: boolean;
biometricLockSet: boolean;
biometricsStatusForUser: BiometricsStatus;
hasBiometricEncryptedUserKeyStored: boolean;
platformSupportsSecureStorage: boolean;
pinDecryptionAvailable: boolean;
@@ -133,8 +132,7 @@ describe("ExtensionLockComponentService", () => {
// MP + PIN + Biometrics available
{
hasMasterPassword: true,
osSupportsBiometric: true,
biometricLockSet: true,
biometricsStatusForUser: BiometricsStatus.Available,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: true,
@@ -148,7 +146,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: true,
disableReason: null,
biometricsStatus: BiometricsStatus.Available,
},
},
],
@@ -156,8 +154,7 @@ describe("ExtensionLockComponentService", () => {
// PIN + Biometrics available
{
hasMasterPassword: false,
osSupportsBiometric: true,
biometricLockSet: true,
biometricsStatusForUser: BiometricsStatus.Available,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: true,
@@ -171,7 +168,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: true,
disableReason: null,
biometricsStatus: BiometricsStatus.Available,
},
},
],
@@ -179,8 +176,7 @@ describe("ExtensionLockComponentService", () => {
// Biometrics available: user key stored with no secure storage
{
hasMasterPassword: false,
osSupportsBiometric: true,
biometricLockSet: true,
biometricsStatusForUser: BiometricsStatus.Available,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: false,
pinDecryptionAvailable: false,
@@ -194,7 +190,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: true,
disableReason: null,
biometricsStatus: BiometricsStatus.Available,
},
},
],
@@ -202,8 +198,7 @@ describe("ExtensionLockComponentService", () => {
// Biometrics available: no user key stored with no secure storage
{
hasMasterPassword: false,
osSupportsBiometric: true,
biometricLockSet: true,
biometricsStatusForUser: BiometricsStatus.Available,
hasBiometricEncryptedUserKeyStored: false,
platformSupportsSecureStorage: false,
pinDecryptionAvailable: false,
@@ -217,7 +212,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: true,
disableReason: null,
biometricsStatus: BiometricsStatus.Available,
},
},
],
@@ -225,8 +220,7 @@ describe("ExtensionLockComponentService", () => {
// Biometrics not available: biometric lock not set
{
hasMasterPassword: false,
osSupportsBiometric: true,
biometricLockSet: false,
biometricsStatusForUser: BiometricsStatus.UnlockNeeded,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: false,
@@ -240,7 +234,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: false,
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
biometricsStatus: BiometricsStatus.UnlockNeeded,
},
},
],
@@ -248,8 +242,7 @@ describe("ExtensionLockComponentService", () => {
// Biometrics not available: user key not stored
{
hasMasterPassword: false,
osSupportsBiometric: true,
biometricLockSet: true,
biometricsStatusForUser: BiometricsStatus.NotEnabledInConnectedDesktopApp,
hasBiometricEncryptedUserKeyStored: false,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: false,
@@ -263,7 +256,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: false,
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
biometricsStatus: BiometricsStatus.NotEnabledInConnectedDesktopApp,
},
},
],
@@ -271,8 +264,7 @@ describe("ExtensionLockComponentService", () => {
// Biometrics not available: OS doesn't support
{
hasMasterPassword: false,
osSupportsBiometric: false,
biometricLockSet: true,
biometricsStatusForUser: BiometricsStatus.HardwareUnavailable,
hasBiometricEncryptedUserKeyStored: true,
platformSupportsSecureStorage: true,
pinDecryptionAvailable: false,
@@ -286,7 +278,7 @@ describe("ExtensionLockComponentService", () => {
},
biometrics: {
enabled: false,
disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem,
biometricsStatus: BiometricsStatus.HardwareUnavailable,
},
},
],
@@ -304,8 +296,12 @@ describe("ExtensionLockComponentService", () => {
);
// Biometrics
biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric);
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet);
biometricsService.getBiometricsStatusForUser.mockResolvedValue(
mockInputs.biometricsStatusForUser,
);
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(
mockInputs.hasBiometricEncryptedUserKeyStored,
);
keyService.hasUserKeyStored.mockResolvedValue(mockInputs.hasBiometricEncryptedUserKeyStored);
platformUtilsService.supportsSecureStorage.mockReturnValue(
mockInputs.platformSupportsSecureStorage,

View File

@@ -7,27 +7,17 @@ import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { UserId } from "@bitwarden/common/types/guid";
import { KeyService, BiometricsService } from "@bitwarden/key-management";
import {
LockComponentService,
BiometricsDisableReason,
UnlockOptions,
} from "@bitwarden/key-management/angular";
import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular";
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service";
export class ExtensionLockComponentService implements LockComponentService {
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
private readonly platformUtilsService = inject(PlatformUtilsService);
private readonly biometricsService = inject(BiometricsService);
private readonly pinService = inject(PinServiceAbstraction);
private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
private readonly keyService = inject(KeyService);
private readonly routerService = inject(BrowserRouterService);
getPreviousUrl(): string | null {
@@ -52,67 +42,28 @@ export class ExtensionLockComponentService implements LockComponentService {
return "unlockWithBiometrics";
}
private async isBiometricLockSet(userId: UserId): Promise<boolean> {
const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId);
const hasBiometricEncryptedUserKeyStored = await this.keyService.hasUserKeyStored(
KeySuffixOptions.Biometric,
userId,
);
const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage();
return (
biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage)
);
}
private getBiometricsDisabledReason(
osSupportsBiometric: boolean,
biometricLockSet: boolean,
): BiometricsDisableReason | null {
if (!osSupportsBiometric) {
return BiometricsDisableReason.NotSupportedOnOperatingSystem;
} else if (!biometricLockSet) {
return BiometricsDisableReason.EncryptedKeysUnavailable;
}
return null;
}
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> {
return combineLatest([
// Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to
defer(() => this.biometricsService.supportsBiometric()),
defer(() => this.isBiometricLockSet(userId)),
defer(async () => await this.biometricsService.getBiometricsStatusForUser(userId)),
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
defer(() => this.pinService.isPinDecryptionAvailable(userId)),
]).pipe(
map(
([
supportsBiometric,
isBiometricsLockSet,
userDecryptionOptions,
pinDecryptionAvailable,
]) => {
const disableReason = this.getBiometricsDisabledReason(
supportsBiometric,
isBiometricsLockSet,
);
const unlockOpts: UnlockOptions = {
masterPassword: {
enabled: userDecryptionOptions.hasMasterPassword,
},
pin: {
enabled: pinDecryptionAvailable,
},
biometrics: {
enabled: supportsBiometric && isBiometricsLockSet,
disableReason: disableReason,
},
};
return unlockOpts;
},
),
map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => {
const unlockOpts: UnlockOptions = {
masterPassword: {
enabled: userDecryptionOptions.hasMasterPassword,
},
pin: {
enabled: pinDecryptionAvailable,
},
biometrics: {
enabled: biometricsStatus === BiometricsStatus.Available,
biometricsStatus: biometricsStatus,
},
};
return unlockOpts;
}),
);
}
}

View File

@@ -111,8 +111,8 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
import {
KdfConfigService,
KeyService,
BiometricStateService,
BiometricsService,
DefaultKeyService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management/angular";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -126,7 +126,6 @@ import { AutofillService as AutofillServiceAbstraction } from "../../autofill/se
import AutofillService from "../../autofill/services/autofill.service";
import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service";
import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics";
import { BrowserKeyService } from "../../key-management/browser-key.service";
import { ExtensionLockComponentService } from "../../key-management/lock/services/extension-lock-component.service";
import { BrowserApi } from "../../platform/browser/browser-api";
import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator";
@@ -232,11 +231,9 @@ const safeProviders: SafeProvider[] = [
stateService: StateService,
accountService: AccountServiceAbstraction,
stateProvider: StateProvider,
biometricStateService: BiometricStateService,
biometricsService: BiometricsService,
kdfConfigService: KdfConfigService,
) => {
const keyService = new BrowserKeyService(
const keyService = new DefaultKeyService(
pinService,
masterPasswordService,
keyGenerationService,
@@ -247,8 +244,6 @@ const safeProviders: SafeProvider[] = [
stateService,
accountService,
stateProvider,
biometricStateService,
biometricsService,
kdfConfigService,
);
new ContainerService(keyService, encryptService).attachToGlobal(self);
@@ -265,8 +260,6 @@ const safeProviders: SafeProvider[] = [
StateService,
AccountServiceAbstraction,
StateProvider,
BiometricStateService,
BiometricsService,
KdfConfigService,
],
}),

View File

@@ -86,8 +86,203 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
context.completeRequest(returningItems: [response], completionHandler: nil)
}
return
case "biometricUnlock":
case "authenticateWithBiometrics":
let messageId = message?["messageId"] as? Int
let laContext = LAContext()
guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .userPresence], nil) else {
response.userInfo = [
SFExtensionMessageKey: [
"message": [
"command": "authenticateWithBiometrics",
"response": false,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"messageId": messageId,
],
],
]
break
}
laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "authenticate") { (success, error) in
if success {
response.userInfo = [ SFExtensionMessageKey: [
"message": [
"command": "authenticateWithBiometrics",
"response": true,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"messageId": messageId,
],
]]
} else {
response.userInfo = [ SFExtensionMessageKey: [
"message": [
"command": "authenticateWithBiometrics",
"response": false,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"messageId": messageId,
],
]]
}
context.completeRequest(returningItems: [response], completionHandler: nil)
}
return
case "getBiometricsStatus":
let messageId = message?["messageId"] as? Int
response.userInfo = [
SFExtensionMessageKey: [
"message": [
"command": "getBiometricsStatus",
"response": BiometricsStatus.Available.rawValue,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"messageId": messageId,
],
],
]
context.completeRequest(returningItems: [response], completionHandler: nil);
break
case "unlockWithBiometricsForUser":
let messageId = message?["messageId"] as? Int
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": false,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"messageId": messageId,
],
],
]
context.completeRequest(returningItems: [response], completionHandler: nil)
break
}
guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .userPresence], nil) else {
let messageId = message?["messageId"] as? Int
response.userInfo = [
SFExtensionMessageKey: [
"message": [
"command": "biometricUnlock",
"response": false,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"messageId": messageId,
],
],
]
context.completeRequest(returningItems: [response], completionHandler: nil)
break
}
laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "unlock your vault") { (success, error) in
if success {
guard let userId = message?["userId"] as? String else {
return
}
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": true,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"userKeyB64": result!.replacingOccurrences(of: "\"", with: ""),
"messageId": messageId,
],
]]
} else {
response.userInfo = [
SFExtensionMessageKey: [
"message": [
"command": "biometricUnlock",
"response": true,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"messageId": messageId,
],
],
]
}
}
context.completeRequest(returningItems: [response], completionHandler: nil)
}
return
case "getBiometricsStatusForUser":
let messageId = message?["messageId"] as? Int
let laContext = LAContext()
if !laContext.isBiometricsAvailable() {
response.userInfo = [
SFExtensionMessageKey: [
"message": [
"command": "getBiometricsStatusForUser",
"response": BiometricsStatus.HardwareUnavailable.rawValue,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"messageId": messageId,
],
],
]
context.completeRequest(returningItems: [response], completionHandler: nil)
break
}
guard let userId = message?["userId"] as? String else {
return
}
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 {
response.userInfo = [
SFExtensionMessageKey: [
"message": [
"command": "getBiometricsStatusForUser",
"response": BiometricsStatus.Available.rawValue,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"messageId": messageId,
],
],
]
} else {
response.userInfo = [
SFExtensionMessageKey: [
"message": [
"command": "getBiometricsStatusForUser",
"response": BiometricsStatus.NotEnabledInConnectedDesktopApp.rawValue,
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"messageId": messageId,
],
],
]
}
break
case "biometricUnlock":
var error: NSError?
let laContext = LAContext()
if(!laContext.isBiometricsAvailable()){
@@ -115,7 +310,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
]
break
}
laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "Bitwarden Safari Extension") { (success, error) in
laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "Biometric Unlock") { (success, error) in
if success {
guard let userId = message?["userId"] as? String else {
return
@@ -157,7 +352,6 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
context.completeRequest(returningItems: [response], completionHandler: nil)
}
return
case "biometricUnlockAvailable":
let laContext = LAContext()
@@ -228,3 +422,15 @@ class DownloadFileMessage: Decodable, Encodable {
class DownloadFileMessageBlobOptions: Decodable, Encodable {
var type: String?
}
enum BiometricsStatus : Int {
case Available = 0
case UnlockNeeded = 1
case HardwareUnavailable = 2
case AutoSetupNeeded = 3
case ManualSetupNeeded = 4
case PlatformUnsupported = 5
case DesktopDisconnected = 6
case NotEnabledLocally = 7
case NotEnabledInConnectedDesktopApp = 8
}