mirror of
https://github.com/bitwarden/browser
synced 2026-02-08 04:33:38 +00:00
Implement synced unlock
This commit is contained in:
@@ -2161,6 +2161,9 @@
|
||||
"tooManyInvalidPinEntryAttemptsLoggingOut": {
|
||||
"message": "Too many invalid PIN entry attempts. Logging out."
|
||||
},
|
||||
"syncUnlockWithDesktop": {
|
||||
"message": "Synchronize unlock state with desktop app."
|
||||
},
|
||||
"unlockWithBiometrics": {
|
||||
"message": "Unlock with biometrics"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { LockService } from "@bitwarden/auth/common";
|
||||
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 { SyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/abstractions/synced-unlock.service";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutService,
|
||||
@@ -70,6 +71,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private authService: AuthService,
|
||||
private lockService: LockService,
|
||||
private foregroundSyncedUnlockService: SyncedUnlockService,
|
||||
) {}
|
||||
|
||||
get accountLimit() {
|
||||
@@ -120,6 +122,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
|
||||
async lock(userId: string) {
|
||||
this.loading = true;
|
||||
await this.foregroundSyncedUnlockService.lock(userId as UserId);
|
||||
await this.vaultTimeoutService.lock(userId);
|
||||
// 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
|
||||
|
||||
@@ -11,7 +11,21 @@
|
||||
<h2 bitTypography="h6">{{ "unlockMethods" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<bit-card>
|
||||
<bit-form-control [disableMargin]="!((pinEnabled$ | async) || this.form.value.pin)">
|
||||
<bit-form-control [disableMargin]="this.form.value.syncUnlockWithDesktop">
|
||||
<input
|
||||
bitCheckbox
|
||||
id="syncUnlockWithDesktop"
|
||||
type="checkbox"
|
||||
formControlName="syncUnlockWithDesktop"
|
||||
/>
|
||||
<bit-label for="syncUnlockWithDesktop" class="tw-whitespace-normal">
|
||||
{{ "syncUnlockWithDesktop" | i18n }}
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
[disableMargin]="!((pinEnabled$ | async) || this.form.value.pin)"
|
||||
*ngIf="!this.form.value.syncUnlockWithDesktop"
|
||||
>
|
||||
<input bitCheckbox id="biometric" type="checkbox" formControlName="biometric" />
|
||||
<bit-label for="biometric" class="tw-whitespace-normal">{{
|
||||
"unlockWithBiometrics" | i18n
|
||||
@@ -23,7 +37,7 @@
|
||||
<bit-form-control
|
||||
class="tw-pl-5"
|
||||
[disableMargin]="!((pinEnabled$ | async) || this.form.value.pin)"
|
||||
*ngIf="this.form.value.biometric"
|
||||
*ngIf="this.form.value.biometric && !this.form.value.syncUnlockWithDesktop"
|
||||
>
|
||||
<input
|
||||
bitCheckbox
|
||||
@@ -37,7 +51,9 @@
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
[disableMargin]="!(this.form.value.pin && showMasterPasswordOnClientRestartOption)"
|
||||
*ngIf="(pinEnabled$ | async) || this.form.value.pin"
|
||||
*ngIf="
|
||||
((pinEnabled$ | async) || this.form.value.pin) && !this.form.value.syncUnlockWithDesktop
|
||||
"
|
||||
>
|
||||
<input bitCheckbox id="pin" type="checkbox" formControlName="pin" />
|
||||
<bit-label for="pin" class="tw-whitespace-normal">{{ "unlockWithPin" | i18n }}</bit-label>
|
||||
@@ -45,7 +61,11 @@
|
||||
<bit-form-control
|
||||
class="tw-pl-5"
|
||||
disableMargin
|
||||
*ngIf="this.form.value.pin && showMasterPasswordOnClientRestartOption"
|
||||
*ngIf="
|
||||
this.form.value.pin &&
|
||||
showMasterPasswordOnClientRestartOption &&
|
||||
!this.form.value.syncUnlockWithDesktop
|
||||
"
|
||||
>
|
||||
<input
|
||||
bitCheckbox
|
||||
@@ -70,10 +90,11 @@
|
||||
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||
[formControl]="form.controls.vaultTimeout"
|
||||
ngDefaultControl
|
||||
*ngIf="!this.form.value.syncUnlockWithDesktop"
|
||||
>
|
||||
</auth-vault-timeout-input>
|
||||
|
||||
<bit-form-field disableMargin>
|
||||
<bit-form-field disableMargin *ngIf="!this.form.value.syncUnlockWithDesktop">
|
||||
<bit-label for="vaultTimeoutAction">{{ "vaultTimeoutAction1" | i18n }}</bit-label>
|
||||
<bit-select id="vaultTimeoutAction" formControlName="vaultTimeoutAction">
|
||||
<bit-option
|
||||
@@ -89,9 +110,15 @@
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-hint *ngIf="hasVaultTimeoutPolicy" class="tw-mt-4">
|
||||
<bit-hint
|
||||
*ngIf="hasVaultTimeoutPolicy && !this.form.value.syncUnlockWithDesktop"
|
||||
class="tw-mt-4"
|
||||
>
|
||||
{{ "vaultTimeoutPolicyAffectingOptions" | i18n }}
|
||||
</bit-hint>
|
||||
<bit-hint *ngIf="this.form.value.syncUnlockWithDesktop">
|
||||
The desktop app's vault timeout settings will be used to lock the vault on this device.
|
||||
</bit-hint>
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
combineLatest,
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
@@ -30,6 +31,7 @@ import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { SyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/abstractions/synced-unlock.service";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
@@ -63,6 +65,7 @@ import {
|
||||
BiometricsService,
|
||||
BiometricStateService,
|
||||
BiometricsStatus,
|
||||
SyncedUnlockStateServiceAbstraction,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
|
||||
@@ -110,6 +113,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
biometricUnavailabilityReason: string;
|
||||
showChangeMasterPass = true;
|
||||
pinEnabled$: Observable<boolean> = of(true);
|
||||
isConnected = false;
|
||||
|
||||
form = this.formBuilder.group({
|
||||
vaultTimeout: [null as VaultTimeout | null],
|
||||
@@ -118,6 +122,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
pinLockWithMasterPassword: false,
|
||||
biometric: false,
|
||||
enableAutoBiometricsPrompt: true,
|
||||
syncUnlockWithDesktop: false,
|
||||
});
|
||||
|
||||
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
|
||||
@@ -142,6 +147,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
private biometricStateService: BiometricStateService,
|
||||
private toastService: ToastService,
|
||||
private biometricsService: BiometricsService,
|
||||
private syncedUnlockService: SyncedUnlockService,
|
||||
private syncedUnlockStateService: SyncedUnlockStateServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -219,11 +226,34 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
enableAutoBiometricsPrompt: await firstValueFrom(
|
||||
this.biometricStateService.promptAutomatically$,
|
||||
),
|
||||
syncUnlockWithDesktop: await firstValueFrom(
|
||||
this.syncedUnlockStateService.syncedUnlockEnabled$,
|
||||
),
|
||||
};
|
||||
this.form.patchValue(initialValues, { emitEvent: false });
|
||||
|
||||
this.form.controls.syncUnlockWithDesktop.valueChanges
|
||||
.pipe(
|
||||
concatMap(async (enabled) => {
|
||||
if (enabled) {
|
||||
const granted = await BrowserApi.requestPermission({
|
||||
permissions: ["nativeMessaging"],
|
||||
});
|
||||
if (!granted) {
|
||||
this.form.controls.syncUnlockWithDesktop.setValue(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.syncedUnlockStateService.setSyncedUnlockEnabled(enabled);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
timer(0, 1000)
|
||||
.pipe(
|
||||
filter(() => !this.form.controls.syncUnlockWithDesktop.value),
|
||||
switchMap(async () => {
|
||||
const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id);
|
||||
const biometricSettingAvailable = await this.biometricsService.canEnableBiometricUnlock();
|
||||
|
||||
@@ -84,6 +84,7 @@ import { KeyConnectorService } from "@bitwarden/common/key-management/key-connec
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
import { SyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/abstractions/synced-unlock.service";
|
||||
import {
|
||||
DefaultVaultTimeoutSettingsService,
|
||||
VaultTimeoutSettingsService,
|
||||
@@ -225,6 +226,7 @@ import {
|
||||
DefaultBiometricStateService,
|
||||
DefaultKdfConfigService,
|
||||
DefaultKeyService,
|
||||
DefaultSyncedUnlockStateService,
|
||||
KdfConfigService,
|
||||
KeyService as KeyServiceAbstraction,
|
||||
} from "@bitwarden/key-management";
|
||||
@@ -261,6 +263,7 @@ 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 { BackgroundSyncedUnlockService } from "../key-management/synced-unlock/background-synced-unlock.service";
|
||||
import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.service";
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
@@ -388,6 +391,7 @@ export default class MainBackground {
|
||||
vaultSettingsService: VaultSettingsServiceAbstraction;
|
||||
biometricStateService: BiometricStateService;
|
||||
biometricsService: BiometricsService;
|
||||
syncedUnlockService: SyncedUnlockService;
|
||||
stateEventRunnerService: StateEventRunnerService;
|
||||
ssoLoginService: SsoLoginServiceAbstraction;
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService;
|
||||
@@ -914,6 +918,8 @@ export default class MainBackground {
|
||||
|
||||
this.vaultSettingsService = new VaultSettingsService(this.stateProvider);
|
||||
|
||||
const syncedUnlockStateService = new DefaultSyncedUnlockStateService(this.stateProvider);
|
||||
|
||||
this.vaultTimeoutService = new VaultTimeoutService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
@@ -930,9 +936,20 @@ export default class MainBackground {
|
||||
this.taskSchedulerService,
|
||||
this.logService,
|
||||
this.biometricsService,
|
||||
syncedUnlockStateService,
|
||||
lockedCallback,
|
||||
logoutCallback,
|
||||
);
|
||||
|
||||
this.syncedUnlockService = new BackgroundSyncedUnlockService(
|
||||
runtimeNativeMessagingBackground,
|
||||
this.logService,
|
||||
this.keyService,
|
||||
this.accountService,
|
||||
this.authService,
|
||||
this.vaultTimeoutService,
|
||||
);
|
||||
|
||||
this.containerService = new ContainerService(this.keyService, this.encryptService);
|
||||
|
||||
this.sendStateProvider = new SendStateProvider(this.stateProvider);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs";
|
||||
import { BehaviorSubject, concatMap, firstValueFrom, timer } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
@@ -11,7 +11,7 @@ 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 { KeyService, BiometricStateService, BiometricsCommands } from "@bitwarden/key-management";
|
||||
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
|
||||
@@ -74,6 +74,7 @@ type SecureChannel = {
|
||||
export class NativeMessagingBackground {
|
||||
connected = false;
|
||||
private connecting: boolean = false;
|
||||
private connected$ = new BehaviorSubject<boolean>(false);
|
||||
private port?: browser.runtime.Port | chrome.runtime.Port;
|
||||
private appId?: string;
|
||||
|
||||
@@ -82,8 +83,6 @@ export class NativeMessagingBackground {
|
||||
private messageId = 0;
|
||||
private callbacks = new Map<number, Callback>();
|
||||
|
||||
isConnectedToOutdatedDesktopClient = true;
|
||||
|
||||
constructor(
|
||||
private keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
@@ -105,6 +104,22 @@ export class NativeMessagingBackground {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
timer(0, 5000)
|
||||
.pipe(
|
||||
concatMap(async () => {
|
||||
if (!this.connected) {
|
||||
if (await this.hasPermission()) {
|
||||
await this.connect();
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async hasPermission(): Promise<boolean> {
|
||||
return await BrowserApi.permissionsGranted(["nativeMessaging"]);
|
||||
}
|
||||
|
||||
async connect() {
|
||||
@@ -130,6 +145,7 @@ export class NativeMessagingBackground {
|
||||
);
|
||||
}
|
||||
this.connected = true;
|
||||
this.connected$.next(true);
|
||||
this.connecting = false;
|
||||
resolve();
|
||||
};
|
||||
@@ -137,7 +153,6 @@ export class NativeMessagingBackground {
|
||||
// Safari has a bundled native component which is always available, no need to
|
||||
// check if the desktop app is running.
|
||||
if (this.platformUtilsService.isSafari()) {
|
||||
this.isConnectedToOutdatedDesktopClient = false;
|
||||
connectedCallback();
|
||||
}
|
||||
|
||||
@@ -153,6 +168,7 @@ export class NativeMessagingBackground {
|
||||
reject(new Error("startDesktop"));
|
||||
}
|
||||
this.connected = false;
|
||||
this.connected$.next(false);
|
||||
port.disconnect();
|
||||
// reject all
|
||||
for (const callback of this.callbacks.values()) {
|
||||
@@ -188,15 +204,6 @@ export class NativeMessagingBackground {
|
||||
|
||||
this.secureChannel.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.secureChannel.setupResolve();
|
||||
break;
|
||||
}
|
||||
@@ -211,6 +218,7 @@ export class NativeMessagingBackground {
|
||||
|
||||
this.secureChannel = undefined;
|
||||
this.connected = false;
|
||||
this.connected$.next(false);
|
||||
|
||||
if (message.messageId != null) {
|
||||
if (this.callbacks.has(message.messageId)) {
|
||||
@@ -274,6 +282,7 @@ export class NativeMessagingBackground {
|
||||
|
||||
this.secureChannel = undefined;
|
||||
this.connected = false;
|
||||
this.connected$.next(false);
|
||||
|
||||
this.logService.error("NativeMessaging port disconnected because of error: " + error);
|
||||
|
||||
@@ -285,30 +294,6 @@ 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.3
|
||||
// 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 });
|
||||
});
|
||||
@@ -417,22 +402,6 @@ export class NativeMessagingBackground {
|
||||
|
||||
const messageId = message.messageId;
|
||||
|
||||
if (
|
||||
message.command == BiometricsCommands.Unlock ||
|
||||
message.command == BiometricsCommands.IsAvailable
|
||||
) {
|
||||
this.logService.info(
|
||||
`[Native Messaging IPC] Received legacy message of type ${message.command}`,
|
||||
);
|
||||
const messageId: number | undefined = this.callbacks.keys().next().value;
|
||||
if (messageId != null) {
|
||||
const resolver = this.callbacks.get(messageId);
|
||||
this.callbacks.delete(messageId);
|
||||
resolver!.resolver(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.callbacks.has(messageId)) {
|
||||
const callback = this.callbacks!.get(messageId)!;
|
||||
this.callbacks.delete(messageId);
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
import { BiometricsCommands } from "@bitwarden/key-management";
|
||||
import { BiometricsCommands, SyncedUnlockStateCommands } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
closeUnlockPopout,
|
||||
@@ -80,6 +80,11 @@ export default class RuntimeBackground {
|
||||
BiometricsCommands.UnlockWithBiometricsForUser,
|
||||
BiometricsCommands.GetBiometricsStatusForUser,
|
||||
BiometricsCommands.CanEnableBiometricUnlock,
|
||||
SyncedUnlockStateCommands.IsConnected,
|
||||
SyncedUnlockStateCommands.SendLockToDesktop,
|
||||
SyncedUnlockStateCommands.GetUserKeyFromDesktop,
|
||||
SyncedUnlockStateCommands.GetUserStatusFromDesktop,
|
||||
SyncedUnlockStateCommands.FocusDesktopApp,
|
||||
"getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag",
|
||||
"getUserPremiumStatus",
|
||||
];
|
||||
@@ -205,6 +210,21 @@ export default class RuntimeBackground {
|
||||
case BiometricsCommands.CanEnableBiometricUnlock: {
|
||||
return await this.main.biometricsService.canEnableBiometricUnlock();
|
||||
}
|
||||
case SyncedUnlockStateCommands.IsConnected: {
|
||||
return await this.main.syncedUnlockService.isConnected();
|
||||
}
|
||||
case SyncedUnlockStateCommands.SendLockToDesktop: {
|
||||
return await this.main.syncedUnlockService.lock(msg.userId);
|
||||
}
|
||||
case SyncedUnlockStateCommands.GetUserKeyFromDesktop: {
|
||||
return await this.main.syncedUnlockService.getUserKeyFromDesktop(msg.userId);
|
||||
}
|
||||
case SyncedUnlockStateCommands.GetUserStatusFromDesktop: {
|
||||
return await this.main.syncedUnlockService.getUserStatusFromDesktop(msg.userId);
|
||||
}
|
||||
case SyncedUnlockStateCommands.FocusDesktopApp: {
|
||||
return await this.main.syncedUnlockService.focusDesktopApp();
|
||||
}
|
||||
case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": {
|
||||
return await this.configService.getFeatureFlag(
|
||||
FeatureFlag.UseTreeWalkerApiForPageDetailsCollection,
|
||||
|
||||
@@ -35,17 +35,10 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
|
||||
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;
|
||||
}
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.AuthenticateWithBiometrics,
|
||||
});
|
||||
return response.response;
|
||||
} catch (e) {
|
||||
this.logService.info("Biometric authentication failed", e);
|
||||
return false;
|
||||
@@ -60,23 +53,12 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
|
||||
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,
|
||||
});
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.GetBiometricsStatus,
|
||||
});
|
||||
|
||||
if (response.response) {
|
||||
return response.response;
|
||||
}
|
||||
if (response.response) {
|
||||
return response.response;
|
||||
}
|
||||
return BiometricsStatus.Available;
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
@@ -90,43 +72,23 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
|
||||
try {
|
||||
await this.ensureConnected();
|
||||
|
||||
// todo remove after 2025.3
|
||||
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.Unlock,
|
||||
});
|
||||
if (response.response == "unlocked") {
|
||||
const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64);
|
||||
const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey;
|
||||
if (await this.keyService.validateUserKey(userKey, userId)) {
|
||||
await this.biometricStateService.setBiometricUnlockEnabled(true);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
// to update badge and other things
|
||||
this.messagingService.send("switchAccount", { userId });
|
||||
return userKey;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.UnlockWithBiometricsForUser,
|
||||
userId: userId,
|
||||
});
|
||||
if (response.response) {
|
||||
// In case the requesting foreground context dies (popup), the userkey should still be set, so the user is unlocked / the setting should be enabled
|
||||
const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64);
|
||||
const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey;
|
||||
if (await this.keyService.validateUserKey(userKey, userId)) {
|
||||
await this.biometricStateService.setBiometricUnlockEnabled(true);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
// to update badge and other things
|
||||
this.messagingService.send("switchAccount", { userId });
|
||||
return userKey;
|
||||
}
|
||||
} else {
|
||||
const response = await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.UnlockWithBiometricsForUser,
|
||||
userId: userId,
|
||||
});
|
||||
if (response.response) {
|
||||
// In case the requesting foreground context dies (popup), the userkey should still be set, so the user is unlocked / the setting should be enabled
|
||||
const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64);
|
||||
const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey;
|
||||
if (await this.keyService.validateUserKey(userKey, userId)) {
|
||||
await this.biometricStateService.setBiometricUnlockEnabled(true);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
// to update badge and other things
|
||||
this.messagingService.send("switchAccount", { userId });
|
||||
return userKey;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.info("Biometric unlock for user failed", e);
|
||||
@@ -140,10 +102,6 @@ export class BackgroundBrowserBiometricsService extends BiometricsService {
|
||||
try {
|
||||
await this.ensureConnected();
|
||||
|
||||
if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) {
|
||||
return await this.getBiometricsStatus();
|
||||
}
|
||||
|
||||
return (
|
||||
await this.nativeMessagingBackground().callCommand({
|
||||
command: BiometricsCommands.GetBiometricsStatusForUser,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { concatMap, firstValueFrom, 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 { SyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/abstractions/synced-unlock.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
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 { KeyService, SyncedUnlockStateCommands } from "@bitwarden/key-management";
|
||||
|
||||
import { NativeMessagingBackground } from "../../background/nativeMessaging.background";
|
||||
|
||||
@Injectable()
|
||||
export class BackgroundSyncedUnlockService extends SyncedUnlockService {
|
||||
constructor(
|
||||
private nativeMessagingBackground: () => NativeMessagingBackground,
|
||||
private logService: LogService,
|
||||
private keyService: KeyService,
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
) {
|
||||
super();
|
||||
timer(0, 1000)
|
||||
.pipe(
|
||||
concatMap(async () => {
|
||||
if (this.nativeMessagingBackground().connected) {
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (activeAccount) {
|
||||
const desktopAccountStatus = await this.getUserStatusFromDesktop(activeAccount.id);
|
||||
const localAccountStatus = await firstValueFrom(
|
||||
this.authService.authStatusFor$(activeAccount.id),
|
||||
);
|
||||
if (
|
||||
desktopAccountStatus === AuthenticationStatus.Locked &&
|
||||
localAccountStatus === AuthenticationStatus.Unlocked
|
||||
) {
|
||||
await this.vaultTimeoutService.lock(activeAccount.id);
|
||||
}
|
||||
if (
|
||||
desktopAccountStatus === AuthenticationStatus.Unlocked &&
|
||||
localAccountStatus === AuthenticationStatus.Locked
|
||||
) {
|
||||
const userKey = await this.getUserKeyFromDesktop(activeAccount.id);
|
||||
if (userKey) {
|
||||
await this.keyService.setUserKey(userKey, activeAccount.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async isConnected(): Promise<boolean> {
|
||||
this.logService.info("abc");
|
||||
const a = this.nativeMessagingBackground().connected;
|
||||
this.logService.info("aaaaa", a);
|
||||
return a;
|
||||
}
|
||||
|
||||
async lock(userId: UserId): Promise<void> {
|
||||
await this.nativeMessagingBackground().callCommand({
|
||||
command: SyncedUnlockStateCommands.SendLockToDesktop,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserKeyFromDesktop(userId: UserId): Promise<UserKey | null> {
|
||||
const res = await this.nativeMessagingBackground().callCommand({
|
||||
command: SyncedUnlockStateCommands.GetUserKeyFromDesktop,
|
||||
userId,
|
||||
});
|
||||
if (res == null) {
|
||||
return null;
|
||||
} else {
|
||||
return SymmetricCryptoKey.fromString(res.response) as UserKey;
|
||||
}
|
||||
}
|
||||
|
||||
async getUserStatusFromDesktop(userId: UserId): Promise<AuthenticationStatus> {
|
||||
const res = await this.nativeMessagingBackground().callCommand({
|
||||
command: SyncedUnlockStateCommands.GetUserStatusFromDesktop,
|
||||
userId,
|
||||
});
|
||||
return res.response;
|
||||
}
|
||||
|
||||
async focusDesktopApp(): Promise<void> {
|
||||
await this.nativeMessagingBackground().callCommand({
|
||||
command: SyncedUnlockStateCommands.FocusDesktopApp,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { SyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/abstractions/synced-unlock.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { SyncedUnlockStateCommands } from "@bitwarden/key-management";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
|
||||
@Injectable()
|
||||
export class ForegroundSyncedUnlockService extends SyncedUnlockService {
|
||||
constructor(private logService: LogService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async isConnected(): Promise<boolean> {
|
||||
const response = await BrowserApi.sendMessageWithResponse<{
|
||||
result: boolean;
|
||||
error: string;
|
||||
}>(SyncedUnlockStateCommands.IsConnected);
|
||||
if (response.result == null) {
|
||||
throw response.error;
|
||||
}
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async lock(userId: UserId): Promise<void> {
|
||||
try {
|
||||
await BrowserApi.sendMessageWithResponse<{
|
||||
result: boolean;
|
||||
error: string;
|
||||
}>(SyncedUnlockStateCommands.SendLockToDesktop, { userId });
|
||||
} catch (e) {
|
||||
this.logService.error("Failed to send lock to desktop", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async getUserStatusFromDesktop(userId: UserId): Promise<AuthenticationStatus> {
|
||||
const response = await BrowserApi.sendMessageWithResponse<{
|
||||
result: AuthenticationStatus;
|
||||
error: string;
|
||||
}>(SyncedUnlockStateCommands.GetUserStatusFromDesktop, { userId });
|
||||
if (!response.result) {
|
||||
throw response.error;
|
||||
}
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async getUserKeyFromDesktop(userId: UserId): Promise<UserKey | null> {
|
||||
const response = await BrowserApi.sendMessageWithResponse<{
|
||||
result: UserKey;
|
||||
error: string;
|
||||
}>(SyncedUnlockStateCommands.GetUserKeyFromDesktop, { userId });
|
||||
if (!response.result) {
|
||||
return null;
|
||||
}
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async focusDesktopApp(): Promise<void> {
|
||||
const response = await BrowserApi.sendMessageWithResponse<{
|
||||
result: boolean;
|
||||
error: string;
|
||||
}>(SyncedUnlockStateCommands.FocusDesktopApp);
|
||||
if (!response.result) {
|
||||
throw response.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { SyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/abstractions/synced-unlock.service";
|
||||
import {
|
||||
VaultTimeoutService,
|
||||
VaultTimeoutStringType,
|
||||
@@ -151,6 +152,7 @@ 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 { ExtensionLockComponentService } from "../../key-management/lock/services/extension-lock-component.service";
|
||||
import { ForegroundSyncedUnlockService } from "../../key-management/synced-unlock/foreground-synced-unlock.service";
|
||||
import { ForegroundVaultTimeoutService } from "../../key-management/vault-timeout/foreground-vault-timeout.service";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator";
|
||||
@@ -321,6 +323,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: ForegroundBrowserBiometricsService,
|
||||
deps: [PlatformUtilsService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SyncedUnlockService,
|
||||
useClass: ForegroundSyncedUnlockService,
|
||||
deps: [LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SyncService,
|
||||
useClass: ForegroundSyncService,
|
||||
|
||||
@@ -167,6 +167,7 @@ import {
|
||||
DefaultKeyService as KeyService,
|
||||
BiometricStateService,
|
||||
DefaultBiometricStateService,
|
||||
DefaultSyncedUnlockStateService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service";
|
||||
import {
|
||||
@@ -733,6 +734,7 @@ export class ServiceContainer {
|
||||
);
|
||||
|
||||
const biometricService = new CliBiometricsService();
|
||||
const syncedUnlockStateService = new DefaultSyncedUnlockStateService(this.stateProvider);
|
||||
|
||||
this.vaultTimeoutService = new DefaultVaultTimeoutService(
|
||||
this.accountService,
|
||||
@@ -750,6 +752,7 @@ export class ServiceContainer {
|
||||
this.taskSchedulerService,
|
||||
this.logService,
|
||||
biometricService,
|
||||
syncedUnlockStateService,
|
||||
lockedCallback,
|
||||
undefined,
|
||||
);
|
||||
|
||||
16
apps/desktop/desktop_native/Cargo.lock
generated
16
apps/desktop/desktop_native/Cargo.lock
generated
@@ -889,7 +889,7 @@ dependencies = [
|
||||
"ssh-encoding",
|
||||
"ssh-key",
|
||||
"sysinfo",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
@@ -931,7 +931,7 @@ dependencies = [
|
||||
"cc",
|
||||
"core-foundation",
|
||||
"glob",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -2480,7 +2480,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"libredox",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2971,11 +2971,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.11"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.11",
|
||||
"thiserror-impl 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2991,9 +2991,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.11"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -56,6 +56,8 @@ import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitw
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
import { SyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/abstractions/synced-unlock.service";
|
||||
import { NoopSyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/noop-synced-unlock.service";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
@@ -160,6 +162,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: RendererBiometricsService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SyncedUnlockService,
|
||||
useClass: NoopSyncedUnlockService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider(NativeMessagingService),
|
||||
safeProvider(BiometricMessageHandlerService),
|
||||
safeProvider(SearchBarService),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { dialog, shell } from "electron";
|
||||
import log from "electron-log";
|
||||
import { autoUpdater, UpdateDownloadedEvent, VerifyUpdateSupport } from "electron-updater";
|
||||
import { autoUpdater, UpdateDownloadedEvent } from "electron-updater";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@@ -16,7 +16,7 @@ export class UpdaterMain {
|
||||
private doingUpdateCheckWithFeedback = false;
|
||||
private canUpdate = false;
|
||||
private updateDownloaded: UpdateDownloadedEvent = null;
|
||||
private originalRolloutFunction: VerifyUpdateSupport = null;
|
||||
//private originalRolloutFunction: VerifyUpdateSupport = null;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
@@ -24,7 +24,7 @@ export class UpdaterMain {
|
||||
) {
|
||||
autoUpdater.logger = log;
|
||||
|
||||
this.originalRolloutFunction = autoUpdater.isUserWithinRollout;
|
||||
//this.originalRolloutFunction = autoUpdater.isUserWithinRollout;
|
||||
|
||||
const linuxCanUpdate = process.platform === "linux" && isAppImage();
|
||||
const windowsCanUpdate =
|
||||
@@ -130,7 +130,7 @@ export class UpdaterMain {
|
||||
|
||||
// If the user has explicitly checked for updates, we want to bypass
|
||||
// the current staging rollout percentage
|
||||
autoUpdater.isUserWithinRollout = (info) => true;
|
||||
//autoUpdater.isUserWithinRollout = (info) => true;
|
||||
}
|
||||
|
||||
await autoUpdater.checkForUpdates();
|
||||
@@ -139,7 +139,7 @@ export class UpdaterMain {
|
||||
private reset() {
|
||||
autoUpdater.autoDownload = true;
|
||||
// Reset the rollout check to the default behavior
|
||||
autoUpdater.isUserWithinRollout = this.originalRolloutFunction;
|
||||
//autoUpdater.isUserWithinRollout = this.originalRolloutFunction;
|
||||
this.doingUpdateCheck = false;
|
||||
this.updateDownloaded = null;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ describe("BiometricMessageHandlerService", () => {
|
||||
authService,
|
||||
ngZone,
|
||||
i18nService,
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -165,6 +166,7 @@ describe("BiometricMessageHandlerService", () => {
|
||||
authService,
|
||||
ngZone,
|
||||
i18nService,
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Injectable, NgZone } from "@angular/core";
|
||||
import { combineLatest, concatMap, firstValueFrom, map } from "rxjs";
|
||||
import { combineLatest, concatMap, firstValueFrom } 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 { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
BiometricsService,
|
||||
BiometricsStatus,
|
||||
KeyService,
|
||||
SyncedUnlockStateCommands,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { BrowserSyncVerificationDialogComponent } from "../app/components/browser-sync-verification-dialog.component";
|
||||
@@ -89,6 +91,7 @@ export class BiometricMessageHandlerService {
|
||||
private authService: AuthService,
|
||||
private ngZone: NgZone,
|
||||
private i18nService: I18nService,
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
) {
|
||||
combineLatest([
|
||||
this.desktopSettingService.browserIntegrationEnabled$,
|
||||
@@ -255,102 +258,61 @@ export class BiometricMessageHandlerService {
|
||||
appId,
|
||||
);
|
||||
}
|
||||
// TODO: legacy, remove after 2025.3
|
||||
case BiometricsCommands.IsAvailable: {
|
||||
const available =
|
||||
(await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available;
|
||||
return this.send(
|
||||
case SyncedUnlockStateCommands.SendLockToDesktop: {
|
||||
const userId = message.userId as UserId;
|
||||
await this.send(
|
||||
{
|
||||
command: BiometricsCommands.IsAvailable,
|
||||
response: available ? "available" : "not available",
|
||||
command: SyncedUnlockStateCommands.SendLockToDesktop,
|
||||
messageId,
|
||||
response: true,
|
||||
},
|
||||
appId,
|
||||
);
|
||||
if (userId != null) {
|
||||
await this.vaultTimeoutService.lock(userId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// TODO: legacy, remove after 2025.3
|
||||
case BiometricsCommands.Unlock: {
|
||||
if (
|
||||
await firstValueFrom(this.desktopSettingService.browserIntegrationFingerprintEnabled$)
|
||||
) {
|
||||
await this.send({ command: "biometricUnlock", response: "not available" }, appId);
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: this.i18nService.t("updateBrowserOrDisableFingerprintDialogTitle"),
|
||||
content: this.i18nService.t("updateBrowserOrDisableFingerprintDialogMessage"),
|
||||
type: "warning",
|
||||
});
|
||||
return;
|
||||
case SyncedUnlockStateCommands.GetUserKeyFromDesktop: {
|
||||
if (!(await this.validateFingerprint(appId))) {
|
||||
this.logService.info("[Native Messaging IPC] Fingerprint validation failed.");
|
||||
}
|
||||
|
||||
const isTemporarilyDisabled =
|
||||
(await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) &&
|
||||
!((await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available);
|
||||
if (isTemporarilyDisabled) {
|
||||
return this.send({ command: "biometricUnlock", response: "not available" }, appId);
|
||||
}
|
||||
|
||||
if (!((await this.biometricsService.getBiometricsStatus()) == BiometricsStatus.Available)) {
|
||||
return this.send({ command: "biometricUnlock", response: "not supported" }, appId);
|
||||
}
|
||||
|
||||
const userId =
|
||||
(message.userId as UserId) ??
|
||||
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
||||
|
||||
if (userId == null) {
|
||||
return this.send({ command: "biometricUnlock", response: "not unlocked" }, appId);
|
||||
}
|
||||
|
||||
const biometricUnlock =
|
||||
message.userId == null
|
||||
? await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
||||
: await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId);
|
||||
if (!biometricUnlock) {
|
||||
await this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
|
||||
|
||||
return this.ngZone.run(() =>
|
||||
this.dialogService.openSimpleDialog({
|
||||
type: "warning",
|
||||
title: { key: "biometricsNotEnabledTitle" },
|
||||
content: { key: "biometricsNotEnabledDesc" },
|
||||
cancelButtonText: null,
|
||||
acceptButtonText: { key: "cancel" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const userKey = await this.biometricsService.unlockWithBiometricsForUser(userId);
|
||||
|
||||
if (userKey != null) {
|
||||
await this.send(
|
||||
const userId = message.userId as UserId;
|
||||
if (userId != null) {
|
||||
const key = await this.keyService.getUserKey(userId);
|
||||
if (key != null) {
|
||||
return await this.send(
|
||||
{
|
||||
command: "biometricUnlock",
|
||||
response: "unlocked",
|
||||
userKeyB64: userKey.keyB64,
|
||||
command: SyncedUnlockStateCommands.GetUserKeyFromDesktop,
|
||||
messageId,
|
||||
response: key.keyB64,
|
||||
},
|
||||
appId,
|
||||
);
|
||||
|
||||
const currentlyActiveAccountId = (
|
||||
await firstValueFrom(this.accountService.activeAccount$)
|
||||
)?.id;
|
||||
const isCurrentlyActiveAccountUnlocked =
|
||||
(await this.authService.getAuthStatus(userId)) == AuthenticationStatus.Unlocked;
|
||||
|
||||
// prevent proc reloading an active account, when it is the same as the browser
|
||||
if (currentlyActiveAccountId != message.userId || !isCurrentlyActiveAccountUnlocked) {
|
||||
ipc.platform.reloadProcess();
|
||||
}
|
||||
} else {
|
||||
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||
}
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncedUnlockStateCommands.GetUserStatusFromDesktop: {
|
||||
const userId = message.userId as UserId;
|
||||
if (userId != null) {
|
||||
const status = await firstValueFrom(this.authService.authStatusFor$(userId));
|
||||
return await this.send(
|
||||
{
|
||||
command: SyncedUnlockStateCommands.GetUserStatusFromDesktop,
|
||||
messageId,
|
||||
response: status,
|
||||
},
|
||||
appId,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SyncedUnlockStateCommands.FocusDesktopApp: {
|
||||
this.messagingService.send("setFocus");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.logService.error("NativeMessage, got unknown command: " + message.command);
|
||||
break;
|
||||
|
||||
@@ -56,6 +56,8 @@ import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-managemen
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { SyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/abstractions/synced-unlock.service";
|
||||
import { NoopSyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/noop-synced-unlock.service";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutStringType,
|
||||
@@ -103,6 +105,8 @@ import {
|
||||
KdfConfigService,
|
||||
KeyService as KeyServiceAbstraction,
|
||||
BiometricsService,
|
||||
SyncedUnlockStateServiceAbstraction,
|
||||
DefaultSyncedUnlockStateService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { LockComponentService } from "@bitwarden/key-management-ui";
|
||||
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
@@ -231,6 +235,16 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebBiometricsService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SyncedUnlockStateServiceAbstraction,
|
||||
useClass: DefaultSyncedUnlockStateService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SyncedUnlockService,
|
||||
useClass: NoopSyncedUnlockService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ThemeStateService,
|
||||
useFactory: (globalStateProvider: GlobalStateProvider) =>
|
||||
|
||||
@@ -306,10 +306,12 @@ import {
|
||||
DefaultBiometricStateService,
|
||||
DefaultKdfConfigService,
|
||||
DefaultKeyService,
|
||||
DefaultSyncedUnlockStateService,
|
||||
DefaultUserAsymmetricKeysRegenerationApiService,
|
||||
DefaultUserAsymmetricKeysRegenerationService,
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
SyncedUnlockStateServiceAbstraction,
|
||||
UserAsymmetricKeysRegenerationApiService,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
} from "@bitwarden/key-management";
|
||||
@@ -822,6 +824,7 @@ const safeProviders: SafeProvider[] = [
|
||||
TaskSchedulerService,
|
||||
LogService,
|
||||
BiometricsService,
|
||||
SyncedUnlockStateServiceAbstraction,
|
||||
LOCKED_CALLBACK,
|
||||
LOGOUT_CALLBACK,
|
||||
],
|
||||
@@ -1295,6 +1298,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultBiometricStateService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SyncedUnlockStateServiceAbstraction,
|
||||
useClass: DefaultSyncedUnlockStateService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VaultSettingsServiceAbstraction,
|
||||
useClass: VaultSettingsService,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { UserKey } from "../../../types/key";
|
||||
|
||||
export abstract class SyncedUnlockService {
|
||||
abstract isConnected(): Promise<boolean>;
|
||||
abstract lock(userId: UserId): Promise<void>;
|
||||
abstract getUserStatusFromDesktop(userId: UserId): Promise<AuthenticationStatus>;
|
||||
abstract getUserKeyFromDesktop(userId: UserId): Promise<UserKey | null>;
|
||||
abstract focusDesktopApp(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { SyncedUnlockService } from "./abstractions/synced-unlock.service";
|
||||
|
||||
export class NoopSyncedUnlockService extends SyncedUnlockService {
|
||||
isConnected(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
lock(userId: UserId): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getUserStatusFromDesktop(userId: UserId): Promise<AuthenticationStatus> {
|
||||
return Promise.resolve(AuthenticationStatus.LoggedOut);
|
||||
}
|
||||
|
||||
getUserKeyFromDesktop(userId: UserId): Promise<UserKey | null> {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
focusDesktopApp(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import { combineLatest, concatMap, filter, firstValueFrom, map, timeout } from "
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { BiometricsService, SyncedUnlockStateServiceAbstraction } from "@bitwarden/key-management";
|
||||
|
||||
import { SearchService } from "../../../abstractions/search.service";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
@@ -43,6 +44,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
private taskSchedulerService: TaskSchedulerService,
|
||||
protected logService: LogService,
|
||||
private biometricService: BiometricsService,
|
||||
private syncedUnlockService: SyncedUnlockStateServiceAbstraction,
|
||||
private lockedCallback: (userId?: string) => Promise<void> = null,
|
||||
private loggedOutCallback: (
|
||||
logoutReason: LogoutReason,
|
||||
@@ -75,6 +77,13 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
}
|
||||
|
||||
async checkVaultTimeout(): Promise<void> {
|
||||
if (
|
||||
(await firstValueFrom(this.syncedUnlockService.syncedUnlockEnabled$)) &&
|
||||
this.platformUtilsService.getClientType() === ClientType.Browser
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get whether or not the view is open a single time so it can be compared for each user
|
||||
const isViewOpen = await this.platformUtilsService.isViewOpen();
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk",
|
||||
web: "disk-local",
|
||||
});
|
||||
export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk");
|
||||
export const SYNCED_UNLOCK_SETTINGS_DISK = new StateDefinition("syncedUnlock", "disk");
|
||||
export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
|
||||
export const CONFIG_DISK = new StateDefinition("config", "disk", {
|
||||
web: "disk-local",
|
||||
|
||||
@@ -5,175 +5,45 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="unlockOptions && !loading; else spinner">
|
||||
<!-- Biometrics Unlock -->
|
||||
<ng-container *ngIf="activeUnlockOption === UnlockOption.Biometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
class="tw-mb-3"
|
||||
[disabled]="unlockingViaBiometrics || !biometricsAvailable"
|
||||
[loading]="unlockingViaBiometrics"
|
||||
block
|
||||
(click)="unlockViaBiometrics()"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<p class="tw-text-center tw-mb-0">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Pin"
|
||||
>
|
||||
{{ "unlockWithPin" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.masterPassword.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.MasterPassword"
|
||||
>
|
||||
{{ "unlockWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- PIN Unlock -->
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled && activeUnlockOption === UnlockOption.Pin">
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "pin" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="pin"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="pin"
|
||||
class="tw-font-mono"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
||||
<ng-container *ngIf="!showLocalUnlockOptions">
|
||||
<form [bitSubmit]="submit" [formGroup]="desktopUnlockFormGroup">
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<bit-hint class="tw-text-center"
|
||||
>This user's account is synchronized with the desktop app.</bit-hint
|
||||
>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="showBiometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!biometricsAvailable"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.masterPassword.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.MasterPassword"
|
||||
>
|
||||
{{ "unlockWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
Continue in desktop app
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showLocalUnlockOptions">
|
||||
<bit-hint class="tw-text-center" *ngIf="unlockViaDesktop">
|
||||
This account has unlock synchronization enabled, but the desktop app is not running.
|
||||
</bit-hint>
|
||||
|
||||
<!-- MP Unlock -->
|
||||
<ng-container
|
||||
*ngIf="
|
||||
unlockOptions.masterPassword.enabled && activeUnlockOption === UnlockOption.MasterPassword
|
||||
"
|
||||
>
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="masterPassword"
|
||||
class="tw-font-mono"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
|
||||
<!-- [attr.aria-pressed]="showPassword" -->
|
||||
</bit-form-field>
|
||||
<!-- Biometrics Unlock -->
|
||||
<ng-container *ngIf="activeUnlockOption === UnlockOption.Biometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
class="tw-mb-3"
|
||||
[disabled]="unlockingViaBiometrics || !biometricsAvailable"
|
||||
[loading]="unlockingViaBiometrics"
|
||||
block
|
||||
(click)="unlockViaBiometrics()"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="showBiometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!biometricsAvailable"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
<p class="tw-text-center tw-mb-0">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Pin"
|
||||
@@ -182,10 +52,158 @@
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
<ng-container *ngIf="unlockOptions.masterPassword.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.MasterPassword"
|
||||
>
|
||||
{{ "unlockWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
<!-- PIN Unlock -->
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled && activeUnlockOption === UnlockOption.Pin">
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "pin" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="pin"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="pin"
|
||||
class="tw-font-mono"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="showBiometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!biometricsAvailable"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.masterPassword.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.MasterPassword"
|
||||
>
|
||||
{{ "unlockWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
<!-- MP Unlock -->
|
||||
<ng-container
|
||||
*ngIf="
|
||||
unlockOptions.masterPassword.enabled && activeUnlockOption === UnlockOption.MasterPassword
|
||||
"
|
||||
>
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="masterPassword"
|
||||
class="tw-font-mono"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
|
||||
<!-- [attr.aria-pressed]="showPassword" -->
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="showBiometrics">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[disabled]="!biometricsAvailable"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Pin"
|
||||
>
|
||||
{{ "unlockWithPin" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import { ClientType, DeviceType } from "@bitwarden/common/enums";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { SyncedUnlockService } from "@bitwarden/common/key-management/synced-unlock/abstractions/synced-unlock.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
BiometricsService,
|
||||
BiometricsStatus,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
SyncedUnlockStateServiceAbstraction,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
@@ -134,6 +136,11 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
|
||||
unlockingViaBiometrics = false;
|
||||
|
||||
unlockViaDesktop = false;
|
||||
isDesktopOpen = false;
|
||||
showLocalUnlockOptions = false;
|
||||
desktopUnlockFormGroup: FormGroup = new FormGroup({});
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private pinService: PinServiceAbstraction,
|
||||
@@ -157,6 +164,8 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
|
||||
|
||||
private biometricService: BiometricsService,
|
||||
private syncedUnlockStateService: SyncedUnlockStateServiceAbstraction,
|
||||
private syncedUnlockService: SyncedUnlockService,
|
||||
|
||||
private lockComponentService: LockComponentService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
@@ -191,6 +200,35 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
this.unlockOptions = await firstValueFrom(
|
||||
this.lockComponentService.getAvailableUnlockOptions$(this.activeAccount.id),
|
||||
);
|
||||
const userKey = await this.keyService.getUserKey(this.activeAccount.id);
|
||||
if (userKey != null) {
|
||||
await this.doContinue(false);
|
||||
}
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
interval(500)
|
||||
.pipe(
|
||||
switchMap(async () => {
|
||||
try {
|
||||
this.isDesktopOpen = await this.syncedUnlockService.isConnected();
|
||||
this.showLocalUnlockOptions = !(this.isDesktopOpen && this.unlockViaDesktop);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
this.syncedUnlockStateService.syncedUnlockEnabled$
|
||||
.pipe(
|
||||
tap((enabled) => {
|
||||
if (enabled) {
|
||||
this.unlockViaDesktop = true;
|
||||
} else {
|
||||
this.unlockViaDesktop = false;
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
@@ -338,11 +376,18 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
// Note: this submit method is only used for unlock methods that require a form and user input.
|
||||
// For biometrics unlock, the method is called directly.
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.activeUnlockOption === UnlockOption.Pin) {
|
||||
return await this.unlockViaPin();
|
||||
}
|
||||
if (
|
||||
this.platformUtilsService.getClientType() === ClientType.Browser &&
|
||||
!this.showLocalUnlockOptions
|
||||
) {
|
||||
await this.syncedUnlockService.focusDesktopApp();
|
||||
} else {
|
||||
if (this.activeUnlockOption === UnlockOption.Pin) {
|
||||
return await this.unlockViaPin();
|
||||
}
|
||||
|
||||
await this.unlockViaMasterPassword();
|
||||
await this.unlockViaMasterPassword();
|
||||
}
|
||||
};
|
||||
|
||||
async logOut() {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum SyncedUnlockStateCommands {
|
||||
IsConnected = "isConnected",
|
||||
SendLockToDesktop = "sendLockToDesktop",
|
||||
GetUserKeyFromDesktop = "getUserKeyFromDesktop",
|
||||
GetUserStatusFromDesktop = "getUserStatusFromDesktop",
|
||||
FocusDesktopApp = "focusDesktopApp",
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Observable, map } from "rxjs";
|
||||
|
||||
import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { SYNCED_UNLOCK_ENABLED } from "./synced-unlock.state";
|
||||
|
||||
export abstract class SyncedUnlockStateServiceAbstraction {
|
||||
/**
|
||||
* syncedUnlockEnabled$ is an observable that emits the current state of the synced unlock feature.
|
||||
*/
|
||||
abstract syncedUnlockEnabled$: Observable<boolean>;
|
||||
/**
|
||||
* Updates whether the unlock state should be synced with the desktop client.
|
||||
* @param enabled the value to save
|
||||
*/
|
||||
abstract setSyncedUnlockEnabled(enabled: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultSyncedUnlockStateService implements SyncedUnlockStateServiceAbstraction {
|
||||
private syncedUnlockEnabledState: ActiveUserState<boolean>;
|
||||
syncedUnlockEnabled$: Observable<boolean>;
|
||||
|
||||
constructor(private stateProvider: StateProvider) {
|
||||
this.syncedUnlockEnabledState = this.stateProvider.getActive(SYNCED_UNLOCK_ENABLED);
|
||||
this.syncedUnlockEnabled$ = this.syncedUnlockEnabledState.state$.pipe(map(Boolean));
|
||||
}
|
||||
|
||||
async setSyncedUnlockEnabled(enabled: boolean): Promise<void> {
|
||||
await this.syncedUnlockEnabledState.update(() => enabled);
|
||||
}
|
||||
}
|
||||
13
libs/key-management/src/biometrics/synced-unlock.state.ts
Normal file
13
libs/key-management/src/biometrics/synced-unlock.state.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { UserKeyDefinition, SYNCED_UNLOCK_SETTINGS_DISK } from "@bitwarden/common/platform/state";
|
||||
|
||||
/**
|
||||
* Indicates whether the user elected to store a biometric key to unlock their vault.
|
||||
*/
|
||||
export const SYNCED_UNLOCK_ENABLED = new UserKeyDefinition<boolean>(
|
||||
SYNCED_UNLOCK_SETTINGS_DISK,
|
||||
"syncedUnlockEnabled",
|
||||
{
|
||||
deserializer: (obj: any) => obj,
|
||||
clearOn: [],
|
||||
},
|
||||
);
|
||||
@@ -2,6 +2,10 @@ export {
|
||||
BiometricStateService,
|
||||
DefaultBiometricStateService,
|
||||
} from "./biometrics/biometric-state.service";
|
||||
export {
|
||||
SyncedUnlockStateServiceAbstraction,
|
||||
DefaultSyncedUnlockStateService,
|
||||
} from "./biometrics/synced-unlock-state.service";
|
||||
export { BiometricsStatus } from "./biometrics/biometrics-status";
|
||||
export { BiometricsCommands } from "./biometrics/biometrics-commands";
|
||||
export { BiometricsService } from "./biometrics/biometric.service";
|
||||
@@ -20,5 +24,6 @@ export {
|
||||
export { KdfConfigService } from "./abstractions/kdf-config.service";
|
||||
export { DefaultKdfConfigService } from "./kdf-config.service";
|
||||
export { KdfType } from "./enums/kdf-type.enum";
|
||||
export { SyncedUnlockStateCommands } from "./biometrics/synced-unlock-commands";
|
||||
|
||||
export * from "./user-asymmetric-key-regeneration";
|
||||
|
||||
Reference in New Issue
Block a user