mirror of
https://github.com/bitwarden/browser
synced 2026-02-24 16:43:27 +00:00
Merge branch 'main' into pm-18701-optional-payment-modal-after-signup
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2025.5.1",
|
||||
"version": "2025.6.0",
|
||||
"scripts": {
|
||||
"build": "npm run build:chrome",
|
||||
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
|
||||
@@ -5403,5 +5403,9 @@
|
||||
},
|
||||
"noPermissionsViewPage": {
|
||||
"message": "You do not have permissions to view this page. Try logging in with a different account."
|
||||
},
|
||||
"wasmNotSupported": {
|
||||
"message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.",
|
||||
"description": "'WebAssembly' is a technical term and should not be translated."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,10 @@
|
||||
{{ "showInlineMenuIdentitiesLabel" | i18n }}
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control *ngIf="enableInlineMenu" class="tw-ml-5">
|
||||
<bit-form-control
|
||||
*ngIf="enableInlineMenu && !(restrictedCardType$ | async)"
|
||||
class="tw-ml-5"
|
||||
>
|
||||
<input
|
||||
bitCheckbox
|
||||
id="show-inline-menu-cards"
|
||||
@@ -114,7 +117,7 @@
|
||||
</a>
|
||||
</bit-hint>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<bit-form-control *ngIf="!(restrictedCardType$ | async)">
|
||||
<input
|
||||
bitCheckbox
|
||||
id="showCardsSuggestions"
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ReactiveFormsModule,
|
||||
} from "@angular/forms";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { filter, firstValueFrom, Observable, switchMap } from "rxjs";
|
||||
import { filter, firstValueFrom, map, Observable, shareReplay, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
@@ -44,6 +44,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import {
|
||||
CardComponent,
|
||||
CheckboxModule,
|
||||
@@ -57,6 +58,7 @@ import {
|
||||
SelectModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
@@ -111,6 +113,11 @@ export class AutofillComponent implements OnInit {
|
||||
this.nudgesService.showNudgeSpotlight$(NudgeType.AutofillNudge, account.id),
|
||||
),
|
||||
);
|
||||
protected restrictedCardType$: Observable<boolean> =
|
||||
this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedTypes) => restrictedTypes.some((type) => type.cipherType === CipherType.Card)),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
protected autofillOnPageLoadForm = new FormGroup({
|
||||
autofillOnPageLoad: new FormControl(),
|
||||
@@ -156,6 +163,7 @@ export class AutofillComponent implements OnInit {
|
||||
private nudgesService: NudgesService,
|
||||
private accountService: AccountService,
|
||||
private autofillBrowserSettingsService: AutofillBrowserSettingsService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
this.autofillOnPageLoadOptions = [
|
||||
{ name: this.i18nService.t("autoFillOnPageLoadYes"), value: true },
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "Bitwarden",
|
||||
"version": "2025.5.1",
|
||||
"version": "2025.6.0",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"minimum_chrome_version": "102.0",
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "Bitwarden",
|
||||
"version": "2025.5.1",
|
||||
"version": "2025.6.0",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -35,9 +35,9 @@ if (BrowserApi.isManifestVersion(3)) {
|
||||
console.info("WebAssembly is supported in this environment");
|
||||
loadingPromise = import("./wasm");
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info("WebAssembly is not supported in this environment");
|
||||
loadingPromise = import("./fallback");
|
||||
loadingPromise = new Promise((_, reject) => {
|
||||
reject(new Error("WebAssembly is not supported in this environment"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,7 @@ async function importModule(): Promise<GlobalWithWasmInit["initSdk"]> {
|
||||
console.info("WebAssembly is supported in this environment");
|
||||
await import("./wasm");
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info("WebAssembly is not supported in this environment");
|
||||
await import("./fallback");
|
||||
throw new Error("WebAssembly is not supported in this environment");
|
||||
}
|
||||
|
||||
// the wasm and fallback imports mutate globalThis to add the initSdk function
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as sdk from "@bitwarden/sdk-internal";
|
||||
import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js";
|
||||
|
||||
import { GlobalWithWasmInit } from "./browser-sdk-load.service";
|
||||
|
||||
(globalThis as GlobalWithWasmInit).initSdk = () => {
|
||||
(sdk as any).init(wasm);
|
||||
};
|
||||
@@ -11,7 +11,17 @@ import {
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap, map } from "rxjs";
|
||||
import {
|
||||
Subject,
|
||||
takeUntil,
|
||||
firstValueFrom,
|
||||
concatMap,
|
||||
filter,
|
||||
tap,
|
||||
catchError,
|
||||
of,
|
||||
map,
|
||||
} from "rxjs";
|
||||
|
||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||
@@ -23,6 +33,7 @@ import { AnimationControlService } from "@bitwarden/common/platform/abstractions
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -48,23 +59,45 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
|
||||
styles: [],
|
||||
animations: [routerTransition],
|
||||
template: `
|
||||
<div [@routerTransition]="getRouteElevation(outlet)">
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
<bit-toast-container></bit-toast-container>
|
||||
@if (showSdkWarning | async) {
|
||||
<div class="tw-h-screen tw-flex tw-justify-center tw-items-center tw-p-4">
|
||||
<bit-callout type="danger">
|
||||
{{ "wasmNotSupported" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/wasm-not-supported/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
</bit-callout>
|
||||
</div>
|
||||
} @else {
|
||||
<div [@routerTransition]="getRouteElevation(outlet)">
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
<bit-toast-container></bit-toast-container>
|
||||
}
|
||||
`,
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
private compactModeService = inject(PopupCompactModeService);
|
||||
private sdkService = inject(SdkService);
|
||||
|
||||
private lastActivity: Date;
|
||||
private activeUserId: UserId;
|
||||
private recordActivitySubject = new Subject<void>();
|
||||
private routerAnimations = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
// Show a warning if the SDK is not available.
|
||||
protected showSdkWarning = this.sdkService.client$.pipe(
|
||||
map(() => false),
|
||||
catchError(() => of(true)),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private i18nService: I18nService,
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
ToastModule,
|
||||
CalloutModule,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { AccountComponent } from "../auth/popup/account-switching/account.component";
|
||||
@@ -87,6 +89,8 @@ import "../platform/popup/locales";
|
||||
CurrentAccountComponent,
|
||||
FormFieldModule,
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
CalloutModule,
|
||||
LinkModule,
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
||||
@@ -3,34 +3,12 @@
|
||||
{{ "new" | i18n }}
|
||||
</button>
|
||||
<bit-menu #itemOptions>
|
||||
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(cipherType.Login)">
|
||||
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeLogin" | i18n }}
|
||||
</a>
|
||||
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(cipherType.Card)">
|
||||
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeCard" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
bitMenuItem
|
||||
[routerLink]="['/add-cipher']"
|
||||
[queryParams]="buildQueryParams(cipherType.Identity)"
|
||||
>
|
||||
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeIdentity" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
bitMenuItem
|
||||
[routerLink]="['/add-cipher']"
|
||||
[queryParams]="buildQueryParams(cipherType.SecureNote)"
|
||||
>
|
||||
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
|
||||
{{ "note" | i18n }}
|
||||
</a>
|
||||
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(cipherType.SshKey)">
|
||||
<i class="bwi bwi-key" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeSshKey" | i18n }}
|
||||
</a>
|
||||
@for (menuItem of cipherMenuItems$ | async; track menuItem.type) {
|
||||
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(menuItem.type)">
|
||||
<i [class]="`bwi ${menuItem.icon}`" slot="start" aria-hidden="true"></i>
|
||||
{{ menuItem.labelKey | i18n }}
|
||||
</a>
|
||||
}
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<button type="button" bitMenuItem (click)="openFolderDialog()">
|
||||
<i class="bwi bwi-folder" slot="start" aria-hidden="true"></i>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, RouterLink } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -12,6 +13,7 @@ import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstraction
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||
import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
@@ -23,6 +25,7 @@ describe("NewItemDropdownV2Component", () => {
|
||||
let fixture: ComponentFixture<NewItemDropdownV2Component>;
|
||||
let dialogServiceMock: jest.Mocked<DialogService>;
|
||||
let browserApiMock: jest.Mocked<typeof BrowserApi>;
|
||||
let restrictedItemTypesServiceMock: jest.Mocked<RestrictedItemTypesService>;
|
||||
|
||||
const mockTab = { url: "https://example.com" };
|
||||
|
||||
@@ -44,6 +47,9 @@ describe("NewItemDropdownV2Component", () => {
|
||||
const folderServiceMock = mock<FolderService>();
|
||||
const folderApiServiceAbstractionMock = mock<FolderApiServiceAbstraction>();
|
||||
const accountServiceMock = mock<AccountService>();
|
||||
restrictedItemTypesServiceMock = {
|
||||
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
|
||||
} as any;
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -65,6 +71,7 @@ describe("NewItemDropdownV2Component", () => {
|
||||
{ provide: FolderService, useValue: folderServiceMock },
|
||||
{ provide: FolderApiServiceAbstraction, useValue: folderApiServiceAbstractionMock },
|
||||
{ provide: AccountService, useValue: accountServiceMock },
|
||||
{ provide: RestrictedItemTypesService, useValue: restrictedItemTypesServiceMock },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { RouterLink } from "@angular/router";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherMenuItem, CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||
import { AddEditFolderDialogComponent } from "@bitwarden/vault";
|
||||
import { AddEditFolderDialogComponent, RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
@@ -34,7 +36,22 @@ export class NewItemDropdownV2Component implements OnInit {
|
||||
@Input()
|
||||
initialValues: NewItemInitialValues;
|
||||
|
||||
constructor(private dialogService: DialogService) {}
|
||||
/**
|
||||
* Observable of cipher menu items that are not restricted by policy
|
||||
*/
|
||||
readonly cipherMenuItems$: Observable<CipherMenuItem[]> =
|
||||
this.restrictedItemTypeService.restricted$.pipe(
|
||||
map((restrictedTypes) => {
|
||||
const restrictedTypeArr = restrictedTypes.map((item) => item.cipherType);
|
||||
|
||||
return CIPHER_MENU_ITEMS.filter((menuItem) => !restrictedTypeArr.includes(menuItem.type));
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private restrictedItemTypeService: RestrictedItemTypesService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
fullWidth
|
||||
placeholderIcon="bwi-list"
|
||||
[placeholderText]="'type' | i18n"
|
||||
[options]="cipherTypes"
|
||||
[options]="cipherTypes$ | async"
|
||||
>
|
||||
</bit-chip-select>
|
||||
</form>
|
||||
|
||||
@@ -18,7 +18,7 @@ export class VaultListFiltersComponent {
|
||||
protected organizations$ = this.vaultPopupListFiltersService.organizations$;
|
||||
protected collections$ = this.vaultPopupListFiltersService.collections$;
|
||||
protected folders$ = this.vaultPopupListFiltersService.folders$;
|
||||
protected cipherTypes = this.vaultPopupListFiltersService.cipherTypes;
|
||||
protected cipherTypes$ = this.vaultPopupListFiltersService.cipherTypes$;
|
||||
|
||||
// Combine all filters into a single observable to eliminate the filters from loading separately in the UI.
|
||||
protected allFilters$ = combineLatest([
|
||||
|
||||
@@ -20,6 +20,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
import {
|
||||
CachedFilterState,
|
||||
@@ -70,6 +71,10 @@ describe("VaultPopupListFiltersService", () => {
|
||||
const state$ = new BehaviorSubject<boolean>(false);
|
||||
const update = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const restrictedItemTypesService = {
|
||||
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
_memberOrganizations$ = new BehaviorSubject<Organization[]>([]); // Fresh instance per test
|
||||
folderViews$ = new BehaviorSubject([]); // Fresh instance per test
|
||||
@@ -125,21 +130,46 @@ describe("VaultPopupListFiltersService", () => {
|
||||
provide: ViewCacheService,
|
||||
useValue: viewCacheService,
|
||||
},
|
||||
{
|
||||
provide: RestrictedItemTypesService,
|
||||
useValue: restrictedItemTypesService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(VaultPopupListFiltersService);
|
||||
});
|
||||
|
||||
describe("cipherTypes", () => {
|
||||
it("returns all cipher types", () => {
|
||||
expect(service.cipherTypes.map((c) => c.value)).toEqual([
|
||||
CipherType.Login,
|
||||
CipherType.Card,
|
||||
CipherType.Identity,
|
||||
CipherType.SecureNote,
|
||||
CipherType.SshKey,
|
||||
describe("cipherTypes$", () => {
|
||||
it("returns all cipher types when no restrictions", (done) => {
|
||||
restrictedItemTypesService.restricted$.next([]);
|
||||
|
||||
service.cipherTypes$.subscribe((cipherTypes) => {
|
||||
expect(cipherTypes.map((c) => c.value)).toEqual([
|
||||
CipherType.Login,
|
||||
CipherType.Card,
|
||||
CipherType.Identity,
|
||||
CipherType.SecureNote,
|
||||
CipherType.SshKey,
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("filters out restricted cipher types", (done) => {
|
||||
restrictedItemTypesService.restricted$.next([
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: [] },
|
||||
]);
|
||||
|
||||
service.cipherTypes$.subscribe((cipherTypes) => {
|
||||
expect(cipherTypes.map((c) => c.value)).toEqual([
|
||||
CipherType.Login,
|
||||
CipherType.Identity,
|
||||
CipherType.SecureNote,
|
||||
CipherType.SshKey,
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -452,6 +482,10 @@ describe("VaultPopupListFiltersService", () => {
|
||||
{ type: CipherType.SecureNote, collectionIds: [], organizationId: null },
|
||||
] as CipherView[];
|
||||
|
||||
beforeEach(() => {
|
||||
restrictedItemTypesService.restricted$.next([]);
|
||||
});
|
||||
|
||||
it("filters by cipherType", (done) => {
|
||||
service.filterFunction$.subscribe((filterFunction) => {
|
||||
expect(filterFunction(ciphers)).toEqual([ciphers[0]]);
|
||||
@@ -690,6 +724,9 @@ function createSeededVaultPopupListFiltersService(
|
||||
} as any;
|
||||
|
||||
const accountServiceMock = mockAccountServiceWith("userId" as UserId);
|
||||
const restrictedItemTypesServiceMock = {
|
||||
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
|
||||
} as any;
|
||||
const formBuilderInstance = new FormBuilder();
|
||||
|
||||
const seededCachedSignal = createMockSignal<CachedFilterState>(cachedState);
|
||||
@@ -713,6 +750,7 @@ function createSeededVaultPopupListFiltersService(
|
||||
stateProviderMock,
|
||||
accountServiceMock,
|
||||
viewCacheServiceMock,
|
||||
restrictedItemTypesServiceMock,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
import { ChipSelectOption } from "@bitwarden/components";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
const FILTER_VISIBILITY_KEY = new KeyDefinition<boolean>(VAULT_SETTINGS_DISK, "filterVisibility", {
|
||||
deserializer: (obj) => obj,
|
||||
@@ -178,6 +180,7 @@ export class VaultPopupListFiltersService {
|
||||
private stateProvider: StateProvider,
|
||||
private accountService: AccountService,
|
||||
private viewCacheService: ViewCacheService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
this.filterForm.controls.organization.valueChanges
|
||||
.pipe(takeUntilDestroyed())
|
||||
@@ -210,74 +213,80 @@ export class VaultPopupListFiltersService {
|
||||
/**
|
||||
* Observable whose value is a function that filters an array of `CipherView` objects based on the current filters
|
||||
*/
|
||||
filterFunction$: Observable<(ciphers: CipherView[]) => CipherView[]> = this.filters$.pipe(
|
||||
filterFunction$: Observable<(ciphers: CipherView[]) => CipherView[]> = combineLatest([
|
||||
this.filters$,
|
||||
this.restrictedItemTypesService.restricted$.pipe(startWith([])),
|
||||
]).pipe(
|
||||
map(
|
||||
(filters) => (ciphers: CipherView[]) =>
|
||||
ciphers.filter((cipher) => {
|
||||
// Vault popup lists never shows deleted ciphers
|
||||
if (cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.cipherType !== null && cipher.type !== filters.cipherType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.collection && !cipher.collectionIds?.includes(filters.collection.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.folder && cipher.folderId !== filters.folder.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isMyVault = filters.organization?.id === MY_VAULT_ID;
|
||||
|
||||
if (isMyVault) {
|
||||
if (cipher.organizationId !== null) {
|
||||
([filters, restrictions]) =>
|
||||
(ciphers: CipherView[]) =>
|
||||
ciphers.filter((cipher) => {
|
||||
// Vault popup lists never shows deleted ciphers
|
||||
if (cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
} else if (filters.organization) {
|
||||
if (cipher.organizationId !== filters.organization.id) {
|
||||
|
||||
// Check if cipher type is restricted (with organization exemptions)
|
||||
if (restrictions && restrictions.length > 0) {
|
||||
const isRestricted = restrictions.some(
|
||||
(restrictedType) =>
|
||||
restrictedType.cipherType === cipher.type &&
|
||||
(cipher.organizationId
|
||||
? !restrictedType.allowViewOrgIds.includes(cipher.organizationId)
|
||||
: restrictedType.allowViewOrgIds.length === 0),
|
||||
);
|
||||
|
||||
if (isRestricted) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.cipherType !== null && cipher.type !== filters.cipherType) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
if (filters.collection && !cipher.collectionIds?.includes(filters.collection.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.folder && cipher.folderId !== filters.folder.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isMyVault = filters.organization?.id === MY_VAULT_ID;
|
||||
|
||||
if (isMyVault) {
|
||||
if (cipher.organizationId !== null) {
|
||||
return false;
|
||||
}
|
||||
} else if (filters.organization) {
|
||||
if (cipher.organizationId !== filters.organization.id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* All available cipher types
|
||||
* All available cipher types (filtered by policy restrictions)
|
||||
*/
|
||||
readonly cipherTypes: ChipSelectOption<CipherType>[] = [
|
||||
{
|
||||
value: CipherType.Login,
|
||||
label: this.i18nService.t("typeLogin"),
|
||||
icon: "bwi-globe",
|
||||
},
|
||||
{
|
||||
value: CipherType.Card,
|
||||
label: this.i18nService.t("typeCard"),
|
||||
icon: "bwi-credit-card",
|
||||
},
|
||||
{
|
||||
value: CipherType.Identity,
|
||||
label: this.i18nService.t("typeIdentity"),
|
||||
icon: "bwi-id-card",
|
||||
},
|
||||
{
|
||||
value: CipherType.SecureNote,
|
||||
label: this.i18nService.t("note"),
|
||||
icon: "bwi-sticky-note",
|
||||
},
|
||||
{
|
||||
value: CipherType.SshKey,
|
||||
label: this.i18nService.t("typeSshKey"),
|
||||
icon: "bwi-key",
|
||||
},
|
||||
];
|
||||
readonly cipherTypes$: Observable<ChipSelectOption<CipherType>[]> =
|
||||
this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedTypes) => {
|
||||
const restrictedCipherTypes = restrictedTypes.map((r) => r.cipherType);
|
||||
|
||||
return CIPHER_MENU_ITEMS.filter((item) => !restrictedCipherTypes.includes(item.type)).map(
|
||||
(item) => ({
|
||||
value: item.type,
|
||||
label: this.i18nService.t(item.labelKey),
|
||||
icon: item.icon,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
/** Resets `filterForm` to the original state */
|
||||
resetFilterForm(): void {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/cli",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.5.0",
|
||||
"version": "2025.6.0",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRQzzQ8nQEouF1FMSHkPx1nejNCzF7g
|
||||
Yb8MHXLdBFM0uJkWs0vzgLJkttts2eDv3SHJqIH6qHpkLtEvgMXE5WcaAAAAoOO1BebjtQ
|
||||
XmAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFDPNDydASi4XUUx
|
||||
IeQ/HWd6M0LMXuBhvwwdct0EUzS4mRazS/OAsmS222zZ4O/dIcmogfqoemQu0S+AxcTlZx
|
||||
oAAAAhAKnIXk6H0Hs3HblklaZ6UmEjjdE/0t7EdYixpMmtpJ4eAAAAB3Rlc3RrZXk=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -1 +0,0 @@
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFDPNDydASi4XUUxIeQ/HWd6M0LMXuBhvwwdct0EUzS4mRazS/OAsmS222zZ4O/dIcmogfqoemQu0S+AxcTlZxo= testkey
|
||||
@@ -1,8 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAUTNb0if
|
||||
fqsoqtfv70CfukAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHGs3Uw3eyqnFjBI
|
||||
2eb7Qto4KVc34ZdnBac59Bab54BLAAAAkPA6aovfxQbP6FoOfaRH6u22CxqiUM0bbMpuFf
|
||||
WETn9FLaBE6LjoHH0ZI5rzNjJaQUNfx0cRcqsIrexw8YINrdVjySmEqrl5hw8gpgy0gGP5
|
||||
1Y6vKWdHdrxJCA9YMFOfDs0UhPfpLKZCwm2Sg+Bd8arlI8Gy7y4Jj/60v2bZOLhD2IZQnK
|
||||
NdJ8xATiIINuTy4g==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -1 +0,0 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHGs3Uw3eyqnFjBI2eb7Qto4KVc34ZdnBac59Bab54BL testkey
|
||||
@@ -1,7 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACAyQo22TXXNqvF+L8jUSSNeu8UqrsDjvf9pwIwDC9ML6gAAAJDSHpL60h6S
|
||||
+gAAAAtzc2gtZWQyNTUxOQAAACAyQo22TXXNqvF+L8jUSSNeu8UqrsDjvf9pwIwDC9ML6g
|
||||
AAAECLdlFLIJbEiFo/f0ROdXMNZAPHGPNhvbbftaPsUZEjaDJCjbZNdc2q8X4vyNRJI167
|
||||
xSquwOO9/2nAjAML0wvqAAAAB3Rlc3RrZXkBAgMEBQY=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -1 +0,0 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDJCjbZNdc2q8X4vyNRJI167xSquwOO9/2nAjAML0wvq testkey
|
||||
@@ -1,4 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MFECAQEwBQYDK2VwBCIEIDY6/OAdDr3PbDss9NsLXK4CxiKUvz5/R9uvjtIzj4Sz
|
||||
gSEAxsxm1xpZ/4lKIRYm0JrJ5gRZUh7H24/YT/0qGVGzPa0=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1 +0,0 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMbMZtcaWf+JSiEWJtCayeYEWVIex9uP2E/9KhlRsz2t
|
||||
@@ -1,8 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
|
||||
c2gtZWQyNTUxOQAAACDp0/9zFBCyZs5BFqXCJN5i1DTanzPGHpUeo2LP8FmQ9wAA
|
||||
AKCyIXPqsiFz6gAAAAtzc2gtZWQyNTUxOQAAACDp0/9zFBCyZs5BFqXCJN5i1DTa
|
||||
nzPGHpUeo2LP8FmQ9wAAAEDQioomhjmD+sh2nsxfQLJ5YYGASNUAlUZHe9Jx0p47
|
||||
H+nT/3MUELJmzkEWpcIk3mLUNNqfM8YelR6jYs/wWZD3AAAAEmVkZHNhLWtleS0y
|
||||
MDI0MTExOAECAwQFBgcICQoL
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -1,39 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
2MmKBw==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -1 +0,0 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/v18xGP3qzRV9iWqyiuwHZ4GpC4K2NO2/i2Yv5A3/bnal7CmiMh/S78lphgxcWtFkwrwlb321FmdHBv6KOW+EzSiPvmsdkkbpfBXB3Qf2SlhZOZZ7lYeu8KAxL3exvvn8O1GGlUjXGUrFgmC60tHWDBc1Ncmo8a2dwDLmA/sbLa8su2dvYEFmRg1vaytLDpkn8GS7zAxrUl/g0W2RwkPsByduUziQuX90v9WAy7MqOlwBRq6t5o8wdDBVODe0VIXC7N1OS42YUsKF+N0XOnLiJrIIKkXpahMDpKZHeHQAdUQzsJVhKoLJR8DNDTYyhnJoQG7Q6m2gDTca9oAWvsBiNoEwCvwrt7cDNCz/GslH9HXQgfWcVXn8+fuZgvjO3CxUI16Ev33m0jWoOKJcgK/ZLRnk8SEvsJ8NO32MeR/qUb7IN/yUcDmPMI/3ecQsakF2cwNzHkyiGVo//yVTpf+vk8b89L+GXbYU5rtswtc2ZEGsQnUkaoNqS8mHqhWQBUk= testkey
|
||||
@@ -1,38 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAYEAtVIe0gnPtD6299/roT7ntZgVe+qIqIMIruJdI2xTanLGhNpBOlzg
|
||||
WqokbQK+aXATcaB7iQL1SPxIWV2M4jEBQbZuimIgDQvKbJ4TZPKEe1VdsrfuIo+9pDK7cG
|
||||
Kc+JiWhKjqeTRMj91/qR1fW5IWOUyE1rkwhTNkwJqtYKZLVmd4TXtQsYMMC+I0cz4krfk1
|
||||
Yqmaae/gj12h8BvE3Y+Koof4JoLsqPufH+H/bVEayv63RyAQ1/tUv9l+rwJ+svWV4X3zf3
|
||||
z40hGF43L/NGl90Vutbn7b9G/RgEdiXyLZciP3XbWbLUM+r7mG9KNuSeoixe5jok15UKqC
|
||||
XXxVb5IEZ73kaubSfz9JtsqtKG/OjOq6Fbl3Ky7kjvJyGpIvesuSInlpzPXqbLUCLJJfOA
|
||||
PUZ1wi8uuuRNePzQBMMhq8UtAbB2Dy16d+HlgghzQ00NxtbQMfDZBdApfxm3shIxkUcHzb
|
||||
DSvriHVaGGoOkmHPAmsdMsMiekuUMe9ljdOhmdTxAAAFgF8XjBxfF4wcAAAAB3NzaC1yc2
|
||||
EAAAGBALVSHtIJz7Q+tvff66E+57WYFXvqiKiDCK7iXSNsU2pyxoTaQTpc4FqqJG0Cvmlw
|
||||
E3Gge4kC9Uj8SFldjOIxAUG2bopiIA0LymyeE2TyhHtVXbK37iKPvaQyu3BinPiYloSo6n
|
||||
k0TI/df6kdX1uSFjlMhNa5MIUzZMCarWCmS1ZneE17ULGDDAviNHM+JK35NWKpmmnv4I9d
|
||||
ofAbxN2PiqKH+CaC7Kj7nx/h/21RGsr+t0cgENf7VL/Zfq8CfrL1leF98398+NIRheNy/z
|
||||
RpfdFbrW5+2/Rv0YBHYl8i2XIj9121my1DPq+5hvSjbknqIsXuY6JNeVCqgl18VW+SBGe9
|
||||
5Grm0n8/SbbKrShvzozquhW5dysu5I7ychqSL3rLkiJ5acz16my1AiySXzgD1GdcIvLrrk
|
||||
TXj80ATDIavFLQGwdg8tenfh5YIIc0NNDcbW0DHw2QXQKX8Zt7ISMZFHB82w0r64h1Whhq
|
||||
DpJhzwJrHTLDInpLlDHvZY3ToZnU8QAAAAMBAAEAAAGAEL3wpRWtVTf+NnR5QgX4KJsOjs
|
||||
bI0ABrVpSFo43uxNMss9sgLzagq5ZurxcUBFHKJdF63puEkPTkbEX4SnFaa5of6kylp3a5
|
||||
fd55rXY8F9Q5xtT3Wr8ZdFYP2xBr7INQUJb1MXRMBnOeBDw3UBH01d0UHexzB7WHXcZacG
|
||||
Ria+u5XrQebwmJ3PYJwENSaTLrxDyjSplQy4QKfgxeWNPWaevylIG9vtue5Xd9WXdl6Szs
|
||||
ONfD3mFxQZagPSIWl0kYIjS3P2ZpLe8+sakRcfci8RjEUP7U+QxqY5VaQScjyX1cSYeQLz
|
||||
t+/6Tb167aNtQ8CVW3IzM2EEN1BrSbVxFkxWFLxogAHct06Kn87nPn2+PWGWOVCBp9KheO
|
||||
FszWAJ0Kzjmaga2BpOJcrwjSpGopAb1YPIoRPVepVZlQ4gGwy5gXCFwykT9WTBoJfg0BMQ
|
||||
r3MSNcoc97eBomIWEa34K0FuQ3rVjMv9ylfyLvDBbRqTJ5zebeOuU+yCQHZUKk8klRAAAA
|
||||
wAsToNZvYWRsOMTWQom0EW1IHzoL8Cyua+uh72zZi/7enm4yHPJiu2KNgQXfB0GEEjHjbo
|
||||
9peCW3gZGTV+Ee+cAqwYLlt0SMl/VJNxN3rEG7BAqPZb42Ii2XGjaxzFq0cliUGAdo6UEd
|
||||
swU8d2I7m9vIZm4nDXzsWOBWgonTKBNyL0DQ6KNOGEyj8W0BTCm7Rzwy7EKzFWbIxr4lSc
|
||||
vDrJ3t6kOd7jZTF58kRMT0nxR0bf43YzF/3/qSvLYhQm/OOAAAAMEA2F6Yp8SrpQDNDFxh
|
||||
gi4GeywArrQO9r3EHjnBZi/bacxllSzCGXAvp7m9OKC1VD2wQP2JL1VEIZRUTuGGT6itrm
|
||||
QpX8OgoxlEJrlC5W0kHumZ3MFGd33W11u37gOilmd6+VfVXBziNG2rFohweAgs8X+Sg5AA
|
||||
nIfMV6ySXUlvLzMHpGeKRRnnQq9Cwn4rDkVQENLd1i4e2nWFhaPTUwVMR8YuOT766bywr3
|
||||
7vG1PQLF7hnf2c/oPHAru+XD9gJWs5AAAAwQDWiB2G23F4Tvq8FiK2mMusSjQzHupl83rm
|
||||
o3BSNRCvCjaLx6bWhDPSA1edNEF7VuP6rSp+i+UfSORHwOnlgnrvtcJeoDuA72hUeYuqD/
|
||||
1C9gghdhKzGTVf/IGTX1tH3rn2Gq9TEyrJs/ITcoOyZprz7VbaD3bP/NEER+m1EHi2TS/3
|
||||
SXQEtRm+IIBwba+QLUcsrWdQyIO+1OCXywDrAw50s7tjgr/goHgXTcrSXaKcIEOlPgBZH3
|
||||
YPuVuEtRYgX3kAAAAHdGVzdGtleQECAwQ=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -1 +0,0 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1Uh7SCc+0Prb33+uhPue1mBV76oiogwiu4l0jbFNqcsaE2kE6XOBaqiRtAr5pcBNxoHuJAvVI/EhZXYziMQFBtm6KYiANC8psnhNk8oR7VV2yt+4ij72kMrtwYpz4mJaEqOp5NEyP3X+pHV9bkhY5TITWuTCFM2TAmq1gpktWZ3hNe1CxgwwL4jRzPiSt+TViqZpp7+CPXaHwG8Tdj4qih/gmguyo+58f4f9tURrK/rdHIBDX+1S/2X6vAn6y9ZXhffN/fPjSEYXjcv80aX3RW61uftv0b9GAR2JfItlyI/ddtZstQz6vuYb0o25J6iLF7mOiTXlQqoJdfFVvkgRnveRq5tJ/P0m2yq0ob86M6roVuXcrLuSO8nIaki96y5IieWnM9epstQIskl84A9RnXCLy665E14/NAEwyGrxS0BsHYPLXp34eWCCHNDTQ3G1tAx8NkF0Cl/GbeyEjGRRwfNsNK+uIdVoYag6SYc8Cax0ywyJ6S5Qx72WN06GZ1PE= testkey
|
||||
@@ -1,42 +0,0 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
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
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@@ -1 +0,0 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCcHkc0xfH4w9aW41S9M/BfancSY4QPc2O4G1cRjFfK8QrLEGDA7NiHtoEML0afcurRXD3NVxuKaAns0w6EoS4CjzXUqVHTLA4SUyuapr8k0Eu2xOpbCwC3jDovhckoKloq7BvE6rC2i5wjSMadtIJKt/dqWI3HLjUMz1BxQJAU/qAbicj1SFZSjA/MubVBzcq93XOvByMtlIFu7wami3FTc37rVkGeUFHtK8ZbvG3n1aaTF79bBgSPuoq5BfcMdGr4WfQyGQzgse4v4hQ8yKYrtE0jo0kf06hEORimwOIU/W5IH1r+/xFs7qGKcPnFSZRIFv5LfMPTo8b+OsBRflosyfUumDEX97GZE7DSQl0EJzNvWeKwl7dQ8RUJTkbph2CjrxY77DFim+165Uj/WRr4uq2qMNhA2xNSD19+TA6AHdpGw4WZd37q2/n+EddlaJEH8MzpgtHNG9MiYh5ScZ+AG0QugflozJcQNc7n8N9Lpu1sRoejV5RhurHg/TYwVK8= testkey
|
||||
@@ -1,40 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQCn4+QiJojZ9mgc
|
||||
9KYJIvDWGaz4qFhf0CButg6L8zEoHKwuiN+mqcEciCCOa9BNiJmm8NTTehZvrrgl
|
||||
GG59zIbqYtDAHjVn+vtb49xPzIv+M651Yqj08lIbR9tEIHKCq7aH8GlDm8NgG9Ez
|
||||
JGjlL7okQym4TH1MHl+s4mUyr/qb2unlZBDixAQsphU8iCLftukWCIkmQg4CSj1G
|
||||
h3WbBlZ+EX5eW0EXuAw4XsSbBTWV9CHRowVIpYqPvEYSpHsoCjEcd988p19hpiGk
|
||||
nA0J4z7JfUlNgyT/1chb8GCTDT+2DCBRApbsIg6TOBVS+PR6emAQ3eZzUW0+3/oR
|
||||
M4ip0ujltQy8uU6gvYIAqx5wXGMThVpZcUgahKiSsVo/s4b84iMe4DG3W8jz4qi6
|
||||
yyNv0VedEzPUZ1lXd1GJFoy9uKNuSTe+1ksicAcluZN6LuNsPHcPxFCzOcmoNnVX
|
||||
EKAXInt+ys//5CDVasroZSAHZnDjUD4oNsLI3VIOnGxgXrkwSH0CAwEAAQKCAYAA
|
||||
2SDMf7OBHw1OGM9OQa1ZS4u+ktfQHhn31+FxbrhWGp+lDt8gYABVf6Y4dKN6rMtn
|
||||
7D9gVSAlZCAn3Hx8aWAvcXHaspxe9YXiZDTh+Kd8EIXxBQn+TiDA5LH0dryABqmM
|
||||
p20vYKtR7OS3lIIXfFBSrBMwdunKzLwmKwZLWq0SWf6vVbwpxRyR9CyByodF6Djm
|
||||
ZK3QB2qQ3jqlL1HWXL0VnyArY7HLvUvfLLK4vMPqnsSH+FdHvhcEhwqMlWT44g+f
|
||||
hqWtCJNnjDgLK3FPbI8Pz9TF8dWJvOmp5Q6iSBua1e9x2LizVuNSqiFc7ZTLeoG4
|
||||
nDj7T2BtqB0E1rNUDEN1aBo+UZmHJK7LrzfW/B+ssi2WwIpfxYa1lO6HFod5/YQi
|
||||
XV1GunyH1chCsbvOFtXvAHASO4HTKlJNbWhRF1GXqnKpAaHDPCVuwp3eq6Yf0oLb
|
||||
XrL3KFZ3jwWiWbpQXRVvpqzaJwZn3CN1yQgYS9j17a9wrPky+BoJxXjZ/oImWLEC
|
||||
gcEA0lkLwiHvmTYFTCC7PN938Agk9/NQs5PQ18MRn9OJmyfSpYqf/gNp+Md7xUgt
|
||||
F/MTif7uelp2J7DYf6fj9EYf9g4EuW+SQgFP4pfiJn1+zGFeTQq1ISvwjsA4E8ZS
|
||||
t+GIumjZTg6YiL1/A79u4wm24swt7iqnVViOPtPGOM34S1tAamjZzq2eZDmAF6pA
|
||||
fmuTMdinCMR1E1kNJYbxeqLiqQCXuwBBnHOOOJofN3AkvzjRUBB9udvniqYxH3PQ
|
||||
cxPxAoHBAMxT5KwBhZhnJedYN87Kkcpl7xdMkpU8b+aXeZoNykCeoC+wgIQexnSW
|
||||
mFk4HPkCNxvCWlbkOT1MHrTAKFnaOww23Ob+Vi6A9n0rozo9vtoJig114GB0gUqE
|
||||
mtfLhO1P5AE8yzogE+ILHyp0BqXt8vGIfzpDnCkN+GKl8gOOMPrR4NAcLO+Rshc5
|
||||
nLs7BGB4SEi126Y6mSfp85m0++1QhWMz9HzqJEHCWKVcZYdCdEONP9js04EUnK33
|
||||
KtlJIWzZTQKBwAT0pBpGwmZRp35Lpx2gBitZhcVxrg0NBnaO2fNyAGPvZD8SLQLH
|
||||
AdAiov/a23Uc/PDbWLL5Pp9gwzj+s5glrssVOXdE8aUscr1b5rARdNNL1/Tos6u8
|
||||
ZUZ3sNqGaZx7a8U4gyYboexWyo9EC1C+AdkGBm7+AkM4euFwC9N6xsa/t5zKK5d6
|
||||
76hc0m+8SxivYCBkgkrqlfeGuZCQxU+mVsC0it6U+va8ojUjLGkZ80OuCwBf4xZl
|
||||
3+acU7vx9o8/gQKBwB7BrhU6MWrsc+cr/1KQaXum9mNyckomi82RFYvb8Yrilcg3
|
||||
8FBy9XqNRKeBa9MLw1HZYpHbzsXsVF7u4eQMloDTLVNUC5L6dKAI1owoyTa24uH9
|
||||
0WWTg/a8mTZMe1jhgrew+AJq27NV6z4PswR9GenDmyshDDudz7rBsflZCQRoXUfW
|
||||
RelV7BHU6UPBsXn4ASF4xnRyM6WvcKy9coKZcUqqgm3fLM/9OizCCMJgfXHBrE+x
|
||||
7nBqst746qlEedSRrQKBwQCVYwwKCHNlZxl0/NMkDJ+hp7/InHF6mz/3VO58iCb1
|
||||
9TLDVUC2dDGPXNYwWTT9PclefwV5HNBHcAfTzgB4dpQyNiDyV914HL7DFEGduoPn
|
||||
wBYjeFre54v0YjjnskjJO7myircdbdX//i+7LMUw5aZZXCC8a5BD/rdV6IKJWJG5
|
||||
QBXbe5fVf1XwOjBTzlhIPIqhNFfSu+mFikp5BRwHGBqsKMju6inYmW6YADeY/SvO
|
||||
QjDEB37RqGZxqyIx8V2ZYwU=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1 +0,0 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCn4+QiJojZ9mgc9KYJIvDWGaz4qFhf0CButg6L8zEoHKwuiN+mqcEciCCOa9BNiJmm8NTTehZvrrglGG59zIbqYtDAHjVn+vtb49xPzIv+M651Yqj08lIbR9tEIHKCq7aH8GlDm8NgG9EzJGjlL7okQym4TH1MHl+s4mUyr/qb2unlZBDixAQsphU8iCLftukWCIkmQg4CSj1Gh3WbBlZ+EX5eW0EXuAw4XsSbBTWV9CHRowVIpYqPvEYSpHsoCjEcd988p19hpiGknA0J4z7JfUlNgyT/1chb8GCTDT+2DCBRApbsIg6TOBVS+PR6emAQ3eZzUW0+3/oRM4ip0ujltQy8uU6gvYIAqx5wXGMThVpZcUgahKiSsVo/s4b84iMe4DG3W8jz4qi6yyNv0VedEzPUZ1lXd1GJFoy9uKNuSTe+1ksicAcluZN6LuNsPHcPxFCzOcmoNnVXEKAXInt+ys//5CDVasroZSAHZnDjUD4oNsLI3VIOnGxgXrkwSH0= testkey
|
||||
@@ -1,30 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz
|
||||
c2gtcnNhAAAAAwEAAQAAAQEAootgTLcKjSgPLS2+RT3ZElhktL1CwIyM/+3IqEq0
|
||||
0fl/rRHBT8otklV3Ld7DOR50HVZSoV0u9qs0WOdxcjEJlJACDClmxZmFr0BQ/E2y
|
||||
V5xzuMZj3Mj+fL26jTmM3ueRHZ0tU5ubSFvINIFyDGG70F7VgkpBA8zsviineMSU
|
||||
t1iIPi/6feL6h7QAFUk6JdQJpPTs9Nb2+DAQ9lMS2614cxLXkfUIXA4NvHMfZGdU
|
||||
dq1mBJIAWZ4PPJ6naUcu0lVYjuVEAOE4UoHxr6YlW4yyAF/I1YXBFpcHG7P0egvg
|
||||
neTPli5Wzum0XDsOPivqr6z2E5k7nzyGXUaP5MjRfDDVLwAAA9D6lTpR+pU6UQAA
|
||||
AAdzc2gtcnNhAAABAQCii2BMtwqNKA8tLb5FPdkSWGS0vULAjIz/7cioSrTR+X+t
|
||||
EcFPyi2SVXct3sM5HnQdVlKhXS72qzRY53FyMQmUkAIMKWbFmYWvQFD8TbJXnHO4
|
||||
xmPcyP58vbqNOYze55EdnS1Tm5tIW8g0gXIMYbvQXtWCSkEDzOy+KKd4xJS3WIg+
|
||||
L/p94vqHtAAVSTol1Amk9Oz01vb4MBD2UxLbrXhzEteR9QhcDg28cx9kZ1R2rWYE
|
||||
kgBZng88nqdpRy7SVViO5UQA4ThSgfGvpiVbjLIAX8jVhcEWlwcbs/R6C+Cd5M+W
|
||||
LlbO6bRcOw4+K+qvrPYTmTufPIZdRo/kyNF8MNUvAAAAAwEAAQAAAQB6YVPVDq9s
|
||||
DfA3RMyQF3vbOyA/kIu0q13xx1cflnfD7AT8CnUwnPloxt5fc+wqkko8WGUIRz93
|
||||
yvkzwrYAkvkymKZh/734IpmrlFIlVF5lZk8enIhNkCtDQho2AFGW9mSlFlUtMOhe
|
||||
N3RqS9fRiLg+r1gzq7J9qQnKNpO48tFBpLkIqr8nZOVhEn8IASrQYBUoocClNrv6
|
||||
Pdl8ni5jqnZ/0K0nq4+41Ag1VMI4LUcRCucid8ci1HKdOmGXkvClbzuFMWv3UC0k
|
||||
qDgzg/gOIgj75I7B34FYVx47UGZ6jmC7iRkHd6RiCHYkmsDSjRQHR6eRbtLPdl9w
|
||||
TlG2NrwkbSlhAAAAgQCapfJLqew9aK8PKfe3FwiC9sb0itCAXPXHhD+pQ6Tl9UMZ
|
||||
hmnG2g9qbowCprz3/kyix+nWL/Kx7eKAZYH2MBz6cxfqs2A+BSuxvX/hsnvF96BP
|
||||
u1I47rGrd0NC78DTY2NDO4Ccirx6uN+AoCl4cC+KC00Kykww6TTEBrQsdQTk5QAA
|
||||
AIEA7JwbIIMwDiQUt3EY/VU0SYvg67aOiyOYEWplSWCGdT58jnfS1H95kGVw+qXR
|
||||
eSQ0VNv6LBz7XDRpfQlNXDNJRnDZuHBbk+T9ZwnynRLWuzK7VqZBPJoNoyLFSMW2
|
||||
DBhLVKIrg0MsBAnRBMDUlVDlzs2LoNLEra3dj8Zb9vMdlbEAAACBAK/db27GfXXg
|
||||
OikZkIqWiFgBArtj0T4iFc7BLUJUeFtl0RP9LLjfvaxSdA1cmVYzzkgmuc2iZLF0
|
||||
37zuPkDrfYVRiw8rSihT3D+WDt3/Tt013WCuxVQOQSW+Qtw6yZpM92DKncbvYsUy
|
||||
5DNklW1+TYxyn2ltM7SaZjmF8UeoTnDfAAAAEHJzYS1rZXktMjAyNDExMTgBAgME
|
||||
BQYHCAkK
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAootgTLcKjSgPLS2+RT3ZElhktL1CwIyM/+3IqEq00fl/rRHB
|
||||
T8otklV3Ld7DOR50HVZSoV0u9qs0WOdxcjEJlJACDClmxZmFr0BQ/E2yV5xzuMZj
|
||||
3Mj+fL26jTmM3ueRHZ0tU5ubSFvINIFyDGG70F7VgkpBA8zsviineMSUt1iIPi/6
|
||||
feL6h7QAFUk6JdQJpPTs9Nb2+DAQ9lMS2614cxLXkfUIXA4NvHMfZGdUdq1mBJIA
|
||||
WZ4PPJ6naUcu0lVYjuVEAOE4UoHxr6YlW4yyAF/I1YXBFpcHG7P0egvgneTPli5W
|
||||
zum0XDsOPivqr6z2E5k7nzyGXUaP5MjRfDDVLwIDAQABAoIBAHphU9UOr2wN8DdE
|
||||
zJAXe9s7ID+Qi7SrXfHHVx+Wd8PsBPwKdTCc+WjG3l9z7CqSSjxYZQhHP3fK+TPC
|
||||
tgCS+TKYpmH/vfgimauUUiVUXmVmTx6ciE2QK0NCGjYAUZb2ZKUWVS0w6F43dGpL
|
||||
19GIuD6vWDOrsn2pCco2k7jy0UGkuQiqvydk5WESfwgBKtBgFSihwKU2u/o92Xye
|
||||
LmOqdn/QrSerj7jUCDVUwjgtRxEK5yJ3xyLUcp06YZeS8KVvO4Uxa/dQLSSoODOD
|
||||
+A4iCPvkjsHfgVhXHjtQZnqOYLuJGQd3pGIIdiSawNKNFAdHp5Fu0s92X3BOUbY2
|
||||
vCRtKWECgYEA7JwbIIMwDiQUt3EY/VU0SYvg67aOiyOYEWplSWCGdT58jnfS1H95
|
||||
kGVw+qXReSQ0VNv6LBz7XDRpfQlNXDNJRnDZuHBbk+T9ZwnynRLWuzK7VqZBPJoN
|
||||
oyLFSMW2DBhLVKIrg0MsBAnRBMDUlVDlzs2LoNLEra3dj8Zb9vMdlbECgYEAr91v
|
||||
bsZ9deA6KRmQipaIWAECu2PRPiIVzsEtQlR4W2XRE/0suN+9rFJ0DVyZVjPOSCa5
|
||||
zaJksXTfvO4+QOt9hVGLDytKKFPcP5YO3f9O3TXdYK7FVA5BJb5C3DrJmkz3YMqd
|
||||
xu9ixTLkM2SVbX5NjHKfaW0ztJpmOYXxR6hOcN8CgYASLZAb+Fg5zeXVjhfYZrJk
|
||||
sB1wno7m+64UMHNlpsfNvCY/n88Pyldhk5mReCnWv8RRfLEEsJlTJSexloReAAay
|
||||
JbtkYyV2AFLDls0P6kGbEjO4XX+Hk2JW1TYI+D+bQEaRUwA6zm9URBjN3661Zgix
|
||||
0bLXgTnhCgmKoTexik4MkQKBgEZR14XGzlG81+SpOTeBG4F83ffJ4NfkTy395jf4
|
||||
iKubGa/Rcvl1VWU7DvZsyU9Dpb8J5Q+JWJPwdKoZ5UCWKPmO8nidSai4Z3/xY352
|
||||
4LTpHdzT5UlH7drGqftfck9FaUEFo3LxM2BAiijWlj1S3HVFO+Ku7JbRigCEQ0bw
|
||||
0HSnAoGBAJql8kup7D1orw8p97cXCIL2xvSK0IBc9ceEP6lDpOX1QxmGacbaD2pu
|
||||
jAKmvPf+TKLH6dYv8rHt4oBlgfYwHPpzF+qzYD4FK7G9f+Gye8X3oE+7Ujjusat3
|
||||
Q0LvwNNjY0M7gJyKvHq434CgKXhwL4oLTQrKTDDpNMQGtCx1BOTl
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.6.0",
|
||||
"version": "2025.6.1",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
||||
@@ -222,6 +222,9 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -242,6 +245,9 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -276,6 +282,9 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -6,10 +7,10 @@ import { BiometricsService } from "@bitwarden/key-management";
|
||||
* specifically for the main process.
|
||||
*/
|
||||
export abstract class DesktopBiometricsService extends BiometricsService {
|
||||
abstract setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise<void>;
|
||||
abstract setBiometricProtectedUnlockKeyForUser(
|
||||
userId: UserId,
|
||||
value: SymmetricCryptoKey,
|
||||
): Promise<void>;
|
||||
abstract deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void>;
|
||||
|
||||
abstract setupBiometrics(): Promise<void>;
|
||||
|
||||
abstract setClientKeyHalfForUser(userId: UserId, value: string | null): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
@@ -37,17 +38,12 @@ export class MainBiometricsIPCListener {
|
||||
}
|
||||
return await this.biometricService.setBiometricProtectedUnlockKeyForUser(
|
||||
message.userId as UserId,
|
||||
message.key,
|
||||
SymmetricCryptoKey.fromString(message.key),
|
||||
);
|
||||
case BiometricAction.RemoveKeyForUser:
|
||||
return await this.biometricService.deleteBiometricUnlockKeyForUser(
|
||||
message.userId as UserId,
|
||||
);
|
||||
case BiometricAction.SetClientKeyHalf:
|
||||
return await this.biometricService.setClientKeyHalfForUser(
|
||||
message.userId as UserId,
|
||||
message.key,
|
||||
);
|
||||
case BiometricAction.Setup:
|
||||
return await this.biometricService.setupBiometrics();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
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";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
BiometricsService,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
import { MainCryptoFunctionService } from "../../platform/main/main-crypto-function.service";
|
||||
|
||||
import { MainBiometricsService } from "./main-biometrics.service";
|
||||
import OsBiometricsServiceLinux from "./os-biometrics-linux.service";
|
||||
@@ -27,21 +28,25 @@ jest.mock("@bitwarden/desktop-napi", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const unlockKey = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
|
||||
describe("MainBiometricsService", function () {
|
||||
const i18nService = mock<I18nService>();
|
||||
const windowMain = mock<WindowMain>();
|
||||
const logService = mock<LogService>();
|
||||
const messagingService = mock<MessagingService>();
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
const cryptoFunctionService = mock<MainCryptoFunctionService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
||||
it("Should call the platformspecific methods", async () => {
|
||||
const sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
const mockService = mock<OsBiometricService>();
|
||||
@@ -57,9 +62,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
"win32",
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
const internalService = (sut as any).osBiometricsService;
|
||||
@@ -72,9 +78,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
"darwin",
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const internalService = (sut as any).osBiometricsService;
|
||||
expect(internalService).not.toBeNull();
|
||||
@@ -86,9 +93,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
"linux",
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
const internalService = (sut as any).osBiometricsService;
|
||||
@@ -106,9 +114,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
innerService = mock();
|
||||
@@ -131,9 +140,9 @@ describe("MainBiometricsService", function () {
|
||||
];
|
||||
|
||||
for (const [supportsBiometric, needsSetup, canAutoSetup, expected] of testCases) {
|
||||
innerService.osSupportsBiometric.mockResolvedValue(supportsBiometric as boolean);
|
||||
innerService.osBiometricsNeedsSetup.mockResolvedValue(needsSetup as boolean);
|
||||
innerService.osBiometricsCanAutoSetup.mockResolvedValue(canAutoSetup as boolean);
|
||||
innerService.supportsBiometrics.mockResolvedValue(supportsBiometric as boolean);
|
||||
innerService.needsSetup.mockResolvedValue(needsSetup as boolean);
|
||||
innerService.canAutoSetup.mockResolvedValue(canAutoSetup as boolean);
|
||||
|
||||
const actual = await sut.getBiometricsStatus();
|
||||
expect(actual).toBe(expected);
|
||||
@@ -175,12 +184,23 @@ describe("MainBiometricsService", function () {
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(
|
||||
requirePasswordOnStart as boolean,
|
||||
);
|
||||
(sut as any).clientKeyHalves = new Map();
|
||||
const userId = "test" as UserId;
|
||||
if (hasKeyHalf) {
|
||||
(sut as any).clientKeyHalves.set(userId, "test");
|
||||
if (!requirePasswordOnStart) {
|
||||
(sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest
|
||||
.fn()
|
||||
.mockResolvedValue(BiometricsStatus.Available);
|
||||
} else {
|
||||
if (hasKeyHalf) {
|
||||
(sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest
|
||||
.fn()
|
||||
.mockResolvedValue(BiometricsStatus.Available);
|
||||
} else {
|
||||
(sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest
|
||||
.fn()
|
||||
.mockResolvedValue(BiometricsStatus.UnlockNeeded);
|
||||
}
|
||||
}
|
||||
|
||||
const userId = "test" as UserId;
|
||||
const actual = await sut.getBiometricsStatusForUser(userId);
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
@@ -193,50 +213,17 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
|
||||
await sut.setupBiometrics();
|
||||
|
||||
expect(osBiometricsService.osBiometricsSetup).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setClientKeyHalfForUser", () => {
|
||||
let sut: MainBiometricsService;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
);
|
||||
});
|
||||
|
||||
it("should set the client key half for the user", async () => {
|
||||
const userId = "test" as UserId;
|
||||
const keyHalf = "testKeyHalf";
|
||||
|
||||
await sut.setClientKeyHalfForUser(userId, keyHalf);
|
||||
|
||||
expect((sut as any).clientKeyHalves.has(userId)).toBe(true);
|
||||
expect((sut as any).clientKeyHalves.get(userId)).toBe(keyHalf);
|
||||
});
|
||||
|
||||
it("should reset the client key half for the user", async () => {
|
||||
const userId = "test" as UserId;
|
||||
|
||||
await sut.setClientKeyHalfForUser(userId, null);
|
||||
|
||||
expect((sut as any).clientKeyHalves.has(userId)).toBe(true);
|
||||
expect((sut as any).clientKeyHalves.get(userId)).toBe(null);
|
||||
expect(osBiometricsService.runSetup).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -246,9 +233,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
@@ -268,9 +256,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
@@ -278,34 +267,24 @@ describe("MainBiometricsService", function () {
|
||||
|
||||
it("should return null if no biometric key is returned ", async () => {
|
||||
const userId = "test" as UserId;
|
||||
(sut as any).clientKeyHalves.set(userId, "testKeyHalf");
|
||||
|
||||
osBiometricsService.getBiometricKey.mockResolvedValue(null);
|
||||
const userKey = await sut.unlockWithBiometricsForUser(userId);
|
||||
|
||||
expect(userKey).toBeNull();
|
||||
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
"testKeyHalf",
|
||||
);
|
||||
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
it("should return the biometric key if a valid key is returned", async () => {
|
||||
const userId = "test" as UserId;
|
||||
(sut as any).clientKeyHalves.set(userId, "testKeyHalf");
|
||||
const biometricKey = Utils.fromBufferToB64(new Uint8Array(64));
|
||||
const biometricKey = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
osBiometricsService.getBiometricKey.mockResolvedValue(biometricKey);
|
||||
|
||||
const userKey = await sut.unlockWithBiometricsForUser(userId);
|
||||
|
||||
expect(userKey).not.toBeNull();
|
||||
expect(userKey!.keyB64).toBe(biometricKey);
|
||||
expect(userKey!.keyB64).toBe(biometricKey.toBase64());
|
||||
expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64);
|
||||
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
"testKeyHalf",
|
||||
);
|
||||
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -318,37 +297,21 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
});
|
||||
|
||||
it("should throw an error if no client key half is provided", async () => {
|
||||
const userId = "test" as UserId;
|
||||
const unlockKey = "testUnlockKey";
|
||||
|
||||
await expect(sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey)).rejects.toThrow(
|
||||
"No client key half provided for user",
|
||||
);
|
||||
});
|
||||
|
||||
it("should call the platform specific setBiometricKey method", async () => {
|
||||
const userId = "test" as UserId;
|
||||
const unlockKey = "testUnlockKey";
|
||||
|
||||
(sut as any).clientKeyHalves.set(userId, "testKeyHalf");
|
||||
|
||||
await sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey);
|
||||
|
||||
expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
unlockKey,
|
||||
"testKeyHalf",
|
||||
);
|
||||
expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith(userId, unlockKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -358,9 +321,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
@@ -369,10 +333,7 @@ describe("MainBiometricsService", function () {
|
||||
|
||||
await sut.deleteBiometricUnlockKeyForUser(userId);
|
||||
|
||||
expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
);
|
||||
expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -384,9 +345,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -413,9 +375,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
const shouldAutoPrompt = await sut.getShouldAutopromptNow();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
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";
|
||||
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";
|
||||
@@ -13,16 +14,16 @@ import { OsBiometricService } from "./os-biometrics.service";
|
||||
|
||||
export class MainBiometricsService extends DesktopBiometricsService {
|
||||
private osBiometricsService: OsBiometricService;
|
||||
private clientKeyHalves = new Map<string, string | null>();
|
||||
private shouldAutoPrompt = true;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private logService: LogService,
|
||||
private messagingService: MessagingService,
|
||||
private platform: NodeJS.Platform,
|
||||
platform: NodeJS.Platform,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
) {
|
||||
super();
|
||||
if (platform === "win32") {
|
||||
@@ -32,6 +33,9 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
this.i18nService,
|
||||
this.windowMain,
|
||||
this.logService,
|
||||
this.biometricStateService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
);
|
||||
} else if (platform === "darwin") {
|
||||
// eslint-disable-next-line
|
||||
@@ -40,7 +44,11 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
} else if (platform === "linux") {
|
||||
// eslint-disable-next-line
|
||||
const OsBiometricsServiceLinux = require("./os-biometrics-linux.service").default;
|
||||
this.osBiometricsService = new OsBiometricsServiceLinux(this.i18nService, this.windowMain);
|
||||
this.osBiometricsService = new OsBiometricsServiceLinux(
|
||||
this.biometricStateService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unsupported platform");
|
||||
}
|
||||
@@ -55,11 +63,11 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
* @returns the status of the biometrics of the platform
|
||||
*/
|
||||
async getBiometricsStatus(): Promise<BiometricsStatus> {
|
||||
if (!(await this.osBiometricsService.osSupportsBiometric())) {
|
||||
if (!(await this.osBiometricsService.supportsBiometrics())) {
|
||||
return BiometricsStatus.HardwareUnavailable;
|
||||
} else {
|
||||
if (await this.osBiometricsService.osBiometricsNeedsSetup()) {
|
||||
if (await this.osBiometricsService.osBiometricsCanAutoSetup()) {
|
||||
if (await this.osBiometricsService.needsSetup()) {
|
||||
if (await this.osBiometricsService.canAutoSetup()) {
|
||||
return BiometricsStatus.AutoSetupNeeded;
|
||||
} else {
|
||||
return BiometricsStatus.ManualSetupNeeded;
|
||||
@@ -80,20 +88,12 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
if (!(await this.biometricStateService.getBiometricUnlockEnabled(userId))) {
|
||||
return BiometricsStatus.NotEnabledLocally;
|
||||
}
|
||||
|
||||
const platformStatus = await this.getBiometricsStatus();
|
||||
if (!(platformStatus === BiometricsStatus.Available)) {
|
||||
return platformStatus;
|
||||
}
|
||||
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
const clientKeyHalfB64 = this.clientKeyHalves.get(userId);
|
||||
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
|
||||
if (!clientKeyHalfSatisfied) {
|
||||
return BiometricsStatus.UnlockNeeded;
|
||||
}
|
||||
|
||||
return BiometricsStatus.Available;
|
||||
return await this.osBiometricsService.getBiometricsFirstUnlockStatusForUser(userId);
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
@@ -101,11 +101,7 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
}
|
||||
|
||||
async setupBiometrics(): Promise<void> {
|
||||
return await this.osBiometricsService.osBiometricsSetup();
|
||||
}
|
||||
|
||||
async setClientKeyHalfForUser(userId: UserId, value: string | null): Promise<void> {
|
||||
this.clientKeyHalves.set(userId, value);
|
||||
return await this.osBiometricsService.runSetup();
|
||||
}
|
||||
|
||||
async authenticateWithBiometrics(): Promise<boolean> {
|
||||
@@ -113,43 +109,23 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
}
|
||||
|
||||
async unlockWithBiometricsForUser(userId: UserId): Promise<UserKey | null> {
|
||||
const biometricKey = await this.osBiometricsService.getBiometricKey(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
this.clientKeyHalves.get(userId) ?? undefined,
|
||||
);
|
||||
if (biometricKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SymmetricCryptoKey.fromString(biometricKey) as UserKey;
|
||||
return (await this.osBiometricsService.getBiometricKey(userId)) as UserKey;
|
||||
}
|
||||
|
||||
async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise<void> {
|
||||
const service = "Bitwarden_biometric";
|
||||
const storageKey = `${userId}_user_biometric`;
|
||||
if (!this.clientKeyHalves.has(userId)) {
|
||||
throw new Error("No client key half provided for user");
|
||||
}
|
||||
|
||||
return await this.osBiometricsService.setBiometricKey(
|
||||
service,
|
||||
storageKey,
|
||||
value,
|
||||
this.clientKeyHalves.get(userId) ?? undefined,
|
||||
);
|
||||
async setBiometricProtectedUnlockKeyForUser(
|
||||
userId: UserId,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<void> {
|
||||
return await this.osBiometricsService.setBiometricKey(userId, key);
|
||||
}
|
||||
|
||||
async deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void> {
|
||||
return await this.osBiometricsService.deleteBiometricKey(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
);
|
||||
return await this.osBiometricsService.deleteBiometricKey(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to auto-prompt the user for biometric unlock; this can be used to prevent auto-prompting being initiated by a process reload.
|
||||
* Reasons for enabling auto prompt include: Starting the app, un-minimizing the app, manually account switching
|
||||
* Reasons for enabling auto-prompt include: Starting the app, un-minimizing the app, manually account switching
|
||||
* @param value Whether to auto-prompt the user for biometric unlock
|
||||
*/
|
||||
async setShouldAutopromptNow(value: boolean): Promise<void> {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { spawn } from "child_process";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
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 { UserId } from "@bitwarden/common/types/guid";
|
||||
import { biometrics, passwords } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
import { isFlatpak, isLinux, isSnapStore } from "../../utils";
|
||||
|
||||
import { OsBiometricService } from "./os-biometrics.service";
|
||||
@@ -28,59 +32,62 @@ const polkitPolicy = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
const policyFileName = "com.bitwarden.Bitwarden.policy";
|
||||
const policyPath = "/usr/share/polkit-1/actions/";
|
||||
|
||||
const SERVICE = "Bitwarden_biometric";
|
||||
function getLookupKeyForUser(userId: UserId): string {
|
||||
return `${userId}_user_biometric`;
|
||||
}
|
||||
|
||||
export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
constructor(
|
||||
private i18nservice: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
) {}
|
||||
private _iv: string | null = null;
|
||||
// Use getKeyMaterial helper instead of direct access
|
||||
private _osKeyHalf: string | null = null;
|
||||
private clientKeyHalves = new Map<UserId, Uint8Array | null>();
|
||||
|
||||
async setBiometricKey(
|
||||
service: string,
|
||||
key: string,
|
||||
value: string,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): Promise<void> {
|
||||
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
const clientKeyPartB64 = Utils.fromBufferToB64(
|
||||
await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key),
|
||||
);
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
await biometrics.setBiometricSecret(
|
||||
service,
|
||||
key,
|
||||
value,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
key.toBase64(),
|
||||
storageDetails.key_material,
|
||||
storageDetails.ivB64,
|
||||
);
|
||||
}
|
||||
async deleteBiometricKey(service: string, key: string): Promise<void> {
|
||||
await passwords.deletePassword(service, key);
|
||||
async deleteBiometricKey(userId: UserId): Promise<void> {
|
||||
await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId));
|
||||
}
|
||||
|
||||
async getBiometricKey(
|
||||
service: string,
|
||||
storageKey: string,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): Promise<string | null> {
|
||||
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
|
||||
const success = await this.authenticateBiometric();
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Biometric authentication failed");
|
||||
}
|
||||
|
||||
const value = await passwords.getPassword(service, storageKey);
|
||||
const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
|
||||
|
||||
if (value == null || value == "") {
|
||||
return null;
|
||||
} else {
|
||||
const clientKeyHalf = this.clientKeyHalves.get(userId);
|
||||
const clientKeyPartB64 = Utils.fromBufferToB64(clientKeyHalf);
|
||||
const encValue = new EncString(value);
|
||||
this.setIv(encValue.iv);
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
const storedValue = await biometrics.getBiometricSecret(
|
||||
service,
|
||||
storageKey,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
storageDetails.key_material,
|
||||
);
|
||||
return storedValue;
|
||||
return SymmetricCryptoKey.fromString(storedValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +96,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return await biometrics.prompt(hwnd, "");
|
||||
}
|
||||
|
||||
async osSupportsBiometric(): Promise<boolean> {
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
// We assume all linux distros have some polkit implementation
|
||||
// that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup.
|
||||
// Snap does not have access at the moment to polkit
|
||||
@@ -99,7 +106,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return await passwords.isAvailable();
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup(): Promise<boolean> {
|
||||
async needsSetup(): Promise<boolean> {
|
||||
if (isSnapStore()) {
|
||||
return false;
|
||||
}
|
||||
@@ -108,7 +115,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return !(await biometrics.available());
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
async canAutoSetup(): Promise<boolean> {
|
||||
// We cannot auto setup on snap or flatpak since the filesystem is sandboxed.
|
||||
// The user needs to manually set up the polkit policy outside of the sandbox
|
||||
// since we allow access to polkit via dbus for the sandboxed clients, the authentication works from
|
||||
@@ -116,7 +123,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return isLinux() && !isSnapStore() && !isFlatpak();
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {
|
||||
async runSetup(): Promise<void> {
|
||||
const process = spawn("pkexec", [
|
||||
"bash",
|
||||
"-c",
|
||||
@@ -165,4 +172,46 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
ivB64: this._iv,
|
||||
};
|
||||
}
|
||||
|
||||
private async getOrCreateBiometricEncryptionClientKeyHalf(
|
||||
userId: UserId,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array | null> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.clientKeyHalves.has(userId)) {
|
||||
return this.clientKeyHalves.get(userId) || null;
|
||||
}
|
||||
|
||||
// Retrieve existing key half if it exists
|
||||
let clientKeyHalf: Uint8Array | null = null;
|
||||
const encryptedClientKeyHalf =
|
||||
await this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||
if (encryptedClientKeyHalf != null) {
|
||||
clientKeyHalf = await this.encryptService.decryptBytes(encryptedClientKeyHalf, key);
|
||||
}
|
||||
if (clientKeyHalf == null) {
|
||||
// Set a key half if it doesn't exist
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
const encKey = await this.encryptService.encryptBytes(keyBytes, key);
|
||||
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
||||
}
|
||||
|
||||
this.clientKeyHalves.set(userId, clientKeyHalf);
|
||||
|
||||
return clientKeyHalf;
|
||||
}
|
||||
|
||||
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
const clientKeyHalfB64 = this.clientKeyHalves.get(userId);
|
||||
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
|
||||
if (!clientKeyHalfSatisfied) {
|
||||
return BiometricsStatus.UnlockNeeded;
|
||||
}
|
||||
return BiometricsStatus.Available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { systemPreferences } from "electron";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { passwords } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus } from "@bitwarden/key-management";
|
||||
|
||||
import { OsBiometricService } from "./os-biometrics.service";
|
||||
|
||||
const SERVICE = "Bitwarden_biometric";
|
||||
function getLookupKeyForUser(userId: UserId): string {
|
||||
return `${userId}_user_biometric`;
|
||||
}
|
||||
|
||||
export default class OsBiometricsServiceMac implements OsBiometricService {
|
||||
constructor(private i18nservice: I18nService) {}
|
||||
|
||||
async osSupportsBiometric(): Promise<boolean> {
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
return systemPreferences.canPromptTouchID();
|
||||
}
|
||||
|
||||
@@ -21,44 +29,52 @@ export default class OsBiometricsServiceMac implements OsBiometricService {
|
||||
}
|
||||
}
|
||||
|
||||
async getBiometricKey(service: string, key: string): Promise<string | null> {
|
||||
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
|
||||
const success = await this.authenticateBiometric();
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Biometric authentication failed");
|
||||
}
|
||||
const keyB64 = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
|
||||
if (keyB64 == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await passwords.getPassword(service, key);
|
||||
return SymmetricCryptoKey.fromString(keyB64);
|
||||
}
|
||||
|
||||
async setBiometricKey(service: string, key: string, value: string): Promise<void> {
|
||||
if (await this.valueUpToDate(service, key, value)) {
|
||||
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
if (await this.valueUpToDate(userId, key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await passwords.setPassword(service, key, value);
|
||||
return await passwords.setPassword(SERVICE, getLookupKeyForUser(userId), key.toBase64());
|
||||
}
|
||||
|
||||
async deleteBiometricKey(service: string, key: string): Promise<void> {
|
||||
return await passwords.deletePassword(service, key);
|
||||
async deleteBiometricKey(user: UserId): Promise<void> {
|
||||
return await passwords.deletePassword(SERVICE, getLookupKeyForUser(user));
|
||||
}
|
||||
|
||||
private async valueUpToDate(service: string, key: string, value: string): Promise<boolean> {
|
||||
private async valueUpToDate(user: UserId, key: SymmetricCryptoKey): Promise<boolean> {
|
||||
try {
|
||||
const existing = await passwords.getPassword(service, key);
|
||||
return existing === value;
|
||||
const existing = await passwords.getPassword(SERVICE, getLookupKeyForUser(user));
|
||||
return existing === key.toBase64();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup() {
|
||||
async needsSetup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
async canAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {}
|
||||
async runSetup(): Promise<void> {}
|
||||
|
||||
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
|
||||
return BiometricsStatus.Available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
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 { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import OsBiometricsServiceWindows from "./os-biometrics-windows.service";
|
||||
|
||||
jest.mock("@bitwarden/desktop-napi", () => ({
|
||||
biometrics: {
|
||||
available: jest.fn(),
|
||||
setBiometricSecret: jest.fn(),
|
||||
getBiometricSecret: jest.fn(),
|
||||
deriveKeyMaterial: jest.fn(),
|
||||
prompt: jest.fn(),
|
||||
},
|
||||
passwords: {
|
||||
getPassword: jest.fn(),
|
||||
deletePassword: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("OsBiometricsServiceWindows", () => {
|
||||
let service: OsBiometricsServiceWindows;
|
||||
let biometricStateService: BiometricStateService;
|
||||
|
||||
beforeEach(() => {
|
||||
const i18nService = mock<I18nService>();
|
||||
const logService = mock<LogService>();
|
||||
biometricStateService = mock<BiometricStateService>();
|
||||
const encryptionService = mock<EncryptService>();
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
service = new OsBiometricsServiceWindows(
|
||||
i18nService,
|
||||
null,
|
||||
logService,
|
||||
biometricStateService,
|
||||
encryptionService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("getBiometricsFirstUnlockStatusForUser", () => {
|
||||
const userId = "test-user-id" as UserId;
|
||||
it("should return Available when requirePasswordOnRestart is false", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(false);
|
||||
const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
|
||||
expect(result).toBe(BiometricsStatus.Available);
|
||||
});
|
||||
it("should return Available when requirePasswordOnRestart is true and client key half is set", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
|
||||
(service as any).clientKeyHalves = new Map<string, Uint8Array>();
|
||||
(service as any).clientKeyHalves.set(userId, new Uint8Array([1, 2, 3, 4]));
|
||||
const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
|
||||
expect(result).toBe(BiometricsStatus.Available);
|
||||
});
|
||||
it("should return UnlockNeeded when requirePasswordOnRestart is true and client key half is not set", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
|
||||
(service as any).clientKeyHalves = new Map<string, Uint8Array>();
|
||||
const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
|
||||
expect(result).toBe(BiometricsStatus.UnlockNeeded);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrCreateBiometricEncryptionClientKeyHalf", () => {
|
||||
const userId = "test-user-id" as UserId;
|
||||
const key = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
let encryptionService: EncryptService;
|
||||
let cryptoFunctionService: CryptoFunctionService;
|
||||
|
||||
beforeEach(() => {
|
||||
encryptionService = mock<EncryptService>();
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
service = new OsBiometricsServiceWindows(
|
||||
mock<I18nService>(),
|
||||
null,
|
||||
mock<LogService>(),
|
||||
biometricStateService,
|
||||
encryptionService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return null if getRequirePasswordOnRestart is false", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(false);
|
||||
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return cached key half if already present", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
|
||||
const cachedKeyHalf = new Uint8Array([10, 20, 30]);
|
||||
(service as any).clientKeyHalves.set(userId.toString(), cachedKeyHalf);
|
||||
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
expect(result).toBe(cachedKeyHalf);
|
||||
});
|
||||
|
||||
it("should decrypt and return existing encrypted client key half", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
|
||||
biometricStateService.getEncryptedClientKeyHalf = jest
|
||||
.fn()
|
||||
.mockResolvedValue(new Uint8Array([1, 2, 3]));
|
||||
const decrypted = new Uint8Array([4, 5, 6]);
|
||||
encryptionService.decryptBytes = jest.fn().mockResolvedValue(decrypted);
|
||||
|
||||
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
|
||||
expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(userId);
|
||||
expect(encryptionService.decryptBytes).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]), key);
|
||||
expect(result).toEqual(decrypted);
|
||||
expect((service as any).clientKeyHalves.get(userId.toString())).toEqual(decrypted);
|
||||
});
|
||||
|
||||
it("should generate, encrypt, store, and cache a new key half if none exists", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
|
||||
biometricStateService.getEncryptedClientKeyHalf = jest.fn().mockResolvedValue(null);
|
||||
const randomBytes = new Uint8Array([7, 8, 9]);
|
||||
cryptoFunctionService.randomBytes = jest.fn().mockResolvedValue(randomBytes);
|
||||
const encrypted = new Uint8Array([10, 11, 12]);
|
||||
encryptionService.encryptBytes = jest.fn().mockResolvedValue(encrypted);
|
||||
biometricStateService.setEncryptedClientKeyHalf = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
|
||||
expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32);
|
||||
expect(encryptionService.encryptBytes).toHaveBeenCalledWith(randomBytes, key);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith(
|
||||
encrypted,
|
||||
userId,
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
expect((service as any).clientKeyHalves.get(userId.toString())).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,14 @@
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
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 { UserId } from "@bitwarden/common/types/guid";
|
||||
import { biometrics, passwords } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
|
||||
@@ -13,87 +17,107 @@ import { OsBiometricService } from "./os-biometrics.service";
|
||||
const KEY_WITNESS_SUFFIX = "_witness";
|
||||
const WITNESS_VALUE = "known key";
|
||||
|
||||
const SERVICE = "Bitwarden_biometric";
|
||||
function getLookupKeyForUser(userId: UserId): string {
|
||||
return `${userId}_user_biometric`;
|
||||
}
|
||||
|
||||
export default class OsBiometricsServiceWindows implements OsBiometricService {
|
||||
// Use set helper method instead of direct access
|
||||
private _iv: string | null = null;
|
||||
// Use getKeyMaterial helper instead of direct access
|
||||
private _osKeyHalf: string | null = null;
|
||||
private clientKeyHalves = new Map<UserId, Uint8Array>();
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private logService: LogService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
) {}
|
||||
|
||||
async osSupportsBiometric(): Promise<boolean> {
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
return await biometrics.available();
|
||||
}
|
||||
|
||||
async getBiometricKey(
|
||||
service: string,
|
||||
storageKey: string,
|
||||
clientKeyHalfB64: string,
|
||||
): Promise<string | null> {
|
||||
const value = await passwords.getPassword(service, storageKey);
|
||||
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
|
||||
const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
|
||||
let clientKeyHalfB64: string | null = null;
|
||||
if (this.clientKeyHalves.has(userId)) {
|
||||
clientKeyHalfB64 = Utils.fromBufferToB64(this.clientKeyHalves.get(userId));
|
||||
}
|
||||
|
||||
if (value == null || value == "") {
|
||||
return null;
|
||||
} else if (!EncString.isSerializedEncString(value)) {
|
||||
// Update to format encrypted with client key half
|
||||
const storageDetails = await this.getStorageDetails({
|
||||
clientKeyHalfB64,
|
||||
clientKeyHalfB64: clientKeyHalfB64,
|
||||
});
|
||||
|
||||
await biometrics.setBiometricSecret(
|
||||
service,
|
||||
storageKey,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
value,
|
||||
storageDetails.key_material,
|
||||
storageDetails.ivB64,
|
||||
);
|
||||
return value;
|
||||
return SymmetricCryptoKey.fromString(value);
|
||||
} else {
|
||||
const encValue = new EncString(value);
|
||||
this.setIv(encValue.iv);
|
||||
const storageDetails = await this.getStorageDetails({
|
||||
clientKeyHalfB64,
|
||||
clientKeyHalfB64: clientKeyHalfB64,
|
||||
});
|
||||
return await biometrics.getBiometricSecret(service, storageKey, storageDetails.key_material);
|
||||
return SymmetricCryptoKey.fromString(
|
||||
await biometrics.getBiometricSecret(
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
storageDetails.key_material,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async setBiometricKey(
|
||||
service: string,
|
||||
storageKey: string,
|
||||
value: string,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): Promise<void> {
|
||||
const parsedValue = SymmetricCryptoKey.fromString(value);
|
||||
if (await this.valueUpToDate({ value: parsedValue, clientKeyPartB64, service, storageKey })) {
|
||||
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
|
||||
if (
|
||||
await this.valueUpToDate({
|
||||
value: key,
|
||||
clientKeyPartB64: Utils.fromBufferToB64(clientKeyHalf),
|
||||
service: SERVICE,
|
||||
storageKey: getLookupKeyForUser(userId),
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
const storageDetails = await this.getStorageDetails({
|
||||
clientKeyHalfB64: Utils.fromBufferToB64(clientKeyHalf),
|
||||
});
|
||||
const storedValue = await biometrics.setBiometricSecret(
|
||||
service,
|
||||
storageKey,
|
||||
value,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
key.toBase64(),
|
||||
storageDetails.key_material,
|
||||
storageDetails.ivB64,
|
||||
);
|
||||
const parsedStoredValue = new EncString(storedValue);
|
||||
await this.storeValueWitness(
|
||||
parsedValue,
|
||||
key,
|
||||
parsedStoredValue,
|
||||
service,
|
||||
storageKey,
|
||||
clientKeyPartB64,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
Utils.fromBufferToB64(clientKeyHalf),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteBiometricKey(service: string, key: string): Promise<void> {
|
||||
await passwords.deletePassword(service, key);
|
||||
await passwords.deletePassword(service, key + KEY_WITNESS_SUFFIX);
|
||||
async deleteBiometricKey(userId: UserId): Promise<void> {
|
||||
await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId));
|
||||
await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId) + KEY_WITNESS_SUFFIX);
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
@@ -240,13 +264,58 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
|
||||
return result;
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup() {
|
||||
async needsSetup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
async canAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {}
|
||||
async runSetup(): Promise<void> {}
|
||||
|
||||
async getOrCreateBiometricEncryptionClientKeyHalf(
|
||||
userId: UserId,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array | null> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.clientKeyHalves.has(userId)) {
|
||||
return this.clientKeyHalves.get(userId);
|
||||
}
|
||||
|
||||
// Retrieve existing key half if it exists
|
||||
let clientKeyHalf: Uint8Array | null = null;
|
||||
const encryptedClientKeyHalf =
|
||||
await this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||
if (encryptedClientKeyHalf != null) {
|
||||
clientKeyHalf = await this.encryptService.decryptBytes(encryptedClientKeyHalf, key);
|
||||
}
|
||||
if (clientKeyHalf == null) {
|
||||
// Set a key half if it doesn't exist
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
const encKey = await this.encryptService.encryptBytes(keyBytes, key);
|
||||
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
||||
}
|
||||
|
||||
this.clientKeyHalves.set(userId, clientKeyHalf);
|
||||
|
||||
return clientKeyHalf;
|
||||
}
|
||||
|
||||
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return BiometricsStatus.Available;
|
||||
}
|
||||
|
||||
if (this.clientKeyHalves.has(userId)) {
|
||||
return BiometricsStatus.Available;
|
||||
} else {
|
||||
return BiometricsStatus.UnlockNeeded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsStatus } from "@bitwarden/key-management";
|
||||
|
||||
export interface OsBiometricService {
|
||||
osSupportsBiometric(): Promise<boolean>;
|
||||
supportsBiometrics(): Promise<boolean>;
|
||||
/**
|
||||
* Check whether support for biometric unlock requires setup. This can be automatic or manual.
|
||||
*
|
||||
* @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
|
||||
*/
|
||||
osBiometricsNeedsSetup: () => Promise<boolean>;
|
||||
needsSetup(): Promise<boolean>;
|
||||
/**
|
||||
* Check whether biometrics can be automatically setup, or requires user interaction.
|
||||
*
|
||||
* @returns true if biometrics support can be automatically setup, false if it requires user interaction.
|
||||
*/
|
||||
osBiometricsCanAutoSetup: () => Promise<boolean>;
|
||||
canAutoSetup(): Promise<boolean>;
|
||||
/**
|
||||
* Starts automatic biometric setup, which places the required configuration files / changes the required settings.
|
||||
*/
|
||||
osBiometricsSetup: () => Promise<void>;
|
||||
runSetup(): Promise<void>;
|
||||
authenticateBiometric(): Promise<boolean>;
|
||||
getBiometricKey(
|
||||
service: string,
|
||||
key: string,
|
||||
clientKeyHalfB64: string | undefined,
|
||||
): Promise<string | null>;
|
||||
setBiometricKey(
|
||||
service: string,
|
||||
key: string,
|
||||
value: string,
|
||||
clientKeyHalfB64: string | undefined,
|
||||
): Promise<void>;
|
||||
deleteBiometricKey(service: string, key: string): Promise<void>;
|
||||
getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null>;
|
||||
setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void>;
|
||||
deleteBiometricKey(userId: UserId): Promise<void>;
|
||||
getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus>;
|
||||
}
|
||||
|
||||
@@ -34,8 +34,14 @@ export class RendererBiometricsService extends DesktopBiometricsService {
|
||||
return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id);
|
||||
}
|
||||
|
||||
async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise<void> {
|
||||
return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser(userId, value);
|
||||
async setBiometricProtectedUnlockKeyForUser(
|
||||
userId: UserId,
|
||||
value: SymmetricCryptoKey,
|
||||
): Promise<void> {
|
||||
return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser(
|
||||
userId,
|
||||
value.toBase64(),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void> {
|
||||
@@ -46,10 +52,6 @@ export class RendererBiometricsService extends DesktopBiometricsService {
|
||||
return await ipc.keyManagement.biometric.setupBiometrics();
|
||||
}
|
||||
|
||||
async setClientKeyHalfForUser(userId: UserId, value: string | null): Promise<void> {
|
||||
return await ipc.keyManagement.biometric.setClientKeyHalf(userId, value);
|
||||
}
|
||||
|
||||
async getShouldAutopromptNow(): Promise<boolean> {
|
||||
return await ipc.keyManagement.biometric.getShouldAutoprompt();
|
||||
}
|
||||
|
||||
@@ -9,14 +9,11 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { BiometricStateService, KdfConfigService } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
makeEncString,
|
||||
makeStaticByteArray,
|
||||
makeSymmetricCryptoKey,
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
@@ -80,7 +77,6 @@ describe("ElectronKeyService", () => {
|
||||
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setClientKeyHalfForUser).not.toHaveBeenCalled();
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
|
||||
@@ -96,14 +92,12 @@ describe("ElectronKeyService", () => {
|
||||
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(mockUserId, null);
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey.keyB64,
|
||||
userKey,
|
||||
);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
|
||||
expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
|
||||
describe("require password on start enabled", () => {
|
||||
@@ -111,73 +105,11 @@ describe("ElectronKeyService", () => {
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("sets new biometric client key half and biometric unlock key when no biometric client key half stored", async () => {
|
||||
const clientKeyHalfBytes = makeStaticByteArray(32);
|
||||
const clientKeyHalf = Utils.fromBufferToUtf8(clientKeyHalfBytes);
|
||||
const encryptedClientKeyHalf = makeEncString();
|
||||
biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(null);
|
||||
cryptoFunctionService.randomBytes.mockResolvedValue(
|
||||
clientKeyHalfBytes.buffer as CsprngArray,
|
||||
);
|
||||
encryptService.encryptString.mockResolvedValue(encryptedClientKeyHalf);
|
||||
|
||||
it("sets biometric key", async () => {
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
clientKeyHalf,
|
||||
);
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey.keyB64,
|
||||
);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith(
|
||||
encryptedClientKeyHalf,
|
||||
mockUserId,
|
||||
);
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32);
|
||||
expect(encryptService.encryptString).toHaveBeenCalledWith(clientKeyHalf, userKey);
|
||||
});
|
||||
|
||||
it("sets decrypted biometric client key half and biometric unlock key when existing biometric client key half stored", async () => {
|
||||
const encryptedClientKeyHalf = makeEncString();
|
||||
const clientKeyHalf = Utils.fromBufferToUtf8(makeStaticByteArray(32));
|
||||
biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(
|
||||
encryptedClientKeyHalf,
|
||||
);
|
||||
encryptService.decryptString.mockResolvedValue(clientKeyHalf);
|
||||
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
clientKeyHalf,
|
||||
);
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey.keyB64,
|
||||
);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(
|
||||
encryptedClientKeyHalf,
|
||||
userKey,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,9 +8,7 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { CsprngString } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
@@ -77,10 +75,7 @@ export class ElectronKeyService extends DefaultKeyService {
|
||||
}
|
||||
|
||||
private async storeBiometricsProtectedUserKey(userKey: UserKey, userId: UserId): Promise<void> {
|
||||
// May resolve to null, in which case no client key have is required
|
||||
const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userKey, userId);
|
||||
await this.biometricService.setClientKeyHalfForUser(userId, clientEncKeyHalf);
|
||||
await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey.keyB64);
|
||||
await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey);
|
||||
}
|
||||
|
||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId): Promise<boolean> {
|
||||
@@ -91,34 +86,4 @@ export class ElectronKeyService extends DefaultKeyService {
|
||||
await this.biometricService.deleteBiometricUnlockKeyForUser(userId);
|
||||
await super.clearAllStoredUserKeys(userId);
|
||||
}
|
||||
|
||||
private async getBiometricEncryptionClientKeyHalf(
|
||||
userKey: UserKey,
|
||||
userId: UserId,
|
||||
): Promise<CsprngString | null> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Retrieve existing key half if it exists
|
||||
let clientKeyHalf: CsprngString | null = null;
|
||||
const encryptedClientKeyHalf =
|
||||
await this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||
if (encryptedClientKeyHalf != null) {
|
||||
clientKeyHalf = (await this.encryptService.decryptString(
|
||||
encryptedClientKeyHalf,
|
||||
userKey,
|
||||
)) as CsprngString;
|
||||
}
|
||||
if (clientKeyHalf == null) {
|
||||
// Set a key half if it doesn't exist
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
clientKeyHalf = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
|
||||
const encKey = await this.encryptService.encryptString(clientKeyHalf, userKey);
|
||||
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
||||
}
|
||||
|
||||
return clientKeyHalf;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,13 @@ const biometric = {
|
||||
action: BiometricAction.GetStatusForUser,
|
||||
userId: userId,
|
||||
} satisfies BiometricMessage),
|
||||
setBiometricProtectedUnlockKeyForUser: (userId: string, value: string): Promise<void> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
setBiometricProtectedUnlockKeyForUser: (userId: string, keyB64: string): Promise<void> => {
|
||||
return ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.SetKeyForUser,
|
||||
userId: userId,
|
||||
key: value,
|
||||
} satisfies BiometricMessage),
|
||||
key: keyB64,
|
||||
} satisfies BiometricMessage);
|
||||
},
|
||||
deleteBiometricUnlockKeyForUser: (userId: string): Promise<void> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.RemoveKeyForUser,
|
||||
@@ -40,12 +41,6 @@ const biometric = {
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.Setup,
|
||||
} satisfies BiometricMessage),
|
||||
setClientKeyHalf: (userId: string, value: string | null): Promise<void> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.SetClientKeyHalf,
|
||||
userId: userId,
|
||||
key: value,
|
||||
} satisfies BiometricMessage),
|
||||
getShouldAutoprompt: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.GetShouldAutoprompt,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Subject, firstValueFrom } from "rxjs";
|
||||
import { SsoUrlService } from "@bitwarden/auth/common";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Message, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- For dependency creation
|
||||
@@ -187,14 +188,19 @@ export class Main {
|
||||
|
||||
this.desktopSettingsService = new DesktopSettingsService(stateProvider);
|
||||
const biometricStateService = new DefaultBiometricStateService(stateProvider);
|
||||
|
||||
const encryptService = new EncryptServiceImplementation(
|
||||
this.mainCryptoFunctionService,
|
||||
this.logService,
|
||||
true,
|
||||
);
|
||||
this.biometricsService = new MainBiometricsService(
|
||||
this.i18nService,
|
||||
this.windowMain,
|
||||
this.logService,
|
||||
this.messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
this.mainCryptoFunctionService,
|
||||
);
|
||||
|
||||
this.windowMain = new WindowMain(
|
||||
|
||||
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.6.0",
|
||||
"version": "2025.6.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.6.0",
|
||||
"version": "2025.6.1",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/desktop-napi": "file:../desktop_native/napi"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/desktop",
|
||||
"productName": "Bitwarden",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.6.0",
|
||||
"version": "2025.6.1",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@@ -9,8 +9,6 @@ export enum BiometricAction {
|
||||
SetKeyForUser = "setKeyForUser",
|
||||
RemoveKeyForUser = "removeKeyForUser",
|
||||
|
||||
SetClientKeyHalf = "setClientKeyHalf",
|
||||
|
||||
Setup = "setup",
|
||||
|
||||
GetShouldAutoprompt = "getShouldAutoprompt",
|
||||
@@ -18,21 +16,13 @@ export enum BiometricAction {
|
||||
}
|
||||
|
||||
export type BiometricMessage =
|
||||
| {
|
||||
action: BiometricAction.SetClientKeyHalf;
|
||||
userId: string;
|
||||
key: string | null;
|
||||
}
|
||||
| {
|
||||
action: BiometricAction.SetKeyForUser;
|
||||
userId: string;
|
||||
key: string;
|
||||
}
|
||||
| {
|
||||
action: Exclude<
|
||||
BiometricAction,
|
||||
BiometricAction.SetClientKeyHalf | BiometricAction.SetKeyForUser
|
||||
>;
|
||||
action: Exclude<BiometricAction, BiometricAction.SetKeyForUser>;
|
||||
userId?: string;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2025.6.0",
|
||||
"version": "2025.6.1",
|
||||
"scripts": {
|
||||
"build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
"build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<p *ngIf="!dataSource.filteredData.length">{{ "noGroupsInList" | i18n }}</p>
|
||||
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
||||
from overflowing the <main> element. -->
|
||||
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<bit-table *ngIf="dataSource.filteredData.length" [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
</bit-callout>
|
||||
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
||||
from overflowing the <main> element. -->
|
||||
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
|
||||
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
|
||||
import { ScrollLayoutDirective } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule } from "../../../shared";
|
||||
import { SharedOrganizationModule } from "../shared";
|
||||
@@ -27,6 +28,7 @@ import { MembersComponent } from "./members.component";
|
||||
PasswordCalloutComponent,
|
||||
ScrollingModule,
|
||||
PasswordStrengthV2Component,
|
||||
ScrollLayoutDirective,
|
||||
],
|
||||
declarations: [
|
||||
BulkConfirmDialogComponent,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { ScrollLayoutDirective } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule } from "../../shared";
|
||||
|
||||
import { CoreOrganizationModule } from "./core";
|
||||
@@ -18,6 +20,7 @@ import { AccessSelectorModule } from "./shared/components/access-selector";
|
||||
OrganizationsRoutingModule,
|
||||
LooseComponentsModule,
|
||||
ScrollingModule,
|
||||
ScrollLayoutDirective,
|
||||
],
|
||||
declarations: [GroupsComponent, GroupAddEditComponent],
|
||||
})
|
||||
|
||||
@@ -591,5 +591,5 @@ export function openCollectionDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<CollectionDialogParams, DialogRef<CollectionDialogResult>>,
|
||||
) {
|
||||
return dialogService.open(CollectionDialogComponent, config);
|
||||
return dialogService.open<CollectionDialogResult>(CollectionDialogComponent, config);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,13 @@ import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { DialogService, ToastService, TableModule, PopoverModule } from "@bitwarden/components";
|
||||
import {
|
||||
DialogService,
|
||||
ToastService,
|
||||
TableModule,
|
||||
PopoverModule,
|
||||
LayoutComponent,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { VaultBannersService } from "../../../vault/individual-vault/vault-banners/services/vault-banners.service";
|
||||
@@ -115,6 +121,12 @@ describe("DeviceManagementComponent", () => {
|
||||
showError: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: LayoutComponent,
|
||||
useValue: {
|
||||
mainContent: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
@@ -19,7 +18,10 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
DialogRef,
|
||||
ButtonModule,
|
||||
DialogConfig,
|
||||
DIALOG_DATA,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { formatDate } from "@angular/common";
|
||||
import { Component, OnInit, signal } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
@@ -16,7 +15,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AddSponsorshipDialogComponent } from "./add-sponsorship-dialog.component";
|
||||
|
||||
@@ -105,11 +105,11 @@
|
||||
></button>
|
||||
<bit-menu #cipherOptions>
|
||||
<ng-container *ngIf="isNotDeletedLoginCipher">
|
||||
<button bitMenuItem type="button" (click)="copy('username')">
|
||||
<button bitMenuItem type="button" (click)="copy('username')" *ngIf="cipher.login.username">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyUsername" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem type="button" (click)="copy('password')" *ngIf="cipher.viewPassword">
|
||||
<button bitMenuItem type="button" (click)="copy('password')" *ngIf="cipher.login.password">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyPassword" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<cdk-virtual-scroll-viewport [itemSize]="RowHeight" scrollWindow class="tw-pb-8">
|
||||
<cdk-virtual-scroll-viewport [itemSize]="RowHeight" bitScrollLayout class="tw-pb-8">
|
||||
<bit-table [dataSource]="dataSource" layout="fixed">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { TableModule } from "@bitwarden/components";
|
||||
import { ScrollLayoutDirective, TableModule } from "@bitwarden/components";
|
||||
|
||||
import { CollectionNameBadgeComponent } from "../../../admin-console/organizations/collections";
|
||||
import { GroupBadgeModule } from "../../../admin-console/organizations/collections/group-badge/group-badge.module";
|
||||
@@ -26,6 +26,7 @@ import { VaultItemsComponent } from "./vault-items.component";
|
||||
CollectionNameBadgeComponent,
|
||||
GroupBadgeModule,
|
||||
PipesModule,
|
||||
ScrollLayoutDirective,
|
||||
],
|
||||
declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent],
|
||||
exports: [VaultItemsComponent],
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
// @ts-strict-ignore
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import {
|
||||
applicationConfig,
|
||||
componentWrapperDecorator,
|
||||
Meta,
|
||||
moduleMetadata,
|
||||
StoryObj,
|
||||
} from "@storybook/angular";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import {
|
||||
@@ -29,6 +35,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { LayoutComponent } from "@bitwarden/components";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
@@ -49,8 +56,9 @@ export default {
|
||||
title: "Web/Vault/Items",
|
||||
component: VaultItemsComponent,
|
||||
decorators: [
|
||||
componentWrapperDecorator((story) => `<bit-layout>${story}</bit-layout>`),
|
||||
moduleMetadata({
|
||||
imports: [VaultItemsModule, RouterModule],
|
||||
imports: [VaultItemsModule, RouterModule, LayoutComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
|
||||
Reference in New Issue
Block a user