mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01:00
Merge branch 'main' into PM-12985-Reports
This commit is contained in:
commit
208c5f6b39
@ -4910,6 +4910,42 @@
|
||||
"beta": {
|
||||
"message": "Beta"
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"extensionWidth": {
|
||||
"message": "Extension width"
|
||||
},
|
||||
|
@ -25,6 +25,7 @@ import { BrowserScriptInjectorService } from "../../../platform/services/browser
|
||||
import { AbortManager } from "../../../vault/background/abort-manager";
|
||||
import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum";
|
||||
import { Fido2PortName } from "../enums/fido2-port-name.enum";
|
||||
import { BrowserFido2ParentWindowReference } from "../services/browser-fido2-user-interface.service";
|
||||
|
||||
import { Fido2ExtensionMessage } from "./abstractions/fido2.background";
|
||||
import { Fido2Background } from "./fido2.background";
|
||||
@ -56,7 +57,7 @@ describe("Fido2Background", () => {
|
||||
let senderMock!: MockProxy<chrome.runtime.MessageSender>;
|
||||
let logService!: MockProxy<LogService>;
|
||||
let fido2ActiveRequestManager: MockProxy<Fido2ActiveRequestManager>;
|
||||
let fido2ClientService!: MockProxy<Fido2ClientService>;
|
||||
let fido2ClientService!: MockProxy<Fido2ClientService<BrowserFido2ParentWindowReference>>;
|
||||
let vaultSettingsService!: MockProxy<VaultSettingsService>;
|
||||
let scriptInjectorServiceMock!: MockProxy<BrowserScriptInjectorService>;
|
||||
let configServiceMock!: MockProxy<ConfigService>;
|
||||
@ -73,7 +74,7 @@ describe("Fido2Background", () => {
|
||||
});
|
||||
senderMock = mock<chrome.runtime.MessageSender>({ id: "1", tab: tabMock });
|
||||
logService = mock<LogService>();
|
||||
fido2ClientService = mock<Fido2ClientService>();
|
||||
fido2ClientService = mock<Fido2ClientService<BrowserFido2ParentWindowReference>>();
|
||||
vaultSettingsService = mock<VaultSettingsService>();
|
||||
abortManagerMock = mock<AbortManager>();
|
||||
abortController = mock<AbortController>();
|
||||
|
@ -23,10 +23,11 @@ import { ScriptInjectorService } from "../../../platform/services/abstractions/s
|
||||
import { AbortManager } from "../../../vault/background/abort-manager";
|
||||
import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum";
|
||||
import { Fido2PortName } from "../enums/fido2-port-name.enum";
|
||||
import { BrowserFido2ParentWindowReference } from "../services/browser-fido2-user-interface.service";
|
||||
|
||||
import {
|
||||
Fido2Background as Fido2BackgroundInterface,
|
||||
Fido2BackgroundExtensionMessageHandlers,
|
||||
Fido2Background as Fido2BackgroundInterface,
|
||||
Fido2ExtensionMessage,
|
||||
SharedFido2ScriptInjectionDetails,
|
||||
SharedFido2ScriptRegistrationOptions,
|
||||
@ -56,7 +57,7 @@ export class Fido2Background implements Fido2BackgroundInterface {
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private fido2ActiveRequestManager: Fido2ActiveRequestManager,
|
||||
private fido2ClientService: Fido2ClientService,
|
||||
private fido2ClientService: Fido2ClientService<BrowserFido2ParentWindowReference>,
|
||||
private vaultSettingsService: VaultSettingsService,
|
||||
private scriptInjectorService: ScriptInjectorService,
|
||||
private configService: ConfigService,
|
||||
|
@ -111,11 +111,15 @@ export type BrowserFido2Message = { sessionId: string } & (
|
||||
}
|
||||
);
|
||||
|
||||
export type BrowserFido2ParentWindowReference = chrome.tabs.Tab;
|
||||
|
||||
/**
|
||||
* Browser implementation of the {@link Fido2UserInterfaceService}.
|
||||
* The user interface is implemented as a popout and the service uses the browser's messaging API to communicate with it.
|
||||
*/
|
||||
export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
|
||||
export class BrowserFido2UserInterfaceService
|
||||
implements Fido2UserInterfaceServiceAbstraction<BrowserFido2ParentWindowReference>
|
||||
{
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
async newSession(
|
||||
|
@ -201,11 +201,11 @@ import {
|
||||
ImportServiceAbstraction,
|
||||
} from "@bitwarden/importer/core";
|
||||
import {
|
||||
DefaultKdfConfigService,
|
||||
KdfConfigService,
|
||||
BiometricStateService,
|
||||
BiometricsService,
|
||||
DefaultBiometricStateService,
|
||||
DefaultKdfConfigService,
|
||||
KdfConfigService,
|
||||
KeyService as KeyServiceAbstraction,
|
||||
} from "@bitwarden/key-management";
|
||||
import {
|
||||
@ -232,7 +232,10 @@ import { MainContextMenuHandler } from "../autofill/browser/main-context-menu-ha
|
||||
import LegacyOverlayBackground from "../autofill/deprecated/background/overlay.background.deprecated";
|
||||
import { Fido2Background as Fido2BackgroundAbstraction } from "../autofill/fido2/background/abstractions/fido2.background";
|
||||
import { Fido2Background } from "../autofill/fido2/background/fido2.background";
|
||||
import { BrowserFido2UserInterfaceService } from "../autofill/fido2/services/browser-fido2-user-interface.service";
|
||||
import {
|
||||
BrowserFido2ParentWindowReference,
|
||||
BrowserFido2UserInterfaceService,
|
||||
} from "../autofill/fido2/services/browser-fido2-user-interface.service";
|
||||
import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service";
|
||||
import AutofillService from "../autofill/services/autofill.service";
|
||||
import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service";
|
||||
@ -337,10 +340,10 @@ export default class MainBackground {
|
||||
policyApiService: PolicyApiServiceAbstraction;
|
||||
sendApiService: SendApiServiceAbstraction;
|
||||
userVerificationApiService: UserVerificationApiServiceAbstraction;
|
||||
fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction;
|
||||
fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction;
|
||||
fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction<BrowserFido2ParentWindowReference>;
|
||||
fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<BrowserFido2ParentWindowReference>;
|
||||
fido2ActiveRequestManager: Fido2ActiveRequestManagerAbstraction;
|
||||
fido2ClientService: Fido2ClientServiceAbstraction;
|
||||
fido2ClientService: Fido2ClientServiceAbstraction<BrowserFido2ParentWindowReference>;
|
||||
avatarService: AvatarServiceAbstraction;
|
||||
mainContextMenuHandler: MainContextMenuHandler;
|
||||
cipherContextMenuHandler: CipherContextMenuHandler;
|
||||
@ -1330,7 +1333,7 @@ export default class MainBackground {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await this.refreshBadge();
|
||||
await this.fullSync(true);
|
||||
await this.fullSync(false);
|
||||
this.taskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.scheduleNextSyncInterval,
|
||||
5 * 60 * 1000, // check every 5 minutes
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||
import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect";
|
||||
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
|
||||
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
@ -43,6 +44,11 @@ import {
|
||||
TwoFactorTimeoutIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import {
|
||||
NewDeviceVerificationNoticePageOneComponent,
|
||||
NewDeviceVerificationNoticePageTwoComponent,
|
||||
VaultIcons,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap";
|
||||
import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard";
|
||||
@ -715,6 +721,33 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "new-device-notice",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
canActivate: [],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: NewDeviceVerificationNoticePageOneComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.ExclamationTriangle,
|
||||
pageTitle: {
|
||||
key: "importantNotice",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "setup",
|
||||
component: NewDeviceVerificationNoticePageTwoComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.UserLock,
|
||||
pageTitle: {
|
||||
key: "setupTwoStepLogin",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
...extensionRefreshSwap(TabsComponent, TabsV2Component, {
|
||||
path: "tabs",
|
||||
data: { elevation: 0 } satisfies RouteDataProperties,
|
||||
@ -734,7 +767,7 @@ const routes: Routes = [
|
||||
},
|
||||
...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, {
|
||||
path: "vault",
|
||||
canActivate: [authGuard],
|
||||
canActivate: [authGuard, NewDeviceVerificationNoticeGuard],
|
||||
canDeactivate: [clearVaultStateGuard],
|
||||
data: { elevation: 0 } satisfies RouteDataProperties,
|
||||
}),
|
||||
|
@ -1,8 +1,8 @@
|
||||
<popup-page>
|
||||
<popup-page [loading]="!cipher">
|
||||
<popup-header slot="header" pageTitle="{{ 'passwordHistory' | i18n }}" showBackButton>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<vault-password-history-view *ngIf="cipherId" [cipherId]="cipherId" />
|
||||
<vault-password-history-view *ngIf="cipher" [cipher]="cipher" />
|
||||
</popup-page>
|
||||
|
@ -1,27 +1,40 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { Subject } from "rxjs";
|
||||
import { BehaviorSubject, Subject } from "rxjs";
|
||||
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
|
||||
import { PasswordHistoryV2Component } from "./vault-password-history-v2.component";
|
||||
|
||||
describe("PasswordHistoryV2Component", () => {
|
||||
let component: PasswordHistoryV2Component;
|
||||
let fixture: ComponentFixture<PasswordHistoryV2Component>;
|
||||
const params$ = new Subject();
|
||||
|
||||
const mockCipherView = {
|
||||
id: "111-222-333",
|
||||
name: "cipher one",
|
||||
} as CipherView;
|
||||
|
||||
const mockCipher = {
|
||||
decrypt: jest.fn().mockResolvedValue(mockCipherView),
|
||||
} as unknown as Cipher;
|
||||
|
||||
const back = jest.fn().mockResolvedValue(undefined);
|
||||
const getCipher = jest.fn().mockResolvedValue(mockCipher);
|
||||
|
||||
beforeEach(async () => {
|
||||
back.mockClear();
|
||||
getCipher.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PasswordHistoryV2Component],
|
||||
@ -29,8 +42,13 @@ describe("PasswordHistoryV2Component", () => {
|
||||
{ provide: WINDOW, useValue: window },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: AccountService, useValue: mock<AccountService>() },
|
||||
{ provide: CipherService, useValue: mock<CipherService>({ get: getCipher }) },
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: mock<AccountService>({
|
||||
activeAccount$: new BehaviorSubject({ id: "acct-1" } as Account),
|
||||
}),
|
||||
},
|
||||
{ provide: PopupRouterCacheService, useValue: { back } },
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: params$ } },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
@ -38,19 +56,21 @@ describe("PasswordHistoryV2Component", () => {
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PasswordHistoryV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("sets the cipherId from the params", () => {
|
||||
params$.next({ cipherId: "444-33-33-1111" });
|
||||
it("loads the cipher from params the cipherId from the params", fakeAsync(() => {
|
||||
params$.next({ cipherId: mockCipherView.id });
|
||||
|
||||
expect(component["cipherId"]).toBe("444-33-33-1111");
|
||||
});
|
||||
tick(100);
|
||||
|
||||
expect(getCipher).toHaveBeenCalledWith(mockCipherView.id);
|
||||
}));
|
||||
|
||||
it("navigates back when a cipherId is not in the params", () => {
|
||||
params$.next({});
|
||||
|
||||
expect(back).toHaveBeenCalledTimes(1);
|
||||
expect(getCipher).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -3,10 +3,14 @@
|
||||
import { NgIf } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first, map } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.component";
|
||||
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
|
||||
@ -28,18 +32,20 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach
|
||||
],
|
||||
})
|
||||
export class PasswordHistoryV2Component implements OnInit {
|
||||
protected cipherId: CipherId;
|
||||
protected cipher: CipherView;
|
||||
|
||||
constructor(
|
||||
private browserRouterHistory: PopupRouterCacheService,
|
||||
private route: ActivatedRoute,
|
||||
private cipherService: CipherService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.route.queryParams.pipe(first()).subscribe((params) => {
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
void this.loadCipher(params.cipherId);
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
@ -49,4 +55,22 @@ export class PasswordHistoryV2Component implements OnInit {
|
||||
close() {
|
||||
void this.browserRouterHistory.back();
|
||||
}
|
||||
|
||||
/** Load the cipher based on the given Id */
|
||||
private async loadCipher(cipherId: string) {
|
||||
const cipher = await this.cipherService.get(cipherId);
|
||||
|
||||
const activeAccount = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)),
|
||||
);
|
||||
|
||||
if (!activeAccount?.id) {
|
||||
throw new Error("Active account is not available.");
|
||||
}
|
||||
|
||||
const activeUserId = activeAccount.id as UserId;
|
||||
this.cipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { BrowserViewPasswordHistoryService } from "./browser-view-password-history.service";
|
||||
|
||||
describe("BrowserViewPasswordHistoryService", () => {
|
||||
@ -19,9 +21,9 @@ describe("BrowserViewPasswordHistoryService", () => {
|
||||
|
||||
describe("viewPasswordHistory", () => {
|
||||
it("navigates to the password history screen", async () => {
|
||||
await service.viewPasswordHistory("test");
|
||||
await service.viewPasswordHistory({ id: "cipher-id" } as CipherView);
|
||||
expect(router.navigate).toHaveBeenCalledWith(["/cipher-password-history"], {
|
||||
queryParams: { cipherId: "test" },
|
||||
queryParams: { cipherId: "cipher-id" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ import { inject } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
/**
|
||||
* This class handles the premium upgrade process for the browser extension.
|
||||
@ -14,7 +15,9 @@ export class BrowserViewPasswordHistoryService implements ViewPasswordHistorySer
|
||||
/**
|
||||
* Navigates to the password history screen.
|
||||
*/
|
||||
async viewPasswordHistory(cipherId: string) {
|
||||
await this.router.navigate(["/cipher-password-history"], { queryParams: { cipherId } });
|
||||
async viewPasswordHistory(cipher: CipherView) {
|
||||
await this.router.navigate(["/cipher-password-history"], {
|
||||
queryParams: { cipherId: cipher.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@
|
||||
"papaparse": "5.4.1",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"rxjs": "7.8.1",
|
||||
"tldts": "6.1.66",
|
||||
"tldts": "6.1.69",
|
||||
"zxcvbn": "4.4.2"
|
||||
}
|
||||
}
|
||||
|
485
apps/desktop/desktop_native/Cargo.lock
generated
485
apps/desktop/desktop_native/Cargo.lock
generated
@ -62,12 +62,55 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.94"
|
||||
@ -103,6 +146,47 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"askama_escape",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"basic-toml",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_escape"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.1"
|
||||
@ -318,6 +402,15 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "basic-toml"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt-pbkdf"
|
||||
version = "0.10.0"
|
||||
@ -329,6 +422,15 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
@ -422,6 +524,38 @@ version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo-platform"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo_metadata"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"cargo-platform",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
@ -487,6 +621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -495,11 +630,24 @@ version = "4.5.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.3"
|
||||
@ -525,6 +673,12 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@ -724,6 +878,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
@ -815,6 +982,8 @@ dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
@ -1035,6 +1204,15 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
|
||||
|
||||
[[package]]
|
||||
name = "fs-err"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@ -1190,12 +1368,35 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "goblin"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
|
||||
dependencies = [
|
||||
"log",
|
||||
"plain",
|
||||
"scroll",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.4.0"
|
||||
@ -1245,7 +1446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1273,6 +1474,12 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
@ -1372,6 +1579,21 @@ version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "macos_provider"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"desktop_core",
|
||||
"futures",
|
||||
"log",
|
||||
"oslog",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
@ -1397,6 +1619,22 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@ -1811,6 +2049,17 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oslog"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dashmap",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@ -1851,6 +2100,12 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@ -1967,6 +2222,12 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.7.4"
|
||||
@ -2235,6 +2496,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "salsa20"
|
||||
version = "0.10.2"
|
||||
@ -2262,6 +2529,26 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
|
||||
|
||||
[[package]]
|
||||
name = "scroll"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
|
||||
dependencies = [
|
||||
"scroll_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scroll_derive"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.11.0"
|
||||
@ -2301,6 +2588,9 @@ name = "semver"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
@ -2322,6 +2612,18 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.19"
|
||||
@ -2391,6 +2693,12 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@ -2406,6 +2714,12 @@ version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.8"
|
||||
@ -2544,6 +2858,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@ -2648,6 +2971,15 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
@ -2726,6 +3058,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.14"
|
||||
@ -2744,6 +3082,136 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "uniffi"
|
||||
version = "0.28.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cb08c58c7ed7033150132febe696bef553f891b1ede57424b40d87a89e3c170"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
"cargo_metadata",
|
||||
"clap",
|
||||
"uniffi_bindgen",
|
||||
"uniffi_build",
|
||||
"uniffi_core",
|
||||
"uniffi_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_bindgen"
|
||||
version = "0.28.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cade167af943e189a55020eda2c314681e223f1e42aca7c4e52614c2b627698f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askama",
|
||||
"camino",
|
||||
"cargo_metadata",
|
||||
"fs-err",
|
||||
"glob",
|
||||
"goblin",
|
||||
"heck",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"serde",
|
||||
"textwrap",
|
||||
"toml",
|
||||
"uniffi_meta",
|
||||
"uniffi_udl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_build"
|
||||
version = "0.28.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7cf32576e08104b7dc2a6a5d815f37616e66c6866c2a639fe16e6d2286b75b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
"uniffi_bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_checksum_derive"
|
||||
version = "0.28.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_core"
|
||||
version = "0.28.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"log",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_macros"
|
||||
version = "0.28.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"camino",
|
||||
"fs-err",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn",
|
||||
"toml",
|
||||
"uniffi_meta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_meta"
|
||||
version = "0.28.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a74ed96c26882dac1ca9b93ca23c827e284bacbd7ec23c6f0b0372f747d59e4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"siphasher",
|
||||
"uniffi_checksum_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_testing"
|
||||
version = "0.28.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6f984f0781f892cc864a62c3a5c60361b1ccbd68e538e6c9fbced5d82268ac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
"cargo_metadata",
|
||||
"fs-err",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_udl"
|
||||
version = "0.28.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037820a4cfc4422db1eaa82f291a3863c92c7d1789dc513489c36223f9b4cdfc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"textwrap",
|
||||
"uniffi_meta",
|
||||
"uniffi_testing",
|
||||
"weedle2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
@ -2754,6 +3222,12 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@ -2839,6 +3313,15 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weedle2"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.1.0"
|
||||
|
@ -1,3 +1,3 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["napi", "core", "proxy"]
|
||||
members = ["napi", "core", "proxy", "macos_provider"]
|
||||
|
1
apps/desktop/desktop_native/macos_provider/.gitignore
vendored
Normal file
1
apps/desktop/desktop_native/macos_provider/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
BitwardenMacosProviderFFI.xcframework
|
30
apps/desktop/desktop_native/macos_provider/Cargo.toml
Normal file
30
apps/desktop/desktop_native/macos_provider/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "macos_provider"
|
||||
license = "GPL-3.0"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "uniffi-bindgen"
|
||||
path = "uniffi-bindgen.rs"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "cdylib"]
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
desktop_core = { path = "../core" }
|
||||
futures = "=0.3.31"
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0.205", features = ["derive"] }
|
||||
serde_json = "1.0.122"
|
||||
tokio = { version = "1.39.2", features = ["sync"] }
|
||||
tokio-util = "0.7.11"
|
||||
uniffi = { version = "0.28.0", features = ["cli"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
oslog = "0.2.0"
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { version = "0.28.0", features = ["build"] }
|
43
apps/desktop/desktop_native/macos_provider/build.sh
Executable file
43
apps/desktop/desktop_native/macos_provider/build.sh
Executable file
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
rm -r BitwardenMacosProviderFFI.xcframework
|
||||
rm -r tmp
|
||||
|
||||
mkdir -p ./tmp/target/universal-darwin/release/
|
||||
|
||||
|
||||
cargo build --package macos_provider --target aarch64-apple-darwin --release
|
||||
cargo build --package macos_provider --target x86_64-apple-darwin --release
|
||||
|
||||
# Create universal libraries
|
||||
lipo -create ../target/aarch64-apple-darwin/release/libmacos_provider.a \
|
||||
../target/x86_64-apple-darwin/release/libmacos_provider.a \
|
||||
-output ./tmp/target/universal-darwin/release/libmacos_provider.a
|
||||
|
||||
# Generate swift bindings
|
||||
cargo run --bin uniffi-bindgen --features uniffi/cli generate \
|
||||
../target/aarch64-apple-darwin/release/libmacos_provider.dylib \
|
||||
--library \
|
||||
--language swift \
|
||||
--no-format \
|
||||
--out-dir tmp/bindings
|
||||
|
||||
# Move generated swift bindings
|
||||
mkdir -p ../../macos/autofill-extension/
|
||||
mv ./tmp/bindings/*.swift ../../macos/autofill-extension/
|
||||
|
||||
# Massage the generated files to fit xcframework
|
||||
mkdir tmp/Headers
|
||||
mv ./tmp/bindings/*.h ./tmp/Headers/
|
||||
cat ./tmp/bindings/*.modulemap > ./tmp/Headers/module.modulemap
|
||||
|
||||
# Build xcframework
|
||||
xcodebuild -create-xcframework \
|
||||
-library ./tmp/target/universal-darwin/release/libmacos_provider.a \
|
||||
-headers ./tmp/Headers \
|
||||
-output ./BitwardenMacosProviderFFI.xcframework
|
||||
|
||||
# Cleanup temporary files
|
||||
rm -r tmp
|
46
apps/desktop/desktop_native/macos_provider/src/assertion.rs
Normal file
46
apps/desktop/desktop_native/macos_provider/src/assertion.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{BitwardenError, Callback, UserVerification};
|
||||
|
||||
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionRequest {
|
||||
rp_id: String,
|
||||
credential_id: Vec<u8>,
|
||||
user_name: String,
|
||||
user_handle: Vec<u8>,
|
||||
record_identifier: Option<String>,
|
||||
client_data_hash: Vec<u8>,
|
||||
user_verification: UserVerification,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionResponse {
|
||||
rp_id: String,
|
||||
user_handle: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
client_data_hash: Vec<u8>,
|
||||
authenticator_data: Vec<u8>,
|
||||
credential_id: Vec<u8>,
|
||||
}
|
||||
|
||||
#[uniffi::export(with_foreign)]
|
||||
pub trait PreparePasskeyAssertionCallback: Send + Sync {
|
||||
fn on_complete(&self, credential: PasskeyAssertionResponse);
|
||||
fn on_error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
impl Callback for Arc<dyn PreparePasskeyAssertionCallback> {
|
||||
fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> {
|
||||
let credential = serde_json::from_value(credential)?;
|
||||
PreparePasskeyAssertionCallback::on_complete(self.as_ref(), credential);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn error(&self, error: BitwardenError) {
|
||||
PreparePasskeyAssertionCallback::on_error(self.as_ref(), error);
|
||||
}
|
||||
}
|
205
apps/desktop/desktop_native/macos_provider/src/lib.rs
Normal file
205
apps/desktop/desktop_native/macos_provider/src/lib.rs
Normal file
@ -0,0 +1,205 @@
|
||||
#![cfg(target_os = "macos")]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{atomic::AtomicU32, Arc, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use futures::FutureExt;
|
||||
use log::{error, info};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
mod assertion;
|
||||
mod registration;
|
||||
|
||||
use assertion::{PasskeyAssertionRequest, PreparePasskeyAssertionCallback};
|
||||
use registration::{PasskeyRegistrationRequest, PreparePasskeyRegistrationCallback};
|
||||
|
||||
#[derive(uniffi::Enum, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum UserVerification {
|
||||
Preferred,
|
||||
Required,
|
||||
Discouraged,
|
||||
}
|
||||
|
||||
#[derive(Debug, uniffi::Error, Serialize, Deserialize)]
|
||||
pub enum BitwardenError {
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
// TODO: These have to be named differently than the actual Uniffi traits otherwise
|
||||
// the generated code will lead to ambiguous trait implementations
|
||||
// These are only used internally, so it doesn't matter that much
|
||||
trait Callback: Send + Sync {
|
||||
fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error>;
|
||||
fn error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct MacOSProviderClient {
|
||||
to_server_send: tokio::sync::mpsc::Sender<String>,
|
||||
|
||||
// We need to keep track of the callbacks so we can call them when we receive a response
|
||||
response_callbacks_counter: AtomicU32,
|
||||
#[allow(clippy::type_complexity)]
|
||||
response_callbacks_queue: Arc<Mutex<HashMap<u32, (Box<dyn Callback>, Instant)>>>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl MacOSProviderClient {
|
||||
#[uniffi::constructor]
|
||||
pub fn connect() -> Self {
|
||||
let _ = oslog::OsLogger::new("com.bitwarden.desktop.autofill-extension")
|
||||
.level_filter(log::LevelFilter::Trace)
|
||||
.init();
|
||||
|
||||
let (from_server_send, mut from_server_recv) = tokio::sync::mpsc::channel(32);
|
||||
let (to_server_send, to_server_recv) = tokio::sync::mpsc::channel(32);
|
||||
|
||||
let client = MacOSProviderClient {
|
||||
to_server_send,
|
||||
response_callbacks_counter: AtomicU32::new(0),
|
||||
response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())),
|
||||
};
|
||||
|
||||
let path = desktop_core::ipc::path("autofill");
|
||||
|
||||
let queue = client.response_callbacks_queue.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Can't create runtime");
|
||||
|
||||
rt.spawn(
|
||||
desktop_core::ipc::client::connect(path, from_server_send, to_server_recv)
|
||||
.map(|r| r.map_err(|e| e.to_string())),
|
||||
);
|
||||
|
||||
rt.block_on(async move {
|
||||
while let Some(message) = from_server_recv.recv().await {
|
||||
match serde_json::from_str::<SerializedMessage>(&message) {
|
||||
Ok(SerializedMessage::Command(CommandMessage::Connected)) => {
|
||||
info!("Connected to server");
|
||||
}
|
||||
Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => {
|
||||
info!("Disconnected from server");
|
||||
}
|
||||
Ok(SerializedMessage::Message {
|
||||
sequence_number,
|
||||
value,
|
||||
}) => match queue.lock().unwrap().remove(&sequence_number) {
|
||||
Some((cb, request_start_time)) => {
|
||||
info!(
|
||||
"Time to process request: {:?}",
|
||||
request_start_time.elapsed()
|
||||
);
|
||||
match value {
|
||||
Ok(value) => {
|
||||
if let Err(e) = cb.complete(value) {
|
||||
error!("Error deserializing message: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error processing message: {e:?}");
|
||||
cb.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("No callback found for sequence number: {sequence_number}")
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error deserializing message: {e}");
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
pub fn prepare_passkey_registration(
|
||||
&self,
|
||||
request: PasskeyRegistrationRequest,
|
||||
callback: Arc<dyn PreparePasskeyRegistrationCallback>,
|
||||
) {
|
||||
self.send_message(request, Box::new(callback));
|
||||
}
|
||||
|
||||
pub fn prepare_passkey_assertion(
|
||||
&self,
|
||||
request: PasskeyAssertionRequest,
|
||||
callback: Arc<dyn PreparePasskeyAssertionCallback>,
|
||||
) {
|
||||
self.send_message(request, Box::new(callback));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "command", rename_all = "camelCase")]
|
||||
enum CommandMessage {
|
||||
Connected,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged, rename_all = "camelCase")]
|
||||
enum SerializedMessage {
|
||||
Command(CommandMessage),
|
||||
Message {
|
||||
sequence_number: u32,
|
||||
value: Result<serde_json::Value, BitwardenError>,
|
||||
},
|
||||
}
|
||||
|
||||
impl MacOSProviderClient {
|
||||
fn add_callback(&self, callback: Box<dyn Callback>) -> u32 {
|
||||
let sequence_number = self
|
||||
.response_callbacks_counter
|
||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
|
||||
self.response_callbacks_queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(sequence_number, (callback, Instant::now()));
|
||||
|
||||
sequence_number
|
||||
}
|
||||
|
||||
fn send_message(
|
||||
&self,
|
||||
message: impl Serialize + DeserializeOwned,
|
||||
callback: Box<dyn Callback>,
|
||||
) {
|
||||
let sequence_number = self.add_callback(callback);
|
||||
|
||||
let message = serde_json::to_string(&SerializedMessage::Message {
|
||||
sequence_number,
|
||||
value: Ok(serde_json::to_value(message).unwrap()),
|
||||
})
|
||||
.expect("Can't serialize message");
|
||||
|
||||
if let Err(e) = self.to_server_send.blocking_send(message) {
|
||||
// Make sure we remove the callback from the queue if we can't send the message
|
||||
if let Some((cb, _)) = self
|
||||
.response_callbacks_queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&sequence_number)
|
||||
{
|
||||
cb.error(BitwardenError::Internal(format!(
|
||||
"Error sending message: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{BitwardenError, Callback, UserVerification};
|
||||
|
||||
#[derive(uniffi::Record, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyRegistrationRequest {
|
||||
rp_id: String,
|
||||
user_name: String,
|
||||
user_handle: Vec<u8>,
|
||||
client_data_hash: Vec<u8>,
|
||||
user_verification: UserVerification,
|
||||
supported_algorithms: Vec<i32>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyRegistrationResponse {
|
||||
rp_id: String,
|
||||
client_data_hash: Vec<u8>,
|
||||
credential_id: Vec<u8>,
|
||||
attestation_object: Vec<u8>,
|
||||
}
|
||||
|
||||
#[uniffi::export(with_foreign)]
|
||||
pub trait PreparePasskeyRegistrationCallback: Send + Sync {
|
||||
fn on_complete(&self, credential: PasskeyRegistrationResponse);
|
||||
fn on_error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
impl Callback for Arc<dyn PreparePasskeyRegistrationCallback> {
|
||||
fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> {
|
||||
let credential = serde_json::from_value(credential)?;
|
||||
PreparePasskeyRegistrationCallback::on_complete(self.as_ref(), credential);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn error(&self, error: BitwardenError) {
|
||||
PreparePasskeyRegistrationCallback::on_error(self.as_ref(), error);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
4
apps/desktop/desktop_native/macos_provider/uniffi.toml
Normal file
4
apps/desktop/desktop_native/macos_provider/uniffi.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[bindings.swift]
|
||||
ffi_module_name = "BitwardenMacosProviderFFI"
|
||||
module_name = "BitwardenMacosProvider"
|
||||
generate_immutable_records = true
|
@ -20,6 +20,8 @@ anyhow = "=1.0.94"
|
||||
desktop_core = { path = "../core" }
|
||||
napi = { version = "=2.16.13", features = ["async"] }
|
||||
napi-derive = "=2.16.13"
|
||||
serde = { version = "1.0.209", features = ["derive"] }
|
||||
serde_json = "1.0.127"
|
||||
tokio = { version = "=1.41.1" }
|
||||
tokio-util = "=0.7.12"
|
||||
tokio-stream = "=0.1.15"
|
||||
|
52
apps/desktop/desktop_native/napi/index.d.ts
vendored
52
apps/desktop/desktop_native/napi/index.d.ts
vendored
@ -124,6 +124,58 @@ export declare namespace ipc {
|
||||
}
|
||||
export declare namespace autofill {
|
||||
export function runCommand(value: string): Promise<string>
|
||||
export const enum UserVerification {
|
||||
Preferred = 'preferred',
|
||||
Required = 'required',
|
||||
Discouraged = 'discouraged'
|
||||
}
|
||||
export interface PasskeyRegistrationRequest {
|
||||
rpId: string
|
||||
userName: string
|
||||
userHandle: Array<number>
|
||||
clientDataHash: Array<number>
|
||||
userVerification: UserVerification
|
||||
supportedAlgorithms: Array<number>
|
||||
}
|
||||
export interface PasskeyRegistrationResponse {
|
||||
rpId: string
|
||||
clientDataHash: Array<number>
|
||||
credentialId: Array<number>
|
||||
attestationObject: Array<number>
|
||||
}
|
||||
export interface PasskeyAssertionRequest {
|
||||
rpId: string
|
||||
credentialId: Array<number>
|
||||
userName: string
|
||||
userHandle: Array<number>
|
||||
recordIdentifier?: string
|
||||
clientDataHash: Array<number>
|
||||
userVerification: UserVerification
|
||||
}
|
||||
export interface PasskeyAssertionResponse {
|
||||
rpId: string
|
||||
userHandle: Array<number>
|
||||
signature: Array<number>
|
||||
clientDataHash: Array<number>
|
||||
authenticatorData: Array<number>
|
||||
credentialId: Array<number>
|
||||
}
|
||||
export class IpcServer {
|
||||
/**
|
||||
* Create and start the IPC server without blocking.
|
||||
*
|
||||
* @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
|
||||
* @param callback This function will be called whenever a message is received from a client.
|
||||
*/
|
||||
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void): Promise<IpcServer>
|
||||
/** Return the path to the IPC server. */
|
||||
getPath(): string
|
||||
/** Stop the IPC server. */
|
||||
stop(): void
|
||||
completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number
|
||||
completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number
|
||||
completeError(clientId: number, sequenceNumber: number, error: string): number
|
||||
}
|
||||
}
|
||||
export declare namespace crypto {
|
||||
export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise<Buffer>
|
||||
|
@ -545,12 +545,256 @@ pub mod ipc {
|
||||
|
||||
#[napi]
|
||||
pub mod autofill {
|
||||
use desktop_core::ipc::server::{Message, MessageType};
|
||||
use napi::threadsafe_function::{
|
||||
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
#[napi]
|
||||
pub async fn run_command(value: String) -> napi::Result<String> {
|
||||
desktop_core::autofill::run_command(value)
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde:: Deserialize)]
|
||||
pub enum BitwardenError {
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
#[napi(string_enum)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum UserVerification {
|
||||
#[napi(value = "preferred")]
|
||||
Preferred,
|
||||
#[napi(value = "required")]
|
||||
Required,
|
||||
#[napi(value = "discouraged")]
|
||||
Discouraged,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(bound = "T: Serialize + DeserializeOwned")]
|
||||
pub struct PasskeyMessage<T: Serialize + DeserializeOwned> {
|
||||
pub sequence_number: u32,
|
||||
pub value: Result<T, BitwardenError>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyRegistrationRequest {
|
||||
pub rp_id: String,
|
||||
pub user_name: String,
|
||||
pub user_handle: Vec<u8>,
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub user_verification: UserVerification,
|
||||
pub supported_algorithms: Vec<i32>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyRegistrationResponse {
|
||||
pub rp_id: String,
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub credential_id: Vec<u8>,
|
||||
pub attestation_object: Vec<u8>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionRequest {
|
||||
pub rp_id: String,
|
||||
pub credential_id: Vec<u8>,
|
||||
pub user_name: String,
|
||||
pub user_handle: Vec<u8>,
|
||||
pub record_identifier: Option<String>,
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub user_verification: UserVerification,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionResponse {
|
||||
pub rp_id: String,
|
||||
pub user_handle: Vec<u8>,
|
||||
pub signature: Vec<u8>,
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub authenticator_data: Vec<u8>,
|
||||
pub credential_id: Vec<u8>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct IpcServer {
|
||||
server: desktop_core::ipc::server::Server,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl IpcServer {
|
||||
/// Create and start the IPC server without blocking.
|
||||
///
|
||||
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
|
||||
/// @param callback This function will be called whenever a message is received from a client.
|
||||
#[napi(factory)]
|
||||
pub async fn listen(
|
||||
name: String,
|
||||
// Ideally we'd have a single callback that has an enum containing the request values,
|
||||
// but NAPI doesn't support that just yet
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void"
|
||||
)]
|
||||
registration_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyRegistrationRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
>,
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void"
|
||||
)]
|
||||
assertion_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyAssertionRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
>,
|
||||
) -> napi::Result<Self> {
|
||||
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||||
tokio::spawn(async move {
|
||||
while let Some(Message {
|
||||
client_id,
|
||||
kind,
|
||||
message,
|
||||
}) = recv.recv().await
|
||||
{
|
||||
match kind {
|
||||
// TODO: We're ignoring the connection and disconnection messages for now
|
||||
MessageType::Connected | MessageType::Disconnected => continue,
|
||||
MessageType::Message => {
|
||||
let Some(message) = message else {
|
||||
println!("[ERROR] Message is empty");
|
||||
continue;
|
||||
};
|
||||
|
||||
match serde_json::from_str::<PasskeyMessage<PasskeyAssertionRequest>>(
|
||||
&message,
|
||||
) {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
|
||||
assertion_callback
|
||||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[ERROR] Error deserializing message1: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
match serde_json::from_str::<PasskeyMessage<PasskeyRegistrationRequest>>(
|
||||
&message,
|
||||
) {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
registration_callback
|
||||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[ERROR] Error deserializing message2: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
println!("[ERROR] Received an unknown message2: {message:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let path = desktop_core::ipc::path(&name);
|
||||
|
||||
let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| {
|
||||
napi::Error::from_reason(format!(
|
||||
"Error listening to server - Path: {path:?} - Error: {e} - {e:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(IpcServer { server })
|
||||
}
|
||||
|
||||
/// Return the path to the IPC server.
|
||||
#[napi]
|
||||
pub fn get_path(&self) -> String {
|
||||
self.server.path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// Stop the IPC server.
|
||||
#[napi]
|
||||
pub fn stop(&self) -> napi::Result<()> {
|
||||
self.server.stop();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn complete_registration(
|
||||
&self,
|
||||
client_id: u32,
|
||||
sequence_number: u32,
|
||||
response: PasskeyRegistrationResponse,
|
||||
) -> napi::Result<u32> {
|
||||
let message = PasskeyMessage {
|
||||
sequence_number,
|
||||
value: Ok(response),
|
||||
};
|
||||
self.send(client_id, serde_json::to_string(&message).unwrap())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn complete_assertion(
|
||||
&self,
|
||||
client_id: u32,
|
||||
sequence_number: u32,
|
||||
response: PasskeyAssertionResponse,
|
||||
) -> napi::Result<u32> {
|
||||
let message = PasskeyMessage {
|
||||
sequence_number,
|
||||
value: Ok(response),
|
||||
};
|
||||
self.send(client_id, serde_json::to_string(&message).unwrap())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn complete_error(
|
||||
&self,
|
||||
client_id: u32,
|
||||
sequence_number: u32,
|
||||
error: String,
|
||||
) -> napi::Result<u32> {
|
||||
let message: PasskeyMessage<()> = PasskeyMessage {
|
||||
sequence_number,
|
||||
value: Err(BitwardenError::Internal(error)),
|
||||
};
|
||||
self.send(client_id, serde_json::to_string(&message).unwrap())
|
||||
}
|
||||
|
||||
// TODO: Add a way to send a message to a specific client?
|
||||
fn send(&self, _client_id: u32, message: String) -> napi::Result<u32> {
|
||||
self.server
|
||||
.send(message)
|
||||
.map_err(|e| {
|
||||
napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}"))
|
||||
})
|
||||
// NAPI doesn't support u64 or usize, so we need to convert to u32
|
||||
.map(|u| u32::try_from(u).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
1
apps/desktop/macos/.gitignore
vendored
Normal file
1
apps/desktop/macos/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
BitwardenMacosProvider.swift
|
@ -9,7 +9,45 @@ import AuthenticationServices
|
||||
import os
|
||||
|
||||
class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
let logger = Logger()
|
||||
let logger: Logger
|
||||
|
||||
// There is something a bit strange about the initialization/deinitialization in this class.
|
||||
// Sometimes deinit won't be called after a request has successfully finished,
|
||||
// which would leave this class hanging in memory and the IPC connection open.
|
||||
//
|
||||
// If instead I make this a static, the deinit gets called correctly after each request.
|
||||
// I think we still might want a static regardless, to be able to reuse the connection if possible.
|
||||
static let client: MacOsProviderClient = {
|
||||
let instance = MacOsProviderClient.connect()
|
||||
// setup code
|
||||
return instance
|
||||
}()
|
||||
|
||||
init() {
|
||||
logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider")
|
||||
|
||||
logger.log("[autofill-extension] initializing extension")
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
logger.log("[autofill-extension] deinitializing extension")
|
||||
}
|
||||
|
||||
|
||||
@IBAction func cancel(_ sender: AnyObject?) {
|
||||
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
||||
}
|
||||
|
||||
@IBAction func passwordSelected(_ sender: AnyObject?) {
|
||||
let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
|
||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
|
||||
}
|
||||
|
||||
/*
|
||||
Implement this method if your extension supports showing credentials in the QuickType bar.
|
||||
@ -21,7 +59,14 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
|
||||
*/
|
||||
|
||||
// Deprecated
|
||||
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction called \(credentialIdentity)")
|
||||
logger.log("[autofill-extension] user \(credentialIdentity.user)")
|
||||
logger.log("[autofill-extension] id \(credentialIdentity.recordIdentifier ?? "")")
|
||||
logger.log("[autofill-extension] sid \(credentialIdentity.serviceIdentifier.identifier)")
|
||||
logger.log("[autofill-extension] sidt \(credentialIdentity.serviceIdentifier.type.rawValue)")
|
||||
|
||||
// let databaseIsUnlocked = true
|
||||
// if (databaseIsUnlocked) {
|
||||
let passwordCredential = ASPasswordCredential(user: credentialIdentity.user, password: "example1234")
|
||||
@ -31,6 +76,67 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
// }
|
||||
}
|
||||
|
||||
override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) {
|
||||
if let request = credentialRequest as? ASPasskeyCredentialRequest {
|
||||
if let passkeyIdentity = request.credentialIdentity as? ASPasskeyCredentialIdentity {
|
||||
|
||||
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(passkey) called \(request)")
|
||||
|
||||
class CallbackImpl: PreparePasskeyAssertionCallback {
|
||||
let ctx: ASCredentialProviderExtensionContext
|
||||
required init(_ ctx: ASCredentialProviderExtensionContext) {
|
||||
self.ctx = ctx
|
||||
}
|
||||
|
||||
func onComplete(credential: PasskeyAssertionResponse) {
|
||||
ctx.completeAssertionRequest(using: ASPasskeyAssertionCredential(
|
||||
userHandle: credential.userHandle,
|
||||
relyingParty: credential.rpId,
|
||||
signature: credential.signature,
|
||||
clientDataHash: credential.clientDataHash,
|
||||
authenticatorData: credential.authenticatorData,
|
||||
credentialID: credential.credentialId
|
||||
))
|
||||
}
|
||||
|
||||
func onError(error: BitwardenError) {
|
||||
ctx.cancelRequest(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
let userVerification = switch request.userVerificationPreference {
|
||||
case .preferred:
|
||||
UserVerification.preferred
|
||||
case .required:
|
||||
UserVerification.required
|
||||
default:
|
||||
UserVerification.discouraged
|
||||
}
|
||||
|
||||
let req = PasskeyAssertionRequest(
|
||||
rpId: passkeyIdentity.relyingPartyIdentifier,
|
||||
credentialId: passkeyIdentity.credentialID,
|
||||
userName: passkeyIdentity.userName,
|
||||
userHandle: passkeyIdentity.userHandle,
|
||||
recordIdentifier: passkeyIdentity.recordIdentifier,
|
||||
clientDataHash: request.clientDataHash,
|
||||
userVerification: userVerification
|
||||
)
|
||||
|
||||
CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let request = credentialRequest as? ASPasswordCredentialRequest {
|
||||
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(password) called \(request)")
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2 called wrong")
|
||||
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request"))
|
||||
}
|
||||
|
||||
/*
|
||||
Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
|
||||
ASExtensionError.userInteractionRequired. In this case, the system may present your extension's
|
||||
@ -41,34 +147,65 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
}
|
||||
*/
|
||||
|
||||
@IBAction func cancel(_ sender: AnyObject?) {
|
||||
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
||||
}
|
||||
|
||||
@IBAction func passwordSelected(_ sender: AnyObject?) {
|
||||
let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
|
||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
|
||||
}
|
||||
|
||||
override func prepareInterfaceForExtensionConfiguration() {
|
||||
logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called")
|
||||
}
|
||||
|
||||
override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) {
|
||||
logger.log("[autofill-extension] prepare interface for registration request \(registrationRequest.description)")
|
||||
if let request = registrationRequest as? ASPasskeyCredentialRequest {
|
||||
if let passkeyIdentity = registrationRequest.credentialIdentity as? ASPasskeyCredentialIdentity {
|
||||
class CallbackImpl: PreparePasskeyRegistrationCallback {
|
||||
let ctx: ASCredentialProviderExtensionContext
|
||||
required init(_ ctx: ASCredentialProviderExtensionContext) {
|
||||
self.ctx = ctx
|
||||
}
|
||||
|
||||
// self.extensionContext.cancelRequest(withError: ExampleError.nope)
|
||||
}
|
||||
func onComplete(credential: PasskeyRegistrationResponse) {
|
||||
ctx.completeRegistrationRequest(using: ASPasskeyRegistrationCredential(
|
||||
relyingParty: credential.rpId,
|
||||
clientDataHash: credential.clientDataHash,
|
||||
credentialID: credential.credentialId,
|
||||
attestationObject: credential.attestationObject
|
||||
))
|
||||
}
|
||||
|
||||
override func prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) {
|
||||
logger.log("[autofill-extension] prepare interface for credential request \(credentialRequest.description)")
|
||||
func onError(error: BitwardenError) {
|
||||
ctx.cancelRequest(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
let userVerification = switch request.userVerificationPreference {
|
||||
case .preferred:
|
||||
UserVerification.preferred
|
||||
case .required:
|
||||
UserVerification.required
|
||||
default:
|
||||
UserVerification.discouraged
|
||||
}
|
||||
|
||||
let req = PasskeyRegistrationRequest(
|
||||
rpId: passkeyIdentity.relyingPartyIdentifier,
|
||||
userName: passkeyIdentity.userName,
|
||||
userHandle: passkeyIdentity.userHandle,
|
||||
clientDataHash: request.clientDataHash,
|
||||
userVerification: userVerification,
|
||||
supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) }
|
||||
)
|
||||
CredentialProviderViewController.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't get a passkey, return an error
|
||||
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid registration request"))
|
||||
}
|
||||
|
||||
/*
|
||||
Prepare your UI to list available credentials for the user to choose from. The items in
|
||||
'serviceIdentifiers' describe the service the user is logging in to, so your extension can
|
||||
prioritize the most relevant credentials in the list.
|
||||
*/
|
||||
Prepare your UI to list available credentials for the user to choose from. The items in
|
||||
'serviceIdentifiers' describe the service the user is logging in to, so your extension can
|
||||
prioritize the most relevant credentials in the list.
|
||||
*/
|
||||
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
|
||||
logger.log("[autofill-extension] prepareCredentialList for serviceIdentifiers: \(serviceIdentifiers.count)")
|
||||
|
||||
@ -77,18 +214,13 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
logger.log("[autofill-extension] prepareInterfaceToProvideCredential for credentialIdentity: \(credentialIdentity.user)")
|
||||
}
|
||||
|
||||
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier], requestParameters: ASPasskeyCredentialRequestParameters) {
|
||||
logger.log("[autofill-extension] prepareCredentialList(passkey) for serviceIdentifiers: \(serviceIdentifiers.count)")
|
||||
logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)")
|
||||
|
||||
for serviceIdentifier in serviceIdentifiers {
|
||||
logger.log(" service: \(serviceIdentifier.identifier)")
|
||||
}
|
||||
|
||||
logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,5 +6,9 @@
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -7,12 +7,16 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */; };
|
||||
3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */; };
|
||||
E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; };
|
||||
E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; };
|
||||
E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/macos_provider/BitwardenMacosProviderFFI.xcframework; sourceTree = "<group>"; };
|
||||
3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitwardenMacosProvider.swift; sourceTree = "<group>"; };
|
||||
968ED08A2C52A47200FFFEE6 /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = "<group>"; };
|
||||
E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; };
|
||||
@ -28,6 +32,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */,
|
||||
3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -56,6 +61,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */,
|
||||
3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@ -63,6 +69,7 @@
|
||||
E1DF71402B342F6900F29026 /* autofill-extension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */,
|
||||
E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */,
|
||||
E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */,
|
||||
E1DF71462B342F6900F29026 /* Info.plist */,
|
||||
@ -140,6 +147,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */,
|
||||
E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -23,7 +23,7 @@
|
||||
"build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"",
|
||||
"build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js",
|
||||
"build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch",
|
||||
"build:macos-extension": "node scripts/build-macos-extension.js",
|
||||
"build:macos-extension": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js",
|
||||
"build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js",
|
||||
"build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js",
|
||||
"build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js --watch",
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||
import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect";
|
||||
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
@ -40,6 +41,11 @@ import {
|
||||
TwoFactorTimeoutIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import {
|
||||
NewDeviceVerificationNoticePageOneComponent,
|
||||
NewDeviceVerificationNoticePageTwoComponent,
|
||||
VaultIcons,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap";
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
@ -116,10 +122,37 @@ const routes: Routes = [
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{ path: "register", component: RegisterComponent },
|
||||
{
|
||||
path: "new-device-notice",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
canActivate: [],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: NewDeviceVerificationNoticePageOneComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.ExclamationTriangle,
|
||||
pageTitle: {
|
||||
key: "importantNotice",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "setup",
|
||||
component: NewDeviceVerificationNoticePageTwoComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.UserLock,
|
||||
pageTitle: {
|
||||
key: "setupTwoStepLogin",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "vault",
|
||||
component: VaultComponent,
|
||||
canActivate: [authGuard],
|
||||
canActivate: [authGuard, NewDeviceVerificationNoticeGuard],
|
||||
},
|
||||
{ path: "accessibility-cookie", component: AccessibilityCookieComponent },
|
||||
{ path: "set-password", component: SetPasswordComponent },
|
||||
|
@ -56,6 +56,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
|
||||
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
@ -73,6 +75,7 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling";
|
||||
import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
@ -80,6 +83,7 @@ import { SystemService } from "@bitwarden/common/platform/services/system.servic
|
||||
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
||||
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@ -97,6 +101,7 @@ import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-l
|
||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service";
|
||||
import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
|
||||
import { flagEnabled } from "../../platform/flags";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
@ -309,7 +314,29 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DesktopAutofillService,
|
||||
deps: [LogService, CipherServiceAbstraction, ConfigService],
|
||||
deps: [
|
||||
LogService,
|
||||
CipherServiceAbstraction,
|
||||
ConfigService,
|
||||
Fido2AuthenticatorServiceAbstraction,
|
||||
AccountService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: Fido2UserInterfaceServiceAbstraction,
|
||||
useClass: DesktopFido2UserInterfaceService,
|
||||
deps: [AuthServiceAbstraction, CipherServiceAbstraction, AccountService, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: Fido2AuthenticatorServiceAbstraction,
|
||||
useClass: Fido2AuthenticatorService,
|
||||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
Fido2UserInterfaceServiceAbstraction,
|
||||
SyncService,
|
||||
AccountService,
|
||||
LogService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: NativeMessagingManifestService,
|
||||
|
@ -1,9 +1,92 @@
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
import type { autofill } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { Command } from "../platform/main/autofill/command";
|
||||
import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main";
|
||||
|
||||
export default {
|
||||
runCommand: <C extends Command>(params: RunCommandParams<C>): Promise<RunCommandResult<C>> =>
|
||||
ipcRenderer.invoke("autofill.runCommand", params),
|
||||
|
||||
listenPasskeyRegistration: (
|
||||
fn: (
|
||||
clientId: number,
|
||||
sequenceNumber: number,
|
||||
request: autofill.PasskeyRegistrationRequest,
|
||||
completeCallback: (
|
||||
error: Error | null,
|
||||
response: autofill.PasskeyRegistrationResponse,
|
||||
) => void,
|
||||
) => void,
|
||||
) => {
|
||||
ipcRenderer.on(
|
||||
"autofill.passkeyRegistration",
|
||||
(
|
||||
event,
|
||||
data: {
|
||||
clientId: number;
|
||||
sequenceNumber: number;
|
||||
request: autofill.PasskeyRegistrationRequest;
|
||||
},
|
||||
) => {
|
||||
const { clientId, sequenceNumber, request } = data;
|
||||
fn(clientId, sequenceNumber, request, (error, response) => {
|
||||
if (error) {
|
||||
ipcRenderer.send("autofill.completeError", {
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
error: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.send("autofill.completePasskeyRegistration", {
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
response,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
listenPasskeyAssertion: (
|
||||
fn: (
|
||||
clientId: number,
|
||||
sequenceNumber: number,
|
||||
request: autofill.PasskeyAssertionRequest,
|
||||
completeCallback: (error: Error | null, response: autofill.PasskeyAssertionResponse) => void,
|
||||
) => void,
|
||||
) => {
|
||||
ipcRenderer.on(
|
||||
"autofill.passkeyAssertion",
|
||||
(
|
||||
event,
|
||||
data: {
|
||||
clientId: number;
|
||||
sequenceNumber: number;
|
||||
request: autofill.PasskeyAssertionRequest;
|
||||
},
|
||||
) => {
|
||||
const { clientId, sequenceNumber, request } = data;
|
||||
fn(clientId, sequenceNumber, request, (error, response) => {
|
||||
if (error) {
|
||||
ipcRenderer.send("autofill.completeError", {
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
error: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.send("autofill.completePasskeyAssertion", {
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
response,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@ -1,12 +1,32 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { EMPTY, Subject, distinctUntilChanged, mergeMap, switchMap, takeUntil } from "rxjs";
|
||||
import { autofill } from "desktop_native/napi";
|
||||
import {
|
||||
EMPTY,
|
||||
Subject,
|
||||
distinctUntilChanged,
|
||||
firstValueFrom,
|
||||
map,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
Fido2AuthenticatorGetAssertionParams,
|
||||
Fido2AuthenticatorGetAssertionResult,
|
||||
Fido2AuthenticatorMakeCredentialResult,
|
||||
Fido2AuthenticatorMakeCredentialsParams,
|
||||
Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction,
|
||||
} from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils";
|
||||
import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils";
|
||||
import { guidToRawFormat } from "@bitwarden/common/platform/services/fido2/guid-utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@ -26,6 +46,8 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
private logService: LogService,
|
||||
private cipherService: CipherService,
|
||||
private configService: ConfigService,
|
||||
private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<void>,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
@ -47,6 +69,8 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.listenIpc();
|
||||
}
|
||||
|
||||
/** Give metadata about all available credentials in the users vault */
|
||||
@ -114,6 +138,146 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
listenIpc() {
|
||||
ipc.autofill.listenPasskeyRegistration((clientId, sequenceNumber, request, callback) => {
|
||||
this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request);
|
||||
this.logService.warning(
|
||||
"listenPasskeyRegistration2",
|
||||
this.convertRegistrationRequest(request),
|
||||
);
|
||||
|
||||
const controller = new AbortController();
|
||||
void this.fido2AuthenticatorService
|
||||
.makeCredential(this.convertRegistrationRequest(request), null, controller)
|
||||
.then((response) => {
|
||||
callback(null, this.convertRegistrationResponse(request, response));
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logService.error("listenPasskeyRegistration error", error);
|
||||
callback(error, null);
|
||||
});
|
||||
});
|
||||
|
||||
ipc.autofill.listenPasskeyAssertion(async (clientId, sequenceNumber, request, callback) => {
|
||||
this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request);
|
||||
|
||||
// TODO: For some reason the credentialId is passed as an empty array in the request, so we need to
|
||||
// get it from the cipher. For that we use the recordIdentifier, which is the cipherId.
|
||||
if (request.recordIdentifier && request.credentialId.length === 0) {
|
||||
const cipher = await this.cipherService.get(request.recordIdentifier);
|
||||
if (!cipher) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Cipher not found");
|
||||
callback(new Error("Cipher not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const decrypted = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
|
||||
const fido2Credential = decrypted.login.fido2Credentials?.[0];
|
||||
if (!fido2Credential) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found");
|
||||
callback(new Error("Fido2Credential not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
request.credentialId = Array.from(
|
||||
guidToRawFormat(decrypted.login.fido2Credentials?.[0].credentialId),
|
||||
);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
void this.fido2AuthenticatorService
|
||||
.getAssertion(this.convertAssertionRequest(request), null, controller)
|
||||
.then((response) => {
|
||||
callback(null, this.convertAssertionResponse(request, response));
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logService.error("listenPasskeyAssertion error", error);
|
||||
callback(error, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private convertRegistrationRequest(
|
||||
request: autofill.PasskeyRegistrationRequest,
|
||||
): Fido2AuthenticatorMakeCredentialsParams {
|
||||
return {
|
||||
hash: new Uint8Array(request.clientDataHash),
|
||||
rpEntity: {
|
||||
name: request.rpId,
|
||||
id: request.rpId,
|
||||
},
|
||||
userEntity: {
|
||||
id: new Uint8Array(request.userHandle),
|
||||
name: request.userName,
|
||||
displayName: undefined,
|
||||
icon: undefined,
|
||||
},
|
||||
credTypesAndPubKeyAlgs: request.supportedAlgorithms.map((alg) => ({
|
||||
alg,
|
||||
type: "public-key",
|
||||
})),
|
||||
excludeCredentialDescriptorList: [],
|
||||
requireResidentKey: true,
|
||||
requireUserVerification:
|
||||
request.userVerification === "required" || request.userVerification === "preferred",
|
||||
fallbackSupported: false,
|
||||
};
|
||||
}
|
||||
|
||||
private convertRegistrationResponse(
|
||||
request: autofill.PasskeyRegistrationRequest,
|
||||
response: Fido2AuthenticatorMakeCredentialResult,
|
||||
): autofill.PasskeyRegistrationResponse {
|
||||
return {
|
||||
rpId: request.rpId,
|
||||
clientDataHash: request.clientDataHash,
|
||||
credentialId: Array.from(Fido2Utils.bufferSourceToUint8Array(response.credentialId)),
|
||||
attestationObject: Array.from(
|
||||
Fido2Utils.bufferSourceToUint8Array(response.attestationObject),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private convertAssertionRequest(
|
||||
request: autofill.PasskeyAssertionRequest,
|
||||
): Fido2AuthenticatorGetAssertionParams {
|
||||
return {
|
||||
rpId: request.rpId,
|
||||
hash: new Uint8Array(request.clientDataHash),
|
||||
allowCredentialDescriptorList: [
|
||||
{
|
||||
id: new Uint8Array(request.credentialId),
|
||||
type: "public-key",
|
||||
},
|
||||
],
|
||||
extensions: {},
|
||||
requireUserVerification:
|
||||
request.userVerification === "required" || request.userVerification === "preferred",
|
||||
fallbackSupported: false,
|
||||
};
|
||||
}
|
||||
|
||||
private convertAssertionResponse(
|
||||
request: autofill.PasskeyAssertionRequest,
|
||||
response: Fido2AuthenticatorGetAssertionResult,
|
||||
): autofill.PasskeyAssertionResponse {
|
||||
return {
|
||||
userHandle: Array.from(response.selectedCredential.userHandle),
|
||||
rpId: request.rpId,
|
||||
signature: Array.from(response.signature),
|
||||
clientDataHash: request.clientDataHash,
|
||||
authenticatorData: Array.from(response.authenticatorData),
|
||||
credentialId: Array.from(response.selectedCredential.id),
|
||||
};
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
@ -0,0 +1,123 @@
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import {
|
||||
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
|
||||
Fido2UserInterfaceSession,
|
||||
NewCredentialParams,
|
||||
PickCredentialParams,
|
||||
} from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType, CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
||||
|
||||
export class DesktopFido2UserInterfaceService
|
||||
implements Fido2UserInterfaceServiceAbstraction<void>
|
||||
{
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private cipherService: CipherService,
|
||||
private accountService: AccountService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async newSession(
|
||||
fallbackSupported: boolean,
|
||||
_tab: void,
|
||||
abortController?: AbortController,
|
||||
): Promise<DesktopFido2UserInterfaceSession> {
|
||||
this.logService.warning("newSession", fallbackSupported, abortController);
|
||||
return new DesktopFido2UserInterfaceSession(
|
||||
this.authService,
|
||||
this.cipherService,
|
||||
this.accountService,
|
||||
this.logService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSession {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private cipherService: CipherService,
|
||||
private accountService: AccountService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async pickCredential({
|
||||
cipherIds,
|
||||
userVerification,
|
||||
}: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
||||
this.logService.warning("pickCredential", cipherIds, userVerification);
|
||||
|
||||
return { cipherId: cipherIds[0], userVerified: userVerification };
|
||||
}
|
||||
|
||||
async confirmNewCredential({
|
||||
credentialName,
|
||||
userName,
|
||||
userVerification,
|
||||
rpId,
|
||||
}: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
||||
this.logService.warning(
|
||||
"confirmNewCredential",
|
||||
credentialName,
|
||||
userName,
|
||||
userVerification,
|
||||
rpId,
|
||||
);
|
||||
|
||||
// Store the passkey on a new cipher to avoid replacing something important
|
||||
const cipher = new CipherView();
|
||||
cipher.name = credentialName;
|
||||
|
||||
cipher.type = CipherType.Login;
|
||||
cipher.login = new LoginView();
|
||||
cipher.login.username = userName;
|
||||
cipher.login.uris = [new LoginUriView()];
|
||||
cipher.login.uris[0].uri = "https://" + rpId;
|
||||
cipher.card = new CardView();
|
||||
cipher.identity = new IdentityView();
|
||||
cipher.secureNote = new SecureNoteView();
|
||||
cipher.secureNote.type = SecureNoteType.Generic;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const encCipher = await this.cipherService.encrypt(cipher, activeUserId);
|
||||
const createdCipher = await this.cipherService.createWithServer(encCipher);
|
||||
|
||||
return { cipherId: createdCipher.id, userVerified: userVerification };
|
||||
}
|
||||
|
||||
async informExcludedCredential(existingCipherIds: string[]): Promise<void> {
|
||||
this.logService.warning("informExcludedCredential", existingCipherIds);
|
||||
}
|
||||
|
||||
async ensureUnlockedVault(): Promise<void> {
|
||||
this.logService.warning("ensureUnlockedVault");
|
||||
|
||||
const status = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
if (status !== AuthenticationStatus.Unlocked) {
|
||||
throw new Error("Vault is not unlocked");
|
||||
}
|
||||
}
|
||||
|
||||
async informCredentialNotFound(): Promise<void> {
|
||||
this.logService.warning("informCredentialNotFound");
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.logService.warning("close");
|
||||
}
|
||||
}
|
@ -3394,5 +3394,41 @@
|
||||
},
|
||||
"fileSavedToDevice": {
|
||||
"message": "File saved to device. Manage from your device downloads."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
}
|
||||
}
|
||||
|
@ -261,7 +261,7 @@ export class Main {
|
||||
new EphemeralValueStorageService();
|
||||
new SSOLocalhostCallbackService(this.environmentService, this.messagingService);
|
||||
|
||||
this.nativeAutofillMain = new NativeAutofillMain(this.logService);
|
||||
this.nativeAutofillMain = new NativeAutofillMain(this.logService, this.windowMain);
|
||||
void this.nativeAutofillMain.init();
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@ import { ipcMain } from "electron";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { autofill } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { WindowMain } from "../../../main/window.main";
|
||||
|
||||
import { CommandDefinition } from "./command";
|
||||
|
||||
export type RunCommandParams<C extends CommandDefinition> = {
|
||||
@ -14,7 +16,12 @@ export type RunCommandParams<C extends CommandDefinition> = {
|
||||
export type RunCommandResult<C extends CommandDefinition> = C["output"];
|
||||
|
||||
export class NativeAutofillMain {
|
||||
constructor(private logService: LogService) {}
|
||||
private ipcServer: autofill.IpcServer | null;
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private windowMain: WindowMain,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
ipcMain.handle(
|
||||
@ -26,6 +33,52 @@ export class NativeAutofillMain {
|
||||
return this.runCommand(params);
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer = await autofill.IpcServer.listen(
|
||||
"autofill",
|
||||
// RegistrationCallback
|
||||
(error, clientId, sequenceNumber, request) => {
|
||||
if (error) {
|
||||
this.logService.error("autofill.IpcServer.registration", error);
|
||||
return;
|
||||
}
|
||||
this.windowMain.win.webContents.send("autofill.passkeyRegistration", {
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
request,
|
||||
});
|
||||
},
|
||||
// AssertionCallback
|
||||
(error, clientId, sequenceNumber, request) => {
|
||||
if (error) {
|
||||
this.logService.error("autofill.IpcServer.assertion", error);
|
||||
return;
|
||||
}
|
||||
this.windowMain.win.webContents.send("autofill.passkeyAssertion", {
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
request,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on("autofill.completePasskeyRegistration", (event, data) => {
|
||||
this.logService.warning("autofill.completePasskeyRegistration", data);
|
||||
const { clientId, sequenceNumber, response } = data;
|
||||
this.ipcServer.completeRegistration(clientId, sequenceNumber, response);
|
||||
});
|
||||
|
||||
ipcMain.on("autofill.completePasskeyAssertion", (event, data) => {
|
||||
this.logService.warning("autofill.completePasskeyAssertion", data);
|
||||
const { clientId, sequenceNumber, response } = data;
|
||||
this.ipcServer.completeAssertion(clientId, sequenceNumber, response);
|
||||
});
|
||||
|
||||
ipcMain.on("autofill.completeError", (event, data) => {
|
||||
this.logService.warning("autofill.completeError", data);
|
||||
const { clientId, sequenceNumber, error } = data;
|
||||
this.ipcServer.completeAssertion(clientId, sequenceNumber, error);
|
||||
});
|
||||
}
|
||||
|
||||
private async runCommand<C extends CommandDefinition>(
|
||||
|
@ -6,6 +6,7 @@ config.content = [
|
||||
"../../libs/components/src/**/*.{html,ts}",
|
||||
"../../libs/auth/src/**/*.{html,ts}",
|
||||
"../../libs/angular/src/**/*.{html,ts}",
|
||||
"../../libs/vault/src/**/*.{html,ts,mdx}",
|
||||
];
|
||||
|
||||
module.exports = config;
|
||||
|
@ -4,16 +4,22 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwarden/vault";
|
||||
|
||||
import { EmergencyAccessService } from "../../../emergency-access";
|
||||
import { EmergencyAccessAttachmentsComponent } from "../attachments/emergency-access-attachments.component";
|
||||
|
||||
import { EmergencyAddEditCipherComponent } from "./emergency-add-edit-cipher.component";
|
||||
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access-view",
|
||||
templateUrl: "emergency-access-view.component.html",
|
||||
providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class EmergencyAccessViewComponent implements OnInit {
|
||||
@ -31,6 +37,8 @@ export class EmergencyAccessViewComponent implements OnInit {
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private emergencyAccessService: EmergencyAccessService,
|
||||
private configService: ConfigService,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@ -49,6 +57,19 @@ export class EmergencyAccessViewComponent implements OnInit {
|
||||
}
|
||||
|
||||
async selectCipher(cipher: CipherView) {
|
||||
const browserRefreshEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.ExtensionRefresh,
|
||||
);
|
||||
|
||||
if (browserRefreshEnabled) {
|
||||
EmergencyViewDialogComponent.open(this.dialogService, {
|
||||
cipher,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME PM-15385: Remove below dialog service logic once extension refresh is live.
|
||||
|
||||
// eslint-disable-next-line
|
||||
const [_, childComponent] = await this.modalService.openViewRef(
|
||||
EmergencyAddEditCipherComponent,
|
||||
|
@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
@ -30,7 +30,7 @@ import { AddEditComponent as BaseAddEditComponent } from "../../../../vault/indi
|
||||
selector: "app-org-vault-add-edit",
|
||||
templateUrl: "../../../../vault/individual-vault/add-edit.component.html",
|
||||
})
|
||||
export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
|
||||
export class EmergencyAddEditCipherComponent extends BaseAddEditComponent implements OnInit {
|
||||
originalCipher: Cipher = null;
|
||||
viewOnly = true;
|
||||
protected override componentName = "app-org-vault-add-edit";
|
||||
@ -85,6 +85,14 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
|
||||
this.title = this.i18nService.t("viewItem");
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await super.ngOnInit();
|
||||
// The base component `ngOnInit` calculates the `viewOnly` property based on cipher properties
|
||||
// In the case of emergency access, `viewOnly` should always be true, set it manually here after
|
||||
// the base `ngOnInit` is complete.
|
||||
this.viewOnly = true;
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
return Promise.resolve(this.originalCipher);
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
<bit-dialog dialogSize="large" background="alt" #dialog>
|
||||
<span bitDialogTitle aria-live="polite">
|
||||
{{ title }}
|
||||
</span>
|
||||
<div bitDialogContent #dialogContent>
|
||||
<app-cipher-view [cipher]="cipher"></app-cipher-view>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
@ -0,0 +1,108 @@
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
|
||||
|
||||
describe("EmergencyViewDialogComponent", () => {
|
||||
let component: EmergencyViewDialogComponent;
|
||||
let fixture: ComponentFixture<EmergencyViewDialogComponent>;
|
||||
|
||||
const open = jest.fn();
|
||||
const close = jest.fn();
|
||||
|
||||
const mockCipher = {
|
||||
id: "cipher1",
|
||||
name: "Cipher",
|
||||
type: CipherType.Login,
|
||||
login: { uris: [] },
|
||||
card: {},
|
||||
} as CipherView;
|
||||
|
||||
beforeEach(async () => {
|
||||
open.mockClear();
|
||||
close.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EmergencyViewDialogComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
|
||||
{ provide: CollectionService, useValue: mock<CollectionService>() },
|
||||
{ provide: FolderService, useValue: mock<FolderService>() },
|
||||
{ provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } },
|
||||
{ provide: DialogService, useValue: { open } },
|
||||
{ provide: DialogRef, useValue: { close } },
|
||||
{ provide: DIALOG_DATA, useValue: { cipher: mockCipher } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EmergencyViewDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("creates", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens dialog", () => {
|
||||
EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher });
|
||||
|
||||
expect(open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes the dialog", () => {
|
||||
EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher });
|
||||
fixture.detectChanges();
|
||||
|
||||
const cancelButton = fixture.debugElement.queryAll(By.css("button")).pop();
|
||||
|
||||
cancelButton.nativeElement.click();
|
||||
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("updateTitle", () => {
|
||||
it("sets login title", () => {
|
||||
mockCipher.type = CipherType.Login;
|
||||
|
||||
component["updateTitle"]();
|
||||
|
||||
expect(component["title"]).toBe("viewItemType typelogin");
|
||||
});
|
||||
|
||||
it("sets card title", () => {
|
||||
mockCipher.type = CipherType.Card;
|
||||
|
||||
component["updateTitle"]();
|
||||
|
||||
expect(component["title"]).toBe("viewItemType typecard");
|
||||
});
|
||||
|
||||
it("sets identity title", () => {
|
||||
mockCipher.type = CipherType.Identity;
|
||||
|
||||
component["updateTitle"]();
|
||||
|
||||
expect(component["title"]).toBe("viewItemType typeidentity");
|
||||
});
|
||||
|
||||
it("sets note title", () => {
|
||||
mockCipher.type = CipherType.SecureNote;
|
||||
|
||||
component["updateTitle"]();
|
||||
|
||||
expect(component["title"]).toBe("viewItemType note");
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,90 @@
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import { CipherViewComponent } from "@bitwarden/vault";
|
||||
|
||||
import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service";
|
||||
|
||||
export interface EmergencyViewDialogParams {
|
||||
/** The cipher being viewed. */
|
||||
cipher: CipherView;
|
||||
}
|
||||
|
||||
/** Stubbed class, premium upgrade is not applicable for emergency viewing */
|
||||
class PremiumUpgradePromptNoop implements PremiumUpgradePromptService {
|
||||
async promptForPremium() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-emergency-view-dialog",
|
||||
templateUrl: "emergency-view-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [ButtonModule, CipherViewComponent, DialogModule, CommonModule, JslibModule],
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop },
|
||||
],
|
||||
})
|
||||
export class EmergencyViewDialogComponent {
|
||||
/**
|
||||
* The title of the dialog. Updates based on the cipher type.
|
||||
* @protected
|
||||
*/
|
||||
protected title: string = "";
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: EmergencyViewDialogParams,
|
||||
private dialogRef: DialogRef,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
get cipher(): CipherView {
|
||||
return this.params.cipher;
|
||||
}
|
||||
|
||||
cancel = () => {
|
||||
this.dialogRef.close();
|
||||
};
|
||||
|
||||
private updateTitle() {
|
||||
const partOne = "viewItemType";
|
||||
|
||||
const type = this.cipher.type;
|
||||
|
||||
switch (type) {
|
||||
case CipherType.Login:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase());
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase());
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase());
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the EmergencyViewDialog.
|
||||
*/
|
||||
static open(dialogService: DialogService, params: EmergencyViewDialogParams) {
|
||||
return dialogService.open<EmergencyViewDialogParams>(EmergencyViewDialogComponent, {
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import {
|
||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||
import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap";
|
||||
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
|
||||
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
@ -40,6 +41,11 @@ import {
|
||||
LoginDecryptionOptionsComponent,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import {
|
||||
NewDeviceVerificationNoticePageOneComponent,
|
||||
NewDeviceVerificationNoticePageTwoComponent,
|
||||
VaultIcons,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap";
|
||||
import { flagEnabled, Flags } from "../utils/flags";
|
||||
@ -695,10 +701,37 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "new-device-notice",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
canActivate: [],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: NewDeviceVerificationNoticePageOneComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.ExclamationTriangle,
|
||||
pageTitle: {
|
||||
key: "importantNotice",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "setup",
|
||||
component: NewDeviceVerificationNoticePageTwoComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.UserLock,
|
||||
pageTitle: {
|
||||
key: "setupTwoStepLogin",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: UserLayoutComponent,
|
||||
canActivate: [deepLinkGuard(), authGuard],
|
||||
canActivate: [deepLinkGuard(), authGuard, NewDeviceVerificationNoticeGuard],
|
||||
children: [
|
||||
{
|
||||
path: "vault",
|
||||
|
@ -3,7 +3,7 @@
|
||||
{{ "passwordHistory" | i18n }}
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<vault-password-history-view [cipherId]="cipherId" />
|
||||
<vault-password-history-view [cipher]="cipher" />
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton (click)="close()" buttonType="primary" type="button">
|
||||
|
@ -4,8 +4,7 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Inject, Component } from "@angular/core";
|
||||
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { AsyncActionsModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import { PasswordHistoryViewComponent } from "@bitwarden/vault";
|
||||
|
||||
@ -15,7 +14,7 @@ import { SharedModule } from "../../shared/shared.module";
|
||||
* The parameters for the password history dialog.
|
||||
*/
|
||||
export interface ViewPasswordHistoryDialogParams {
|
||||
cipherId: CipherId;
|
||||
cipher: CipherView;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,14 +34,9 @@ export interface ViewPasswordHistoryDialogParams {
|
||||
})
|
||||
export class PasswordHistoryComponent {
|
||||
/**
|
||||
* The ID of the cipher to display the password history for.
|
||||
* The cipher to display the password history for.
|
||||
*/
|
||||
cipherId: CipherId;
|
||||
|
||||
/**
|
||||
* The password history for the cipher.
|
||||
*/
|
||||
history: PasswordHistoryView[] = [];
|
||||
cipher: CipherView;
|
||||
|
||||
/**
|
||||
* The constructor for the password history dialog component.
|
||||
@ -54,9 +48,9 @@ export class PasswordHistoryComponent {
|
||||
private dialogRef: DialogRef<PasswordHistoryComponent>,
|
||||
) {
|
||||
/**
|
||||
* Set the cipher ID from the parameters.
|
||||
* Set the cipher from the parameters.
|
||||
*/
|
||||
this.cipherId = params.cipherId;
|
||||
this.cipher = params.cipher;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Overlay } from "@angular/cdk/overlay";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { openPasswordHistoryDialog } from "../individual-vault/password-history.component";
|
||||
@ -35,10 +35,10 @@ describe("WebViewPasswordHistoryService", () => {
|
||||
|
||||
describe("viewPasswordHistory", () => {
|
||||
it("calls openPasswordHistoryDialog with the correct parameters", async () => {
|
||||
const mockCipherId = "cipher-id" as CipherId;
|
||||
await service.viewPasswordHistory(mockCipherId);
|
||||
const mockCipher = { id: "cipher-id" } as CipherView;
|
||||
await service.viewPasswordHistory(mockCipher);
|
||||
expect(openPasswordHistoryDialog).toHaveBeenCalledWith(dialogService, {
|
||||
data: { cipherId: mockCipherId },
|
||||
data: { cipher: mockCipher },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ViewPasswordHistoryService } from "../../../../../../libs/common/src/vault/abstractions/view-password-history.service";
|
||||
@ -17,7 +17,7 @@ export class WebViewPasswordHistoryService implements ViewPasswordHistoryService
|
||||
* Opens the password history dialog for the given cipher ID.
|
||||
* @param cipherId The ID of the cipher to view the password history for.
|
||||
*/
|
||||
async viewPasswordHistory(cipherId: CipherId) {
|
||||
openPasswordHistoryDialog(this.dialogService, { data: { cipherId } });
|
||||
async viewPasswordHistory(cipher: CipherView) {
|
||||
openPasswordHistoryDialog(this.dialogService, { data: { cipher } });
|
||||
}
|
||||
}
|
||||
|
@ -9888,6 +9888,42 @@
|
||||
"descriptorCode": {
|
||||
"message": "Descriptor code"
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"removeMembers": {
|
||||
"message": "Remove members"
|
||||
},
|
||||
|
@ -298,6 +298,7 @@ import {
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
|
||||
import { NewDeviceVerificationNoticeService } from "../../../vault/src/services/new-device-verification-notice.service";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { ViewCacheService } from "../platform/abstractions/view-cache.service";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
@ -1401,6 +1402,7 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultLoginDecryptionOptionsService,
|
||||
deps: [MessagingServiceAbstraction],
|
||||
}),
|
||||
safeProvider(NewDeviceVerificationNoticeService),
|
||||
safeProvider({
|
||||
provide: UserAsymmetricKeysRegenerationApiService,
|
||||
useClass: DefaultUserAsymmetricKeysRegenerationApiService,
|
||||
|
1
libs/angular/src/vault/guards/index.ts
Normal file
1
libs/angular/src/vault/guards/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./new-device-verification-notice.guard";
|
@ -0,0 +1,225 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service";
|
||||
import { VaultProfileService } from "../services/vault-profile.service";
|
||||
|
||||
import { NewDeviceVerificationNoticeGuard } from "./new-device-verification-notice.guard";
|
||||
|
||||
describe("NewDeviceVerificationNoticeGuard", () => {
|
||||
const _state = Object.freeze({}) as RouterStateSnapshot;
|
||||
const emptyRoute = Object.freeze({ queryParams: {} }) as ActivatedRouteSnapshot;
|
||||
const eightDaysAgo = new Date();
|
||||
eightDaysAgo.setDate(eightDaysAgo.getDate() - 8);
|
||||
|
||||
const account = {
|
||||
id: "account-id",
|
||||
} as unknown as Account;
|
||||
|
||||
const activeAccount$ = new BehaviorSubject<Account | null>(account);
|
||||
|
||||
const createUrlTree = jest.fn();
|
||||
const getFeatureFlag = jest.fn().mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationTemporaryDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
const isSelfHost = jest.fn().mockResolvedValue(false);
|
||||
const getProfileTwoFactorEnabled = jest.fn().mockResolvedValue(false);
|
||||
const policyAppliesToActiveUser$ = jest.fn().mockReturnValue(new BehaviorSubject<boolean>(false));
|
||||
const noticeState$ = jest.fn().mockReturnValue(new BehaviorSubject(null));
|
||||
const getProfileCreationDate = jest.fn().mockResolvedValue(eightDaysAgo);
|
||||
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockClear();
|
||||
isSelfHost.mockClear();
|
||||
getProfileCreationDate.mockClear();
|
||||
getProfileTwoFactorEnabled.mockClear();
|
||||
policyAppliesToActiveUser$.mockClear();
|
||||
createUrlTree.mockClear();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: Router, useValue: { createUrlTree } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
{ provide: NewDeviceVerificationNoticeService, useValue: { noticeState$ } },
|
||||
{ provide: AccountService, useValue: { activeAccount$ } },
|
||||
{ provide: PlatformUtilsService, useValue: { isSelfHost } },
|
||||
{ provide: PolicyService, useValue: { policyAppliesToActiveUser$ } },
|
||||
{
|
||||
provide: VaultProfileService,
|
||||
useValue: { getProfileCreationDate, getProfileTwoFactorEnabled },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
function newDeviceGuard(route?: ActivatedRouteSnapshot) {
|
||||
// Run the guard within injection context so `inject` works as you'd expect
|
||||
// Pass state object to make TypeScript happy
|
||||
return TestBed.runInInjectionContext(async () =>
|
||||
NewDeviceVerificationNoticeGuard(route ?? emptyRoute, _state),
|
||||
);
|
||||
}
|
||||
|
||||
describe("fromNewDeviceVerification", () => {
|
||||
const route = {
|
||||
queryParams: { fromNewDeviceVerification: "true" },
|
||||
} as unknown as ActivatedRouteSnapshot;
|
||||
|
||||
it("returns `true` when `fromNewDeviceVerification` is present", async () => {
|
||||
expect(await newDeviceGuard(route)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not execute other logic", async () => {
|
||||
// `fromNewDeviceVerification` param should exit early,
|
||||
// not foolproof but a quick way to test that other logic isn't executed
|
||||
await newDeviceGuard(route);
|
||||
|
||||
expect(getFeatureFlag).not.toHaveBeenCalled();
|
||||
expect(isSelfHost).not.toHaveBeenCalled();
|
||||
expect(getProfileTwoFactorEnabled).not.toHaveBeenCalled();
|
||||
expect(getProfileCreationDate).not.toHaveBeenCalled();
|
||||
expect(policyAppliesToActiveUser$).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("missing current account", () => {
|
||||
afterAll(() => {
|
||||
// reset `activeAccount$` observable
|
||||
activeAccount$.next(account);
|
||||
});
|
||||
|
||||
it("redirects to login when account is missing", async () => {
|
||||
activeAccount$.next(null);
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/login"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns `true` when 2FA is enabled", async () => {
|
||||
getProfileTwoFactorEnabled.mockResolvedValueOnce(true);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `true` when the user is self hosted", async () => {
|
||||
isSelfHost.mockReturnValueOnce(true);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `true` SSO is required", async () => {
|
||||
policyAppliesToActiveUser$.mockReturnValueOnce(new BehaviorSubject(true));
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
expect(policyAppliesToActiveUser$).toHaveBeenCalledWith(PolicyType.RequireSso);
|
||||
});
|
||||
|
||||
it("returns `true` when the profile was created less than a week ago", async () => {
|
||||
const sixDaysAgo = new Date();
|
||||
sixDaysAgo.setDate(sixDaysAgo.getDate() - 6);
|
||||
|
||||
getProfileCreationDate.mockResolvedValueOnce(sixDaysAgo);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
describe("temp flag", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationTemporaryDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
getFeatureFlag.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("redirects to notice when the user has not dismissed it", async () => {
|
||||
noticeState$.mockReturnValueOnce(new BehaviorSubject(null));
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
expect(noticeState$).toHaveBeenCalledWith(account.id);
|
||||
});
|
||||
|
||||
it("redirects to notice when the user dismissed it more than 7 days ago", async () => {
|
||||
const eighteenDaysAgo = new Date();
|
||||
eighteenDaysAgo.setDate(eighteenDaysAgo.getDate() - 18);
|
||||
|
||||
noticeState$.mockReturnValueOnce(
|
||||
new BehaviorSubject({ last_dismissal: eighteenDaysAgo.toISOString() }),
|
||||
);
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
});
|
||||
|
||||
it("returns true when the user dismissed less than 7 days ago", async () => {
|
||||
const fourDaysAgo = new Date();
|
||||
fourDaysAgo.setDate(fourDaysAgo.getDate() - 4);
|
||||
|
||||
noticeState$.mockReturnValueOnce(
|
||||
new BehaviorSubject({ last_dismissal: fourDaysAgo.toISOString() }),
|
||||
);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("permanent flag", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
getFeatureFlag.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("redirects when the user has not dismissed", async () => {
|
||||
noticeState$.mockReturnValueOnce(new BehaviorSubject(null));
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
|
||||
noticeState$.mockReturnValueOnce(new BehaviorSubject({ permanent_dismissal: null }));
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledTimes(2);
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
});
|
||||
|
||||
it("returns `true` when the user has dismissed", async () => {
|
||||
noticeState$.mockReturnValueOnce(new BehaviorSubject({ permanent_dismissal: true }));
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,121 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivateFn, Router } from "@angular/router";
|
||||
import { Observable, firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service";
|
||||
import { VaultProfileService } from "../services/vault-profile.service";
|
||||
|
||||
export const NewDeviceVerificationNoticeGuard: CanActivateFn = async (
|
||||
route: ActivatedRouteSnapshot,
|
||||
) => {
|
||||
const router = inject(Router);
|
||||
const configService = inject(ConfigService);
|
||||
const newDeviceVerificationNoticeService = inject(NewDeviceVerificationNoticeService);
|
||||
const accountService = inject(AccountService);
|
||||
const platformUtilsService = inject(PlatformUtilsService);
|
||||
const policyService = inject(PolicyService);
|
||||
const vaultProfileService = inject(VaultProfileService);
|
||||
|
||||
if (route.queryParams["fromNewDeviceVerification"]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tempNoticeFlag = await configService.getFeatureFlag(
|
||||
FeatureFlag.NewDeviceVerificationTemporaryDismiss,
|
||||
);
|
||||
const permNoticeFlag = await configService.getFeatureFlag(
|
||||
FeatureFlag.NewDeviceVerificationPermanentDismiss,
|
||||
);
|
||||
|
||||
if (!tempNoticeFlag && !permNoticeFlag) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentAcct$: Observable<Account | null> = accountService.activeAccount$;
|
||||
const currentAcct = await firstValueFrom(currentAcct$);
|
||||
|
||||
if (!currentAcct) {
|
||||
return router.createUrlTree(["/login"]);
|
||||
}
|
||||
|
||||
const has2FAEnabled = await hasATwoFactorProviderEnabled(vaultProfileService, currentAcct.id);
|
||||
const isSelfHosted = await platformUtilsService.isSelfHost();
|
||||
const requiresSSO = await isSSORequired(policyService);
|
||||
const isProfileLessThanWeekOld = await profileIsLessThanWeekOld(
|
||||
vaultProfileService,
|
||||
currentAcct.id,
|
||||
);
|
||||
|
||||
// When any of the following are true, the device verification notice is
|
||||
// not applicable for the user.
|
||||
if (has2FAEnabled || isSelfHosted || requiresSSO || isProfileLessThanWeekOld) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userItems$ = newDeviceVerificationNoticeService.noticeState$(currentAcct.id);
|
||||
const userItems = await firstValueFrom(userItems$);
|
||||
|
||||
// Show the notice when:
|
||||
// - The temp notice flag is enabled
|
||||
// - The user hasn't dismissed the notice or the user dismissed it more than 7 days ago
|
||||
if (
|
||||
tempNoticeFlag &&
|
||||
(!userItems?.last_dismissal || isMoreThan7DaysAgo(userItems?.last_dismissal))
|
||||
) {
|
||||
return router.createUrlTree(["/new-device-notice"]);
|
||||
}
|
||||
|
||||
// Show the notice when:
|
||||
// - The permanent notice flag is enabled
|
||||
// - The user hasn't dismissed the notice
|
||||
if (permNoticeFlag && !userItems?.permanent_dismissal) {
|
||||
return router.createUrlTree(["/new-device-notice"]);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Returns true has one 2FA provider enabled */
|
||||
async function hasATwoFactorProviderEnabled(
|
||||
vaultProfileService: VaultProfileService,
|
||||
userId: string,
|
||||
): Promise<boolean> {
|
||||
return vaultProfileService.getProfileTwoFactorEnabled(userId);
|
||||
}
|
||||
|
||||
/** Returns true when the user's profile is less than a week old */
|
||||
async function profileIsLessThanWeekOld(
|
||||
vaultProfileService: VaultProfileService,
|
||||
userId: string,
|
||||
): Promise<boolean> {
|
||||
const creationDate = await vaultProfileService.getProfileCreationDate(userId);
|
||||
return !isMoreThan7DaysAgo(creationDate);
|
||||
}
|
||||
|
||||
/** Returns true when the user is required to login via SSO */
|
||||
async function isSSORequired(policyService: PolicyService) {
|
||||
return firstValueFrom(policyService.policyAppliesToActiveUser$(PolicyType.RequireSso));
|
||||
}
|
||||
|
||||
/** Returns the true when the date given is older than 7 days */
|
||||
function isMoreThan7DaysAgo(date?: string | Date): boolean {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputDate = new Date(date).getTime();
|
||||
const today = new Date().getTime();
|
||||
|
||||
const differenceInMS = today - inputDate;
|
||||
const msInADay = 1000 * 60 * 60 * 24;
|
||||
const differenceInDays = Math.round(differenceInMS / msInADay);
|
||||
|
||||
return differenceInDays > 7;
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
|
||||
import { VaultProfileService } from "./vault-profile.service";
|
||||
|
||||
describe("VaultProfileService", () => {
|
||||
let service: VaultProfileService;
|
||||
const userId = "profile-id";
|
||||
const hardcodedDateString = "2024-02-24T12:00:00Z";
|
||||
|
||||
const getProfile = jest.fn().mockResolvedValue({
|
||||
creationDate: hardcodedDateString,
|
||||
twoFactorEnabled: true,
|
||||
id: "new-user-id",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
getProfile.mockClear();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: ApiService, useValue: { getProfile } }],
|
||||
});
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2024-02-22T00:00:00Z"));
|
||||
service = TestBed.runInInjectionContext(() => new VaultProfileService());
|
||||
service["userId"] = userId;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("getProfileCreationDate", () => {
|
||||
it("calls `getProfile` when stored profile date is not set", async () => {
|
||||
expect(service["profileCreatedDate"]).toBeNull();
|
||||
|
||||
const date = await service.getProfileCreationDate(userId);
|
||||
|
||||
expect(date.toISOString()).toBe("2024-02-24T12:00:00.000Z");
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls `getProfile` when stored profile id does not match", async () => {
|
||||
service["profileCreatedDate"] = hardcodedDateString;
|
||||
service["userId"] = "old-user-id";
|
||||
|
||||
const date = await service.getProfileCreationDate(userId);
|
||||
|
||||
expect(date.toISOString()).toBe("2024-02-24T12:00:00.000Z");
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call `getProfile` when the date is already stored", async () => {
|
||||
service["profileCreatedDate"] = hardcodedDateString;
|
||||
|
||||
const date = await service.getProfileCreationDate(userId);
|
||||
|
||||
expect(date.toISOString()).toBe("2024-02-24T12:00:00.000Z");
|
||||
expect(getProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProfileTwoFactorEnabled", () => {
|
||||
it("calls `getProfile` when stored 2FA property is not stored", async () => {
|
||||
expect(service["profile2FAEnabled"]).toBeNull();
|
||||
|
||||
const twoFactorEnabled = await service.getProfileTwoFactorEnabled(userId);
|
||||
|
||||
expect(twoFactorEnabled).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls `getProfile` when stored profile id does not match", async () => {
|
||||
service["profile2FAEnabled"] = false;
|
||||
service["userId"] = "old-user-id";
|
||||
|
||||
const twoFactorEnabled = await service.getProfileTwoFactorEnabled(userId);
|
||||
|
||||
expect(twoFactorEnabled).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call `getProfile` when 2FA property is already stored", async () => {
|
||||
service["profile2FAEnabled"] = false;
|
||||
|
||||
const twoFactorEnabled = await service.getProfileTwoFactorEnabled(userId);
|
||||
|
||||
expect(twoFactorEnabled).toBe(false);
|
||||
expect(getProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
64
libs/angular/src/vault/services/vault-profile.service.ts
Normal file
64
libs/angular/src/vault/services/vault-profile.service.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
/**
|
||||
* Class to provide profile level details without having to call the API each time.
|
||||
* NOTE: This is a temporary service and can be replaced once the `UnauthenticatedExtensionUIRefresh` flag goes live.
|
||||
* The `UnauthenticatedExtensionUIRefresh` introduces a sync that takes place upon logging in. These details can then
|
||||
* be added to account object and retrieved from there.
|
||||
* TODO: PM-16202
|
||||
*/
|
||||
export class VaultProfileService {
|
||||
private apiService = inject(ApiService);
|
||||
|
||||
private userId: string | null = null;
|
||||
|
||||
/** Profile creation stored as a string. */
|
||||
private profileCreatedDate: string | null = null;
|
||||
|
||||
/** True when 2FA is enabled on the profile. */
|
||||
private profile2FAEnabled: boolean | null = null;
|
||||
|
||||
/**
|
||||
* Returns the creation date of the profile.
|
||||
* Note: `Date`s are mutable in JS, creating a new
|
||||
* instance is important to avoid unwanted changes.
|
||||
*/
|
||||
async getProfileCreationDate(userId: string): Promise<Date> {
|
||||
if (this.profileCreatedDate && userId === this.userId) {
|
||||
return Promise.resolve(new Date(this.profileCreatedDate));
|
||||
}
|
||||
|
||||
const profile = await this.fetchAndCacheProfile();
|
||||
|
||||
return new Date(profile.creationDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there is a 2FA provider on the profile.
|
||||
*/
|
||||
async getProfileTwoFactorEnabled(userId: string): Promise<boolean> {
|
||||
if (this.profile2FAEnabled !== null && userId === this.userId) {
|
||||
return Promise.resolve(this.profile2FAEnabled);
|
||||
}
|
||||
|
||||
const profile = await this.fetchAndCacheProfile();
|
||||
|
||||
return profile.twoFactorEnabled;
|
||||
}
|
||||
|
||||
private async fetchAndCacheProfile(): Promise<ProfileResponse> {
|
||||
const profile = await this.apiService.getProfile();
|
||||
|
||||
this.userId = profile.id;
|
||||
this.profileCreatedDate = profile.creationDate;
|
||||
this.profile2FAEnabled = profile.twoFactorEnabled;
|
||||
|
||||
return profile;
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential
|
||||
*
|
||||
* The authenticator provides key management and cryptographic signatures.
|
||||
*/
|
||||
export abstract class Fido2AuthenticatorService {
|
||||
export abstract class Fido2AuthenticatorService<ParentWindowReference> {
|
||||
/**
|
||||
* Create and save a new credential as described in:
|
||||
* https://www.w3.org/TR/webauthn-3/#sctn-op-make-cred
|
||||
@ -19,7 +19,7 @@ export abstract class Fido2AuthenticatorService {
|
||||
**/
|
||||
makeCredential: (
|
||||
params: Fido2AuthenticatorMakeCredentialsParams,
|
||||
tab: chrome.tabs.Tab,
|
||||
window: ParentWindowReference,
|
||||
abortController?: AbortController,
|
||||
) => Promise<Fido2AuthenticatorMakeCredentialResult>;
|
||||
|
||||
@ -33,7 +33,7 @@ export abstract class Fido2AuthenticatorService {
|
||||
*/
|
||||
getAssertion: (
|
||||
params: Fido2AuthenticatorGetAssertionParams,
|
||||
tab: chrome.tabs.Tab,
|
||||
window: ParentWindowReference,
|
||||
abortController?: AbortController,
|
||||
) => Promise<Fido2AuthenticatorGetAssertionResult>;
|
||||
|
||||
|
@ -15,7 +15,7 @@ export type UserVerification = "discouraged" | "preferred" | "required";
|
||||
* It is responsible for both marshalling the inputs for the underlying authenticator operations,
|
||||
* and for returning the results of the latter operations to the Web Authentication API's callers.
|
||||
*/
|
||||
export abstract class Fido2ClientService {
|
||||
export abstract class Fido2ClientService<ParentWindowReference> {
|
||||
isFido2FeatureEnabled: (hostname: string, origin: string) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
@ -28,7 +28,7 @@ export abstract class Fido2ClientService {
|
||||
*/
|
||||
createCredential: (
|
||||
params: CreateCredentialParams,
|
||||
tab: chrome.tabs.Tab,
|
||||
window: ParentWindowReference,
|
||||
abortController?: AbortController,
|
||||
) => Promise<CreateCredentialResult>;
|
||||
|
||||
@ -43,7 +43,7 @@ export abstract class Fido2ClientService {
|
||||
*/
|
||||
assertCredential: (
|
||||
params: AssertCredentialParams,
|
||||
tab: chrome.tabs.Tab,
|
||||
window: ParentWindowReference,
|
||||
abortController?: AbortController,
|
||||
) => Promise<AssertCredentialResult>;
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ export interface PickCredentialParams {
|
||||
* The service is session based and is intended to be used by the FIDO2 authenticator to open a window,
|
||||
* and then use this window to ask the user for input and/or display messages to the user.
|
||||
*/
|
||||
export abstract class Fido2UserInterfaceService {
|
||||
export abstract class Fido2UserInterfaceService<ParentWindowReference> {
|
||||
/**
|
||||
* Creates a new session.
|
||||
* Note: This will not necessarily open a window until it is needed to request something from the user.
|
||||
@ -71,7 +71,7 @@ export abstract class Fido2UserInterfaceService {
|
||||
*/
|
||||
newSession: (
|
||||
fallbackSupported: boolean,
|
||||
tab: chrome.tabs.Tab,
|
||||
window: ParentWindowReference,
|
||||
abortController?: AbortController,
|
||||
) => Promise<Fido2UserInterfaceSession>;
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ import { parseCredentialId } from "./credential-id-utils";
|
||||
import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service";
|
||||
import { Fido2Utils } from "./fido2-utils";
|
||||
|
||||
type ParentWindowReference = string;
|
||||
|
||||
const RpId = "bitwarden.com";
|
||||
|
||||
describe("FidoAuthenticatorService", () => {
|
||||
@ -41,16 +43,16 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
|
||||
let cipherService!: MockProxy<CipherService>;
|
||||
let userInterface!: MockProxy<Fido2UserInterfaceService>;
|
||||
let userInterface!: MockProxy<Fido2UserInterfaceService<ParentWindowReference>>;
|
||||
let userInterfaceSession!: MockProxy<Fido2UserInterfaceSession>;
|
||||
let syncService!: MockProxy<SyncService>;
|
||||
let accountService!: MockProxy<AccountService>;
|
||||
let authenticator!: Fido2AuthenticatorService;
|
||||
let tab!: chrome.tabs.Tab;
|
||||
let authenticator!: Fido2AuthenticatorService<ParentWindowReference>;
|
||||
let windowReference!: ParentWindowReference;
|
||||
|
||||
beforeEach(async () => {
|
||||
cipherService = mock<CipherService>();
|
||||
userInterface = mock<Fido2UserInterfaceService>();
|
||||
userInterface = mock<Fido2UserInterfaceService<ParentWindowReference>>();
|
||||
userInterfaceSession = mock<Fido2UserInterfaceSession>();
|
||||
userInterface.newSession.mockResolvedValue(userInterfaceSession);
|
||||
syncService = mock<SyncService>({
|
||||
@ -63,7 +65,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
syncService,
|
||||
accountService,
|
||||
);
|
||||
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
|
||||
windowReference = Utils.newGuid();
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
});
|
||||
|
||||
@ -78,19 +80,21 @@ describe("FidoAuthenticatorService", () => {
|
||||
// Spec: Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation.
|
||||
it("should throw error when input does not contain any supported algorithms", async () => {
|
||||
const result = async () =>
|
||||
await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, tab);
|
||||
await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotSupported);
|
||||
});
|
||||
|
||||
it("should throw error when requireResidentKey has invalid value", async () => {
|
||||
const result = async () => await authenticator.makeCredential(invalidParams.invalidRk, tab);
|
||||
const result = async () =>
|
||||
await authenticator.makeCredential(invalidParams.invalidRk, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
|
||||
});
|
||||
|
||||
it("should throw error when requireUserVerification has invalid value", async () => {
|
||||
const result = async () => await authenticator.makeCredential(invalidParams.invalidUv, tab);
|
||||
const result = async () =>
|
||||
await authenticator.makeCredential(invalidParams.invalidUv, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
|
||||
});
|
||||
@ -103,7 +107,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
it.skip("should throw error if requireUserVerification is set to true", async () => {
|
||||
const params = await createParams({ requireUserVerification: true });
|
||||
|
||||
const result = async () => await authenticator.makeCredential(params, tab);
|
||||
const result = async () => await authenticator.makeCredential(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint);
|
||||
});
|
||||
@ -117,7 +121,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
|
||||
for (const p of Object.values(invalidParams)) {
|
||||
try {
|
||||
await authenticator.makeCredential(p, tab);
|
||||
await authenticator.makeCredential(p, windowReference);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
}
|
||||
@ -158,7 +162,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
userInterfaceSession.informExcludedCredential.mockResolvedValue();
|
||||
|
||||
try {
|
||||
await authenticator.makeCredential(params, tab);
|
||||
await authenticator.makeCredential(params, windowReference);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
|
||||
@ -169,7 +173,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
it("should throw error", async () => {
|
||||
userInterfaceSession.informExcludedCredential.mockResolvedValue();
|
||||
|
||||
const result = async () => await authenticator.makeCredential(params, tab);
|
||||
const result = async () => await authenticator.makeCredential(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
|
||||
});
|
||||
@ -180,7 +184,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
excludedCipher.organizationId = "someOrganizationId";
|
||||
|
||||
try {
|
||||
await authenticator.makeCredential(params, tab);
|
||||
await authenticator.makeCredential(params, windowReference);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
|
||||
@ -193,7 +197,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
|
||||
for (const p of Object.values(invalidParams)) {
|
||||
try {
|
||||
await authenticator.makeCredential(p, tab);
|
||||
await authenticator.makeCredential(p, windowReference);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
}
|
||||
@ -230,7 +234,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
userVerified: userVerification,
|
||||
});
|
||||
|
||||
await authenticator.makeCredential(params, tab);
|
||||
await authenticator.makeCredential(params, windowReference);
|
||||
|
||||
expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith({
|
||||
credentialName: params.rpEntity.name,
|
||||
@ -250,7 +254,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
|
||||
|
||||
await authenticator.makeCredential(params, tab);
|
||||
await authenticator.makeCredential(params, windowReference);
|
||||
|
||||
const saved = cipherService.encrypt.mock.lastCall?.[0];
|
||||
expect(saved).toEqual(
|
||||
@ -288,7 +292,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
const params = await createParams();
|
||||
|
||||
const result = async () => await authenticator.makeCredential(params, tab);
|
||||
const result = async () => await authenticator.makeCredential(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
|
||||
});
|
||||
@ -302,7 +306,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password };
|
||||
cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher);
|
||||
|
||||
const result = async () => await authenticator.makeCredential(params, tab);
|
||||
const result = async () => await authenticator.makeCredential(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
|
||||
});
|
||||
@ -317,7 +321,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
|
||||
cipherService.updateWithServer.mockRejectedValue(new Error("Internal error"));
|
||||
|
||||
const result = async () => await authenticator.makeCredential(params, tab);
|
||||
const result = async () => await authenticator.makeCredential(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
|
||||
});
|
||||
@ -358,7 +362,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
|
||||
it("should return attestation object", async () => {
|
||||
const result = await authenticator.makeCredential(params, tab);
|
||||
const result = await authenticator.makeCredential(params, windowReference);
|
||||
|
||||
const attestationObject = CBOR.decode(
|
||||
Fido2Utils.bufferSourceToUint8Array(result.attestationObject).buffer,
|
||||
@ -455,7 +459,8 @@ describe("FidoAuthenticatorService", () => {
|
||||
|
||||
describe("invalid input parameters", () => {
|
||||
it("should throw error when requireUserVerification has invalid value", async () => {
|
||||
const result = async () => await authenticator.getAssertion(invalidParams.invalidUv, tab);
|
||||
const result = async () =>
|
||||
await authenticator.getAssertion(invalidParams.invalidUv, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
|
||||
});
|
||||
@ -468,7 +473,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
it.skip("should throw error if requireUserVerification is set to true", async () => {
|
||||
const params = await createParams({ requireUserVerification: true });
|
||||
|
||||
const result = async () => await authenticator.getAssertion(params, tab);
|
||||
const result = async () => await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint);
|
||||
});
|
||||
@ -498,7 +503,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
userInterfaceSession.informCredentialNotFound.mockResolvedValue();
|
||||
|
||||
try {
|
||||
await authenticator.getAssertion(params, tab);
|
||||
await authenticator.getAssertion(params, windowReference);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
|
||||
@ -513,7 +518,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
userInterfaceSession.informCredentialNotFound.mockResolvedValue();
|
||||
|
||||
try {
|
||||
await authenticator.getAssertion(params, tab);
|
||||
await authenticator.getAssertion(params, windowReference);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
|
||||
@ -534,7 +539,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
|
||||
/** Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation. */
|
||||
it("should throw error", async () => {
|
||||
const result = async () => await authenticator.getAssertion(params, tab);
|
||||
const result = async () => await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
|
||||
});
|
||||
@ -573,7 +578,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
userVerified: false,
|
||||
});
|
||||
|
||||
await authenticator.getAssertion(params, tab);
|
||||
await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
|
||||
cipherIds: ciphers.map((c) => c.id),
|
||||
@ -590,7 +595,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
userVerified: false,
|
||||
});
|
||||
|
||||
await authenticator.getAssertion(params, tab);
|
||||
await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
|
||||
cipherIds: [discoverableCiphers[0].id],
|
||||
@ -608,7 +613,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
userVerified: userVerification,
|
||||
});
|
||||
|
||||
await authenticator.getAssertion(params, tab);
|
||||
await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
|
||||
cipherIds: ciphers.map((c) => c.id),
|
||||
@ -625,7 +630,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
userVerified: false,
|
||||
});
|
||||
|
||||
const result = async () => await authenticator.getAssertion(params, tab);
|
||||
const result = async () => await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
|
||||
});
|
||||
@ -637,7 +642,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
userVerified: false,
|
||||
});
|
||||
|
||||
const result = async () => await authenticator.getAssertion(params, tab);
|
||||
const result = async () => await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed);
|
||||
});
|
||||
@ -686,7 +691,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
cipherService.encrypt.mockResolvedValue(encrypted as any);
|
||||
ciphers[0].login.fido2Credentials[0].counter = 9000;
|
||||
|
||||
await authenticator.getAssertion(params, tab);
|
||||
await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted);
|
||||
expect(cipherService.encrypt).toHaveBeenCalledWith(
|
||||
@ -710,13 +715,13 @@ describe("FidoAuthenticatorService", () => {
|
||||
cipherService.encrypt.mockResolvedValue(encrypted as any);
|
||||
ciphers[0].login.fido2Credentials[0].counter = 0;
|
||||
|
||||
await authenticator.getAssertion(params, tab);
|
||||
await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
expect(cipherService.updateWithServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return an assertion result", async () => {
|
||||
const result = await authenticator.getAssertion(params, tab);
|
||||
const result = await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
const encAuthData = result.authenticatorData;
|
||||
const rpIdHash = encAuthData.slice(0, 32);
|
||||
@ -757,7 +762,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await init(); // Reset inputs
|
||||
const result = await authenticator.getAssertion(params, tab);
|
||||
const result = await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
const counter = result.authenticatorData.slice(33, 37);
|
||||
expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // double check that the counter doesn't change
|
||||
@ -774,7 +779,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
it("should throw unkown error if creation fails", async () => {
|
||||
cipherService.updateWithServer.mockRejectedValue(new Error("Internal error"));
|
||||
|
||||
const result = async () => await authenticator.getAssertion(params, tab);
|
||||
const result = async () => await authenticator.getAssertion(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
|
||||
});
|
||||
|
@ -43,10 +43,12 @@ const KeyUsages: KeyUsage[] = ["sign"];
|
||||
*
|
||||
* It is highly recommended that the W3C specification is used a reference when reading this code.
|
||||
*/
|
||||
export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstraction {
|
||||
export class Fido2AuthenticatorService<ParentWindowReference>
|
||||
implements Fido2AuthenticatorServiceAbstraction<ParentWindowReference>
|
||||
{
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private userInterface: Fido2UserInterfaceService,
|
||||
private userInterface: Fido2UserInterfaceService<ParentWindowReference>,
|
||||
private syncService: SyncService,
|
||||
private accountService: AccountService,
|
||||
private logService?: LogService,
|
||||
@ -54,12 +56,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
|
||||
async makeCredential(
|
||||
params: Fido2AuthenticatorMakeCredentialsParams,
|
||||
tab: chrome.tabs.Tab,
|
||||
window: ParentWindowReference,
|
||||
abortController?: AbortController,
|
||||
): Promise<Fido2AuthenticatorMakeCredentialResult> {
|
||||
const userInterfaceSession = await this.userInterface.newSession(
|
||||
params.fallbackSupported,
|
||||
tab,
|
||||
window,
|
||||
abortController,
|
||||
);
|
||||
|
||||
@ -209,12 +211,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
|
||||
async getAssertion(
|
||||
params: Fido2AuthenticatorGetAssertionParams,
|
||||
tab: chrome.tabs.Tab,
|
||||
window: ParentWindowReference,
|
||||
abortController?: AbortController,
|
||||
): Promise<Fido2AuthenticatorGetAssertionResult> {
|
||||
const userInterfaceSession = await this.userInterface.newSession(
|
||||
params.fallbackSupported,
|
||||
tab,
|
||||
window,
|
||||
abortController,
|
||||
);
|
||||
try {
|
||||
|
@ -3,6 +3,9 @@
|
||||
import { CipherType } from "../../../vault/enums";
|
||||
import { CipherView } from "../../../vault/models/view/cipher.view";
|
||||
import { Fido2CredentialAutofillView } from "../../../vault/models/view/fido2-credential-autofill.view";
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
import { parseCredentialId } from "./credential-id-utils";
|
||||
|
||||
// TODO: Move into Fido2AuthenticatorService
|
||||
export async function getCredentialsForAutofill(
|
||||
@ -15,9 +18,14 @@ export async function getCredentialsForAutofill(
|
||||
)
|
||||
.map((cipher) => {
|
||||
const credential = cipher.login.fido2Credentials[0];
|
||||
|
||||
// Credentials are stored as a GUID or b64 string with `b64.` prepended,
|
||||
// but we need to return them as a URL-safe base64 string
|
||||
const credId = Utils.fromBufferToUrlB64(parseCredentialId(credential.credentialId));
|
||||
|
||||
return {
|
||||
cipherId: cipher.id,
|
||||
credentialId: credential.credentialId,
|
||||
credentialId: credId,
|
||||
rpId: credential.rpId,
|
||||
userHandle: credential.userHandle,
|
||||
userName: credential.userName,
|
||||
|
@ -32,12 +32,14 @@ import { Fido2ClientService } from "./fido2-client.service";
|
||||
import { Fido2Utils } from "./fido2-utils";
|
||||
import { guidToRawFormat } from "./guid-utils";
|
||||
|
||||
type ParentWindowReference = string;
|
||||
|
||||
const RpId = "bitwarden.com";
|
||||
const Origin = "https://bitwarden.com";
|
||||
const VaultUrl = "https://vault.bitwarden.com";
|
||||
|
||||
describe("FidoAuthenticatorService", () => {
|
||||
let authenticator!: MockProxy<Fido2AuthenticatorService>;
|
||||
let authenticator!: MockProxy<Fido2AuthenticatorService<ParentWindowReference>>;
|
||||
let configService!: MockProxy<ConfigService>;
|
||||
let authService!: MockProxy<AuthService>;
|
||||
let vaultSettingsService: MockProxy<VaultSettingsService>;
|
||||
@ -45,12 +47,12 @@ describe("FidoAuthenticatorService", () => {
|
||||
let taskSchedulerService: MockProxy<TaskSchedulerService>;
|
||||
let activeRequest!: MockProxy<ActiveRequest>;
|
||||
let requestManager!: MockProxy<Fido2ActiveRequestManager>;
|
||||
let client!: Fido2ClientService;
|
||||
let tab!: chrome.tabs.Tab;
|
||||
let client!: Fido2ClientService<ParentWindowReference>;
|
||||
let windowReference!: ParentWindowReference;
|
||||
let isValidRpId!: jest.SpyInstance;
|
||||
|
||||
beforeEach(async () => {
|
||||
authenticator = mock<Fido2AuthenticatorService>();
|
||||
authenticator = mock<Fido2AuthenticatorService<ParentWindowReference>>();
|
||||
configService = mock<ConfigService>();
|
||||
authService = mock<AuthService>();
|
||||
vaultSettingsService = mock<VaultSettingsService>();
|
||||
@ -82,7 +84,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
vaultSettingsService.enablePasskeys$ = of(true);
|
||||
domainSettingsService.neverDomains$ = of({});
|
||||
authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
|
||||
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
|
||||
windowReference = Utils.newGuid();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -95,7 +97,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
it("should throw error if sameOriginWithAncestors is false", async () => {
|
||||
const params = createParams({ sameOriginWithAncestors: false });
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "NotAllowedError" });
|
||||
@ -106,7 +108,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
it("should throw error if user.id is too small", async () => {
|
||||
const params = createParams({ user: { id: "", displayName: "displayName", name: "name" } });
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toBeInstanceOf(TypeError);
|
||||
});
|
||||
@ -121,7 +123,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toBeInstanceOf(TypeError);
|
||||
});
|
||||
@ -136,7 +138,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
origin: "invalid-domain-name",
|
||||
});
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "SecurityError" });
|
||||
@ -151,7 +153,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
rp: { id: "bitwarden.com", name: "Bitwarden" },
|
||||
});
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "SecurityError" });
|
||||
@ -165,7 +167,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
// `params` actually has a valid rp.id, but we're mocking the function to return false
|
||||
isValidRpId.mockReturnValue(false);
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "SecurityError" });
|
||||
@ -179,7 +181,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrow(FallbackRequestedError);
|
||||
});
|
||||
@ -190,7 +192,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
rp: { id: "bitwarden.com", name: "Bitwarden" },
|
||||
});
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "SecurityError" });
|
||||
@ -204,7 +206,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
|
||||
|
||||
await client.createCredential(params, tab);
|
||||
await client.createCredential(params, windowReference);
|
||||
});
|
||||
|
||||
// Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm.
|
||||
@ -216,7 +218,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
],
|
||||
});
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "NotSupportedError" });
|
||||
@ -231,7 +233,8 @@ describe("FidoAuthenticatorService", () => {
|
||||
const abortController = new AbortController();
|
||||
abortController.abort();
|
||||
|
||||
const result = async () => await client.createCredential(params, tab, abortController);
|
||||
const result = async () =>
|
||||
await client.createCredential(params, windowReference, abortController);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "AbortError" });
|
||||
@ -246,7 +249,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
|
||||
|
||||
await client.createCredential(params, tab);
|
||||
await client.createCredential(params, windowReference);
|
||||
|
||||
expect(authenticator.makeCredential).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@ -259,7 +262,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
displayName: params.user.displayName,
|
||||
}),
|
||||
}),
|
||||
tab,
|
||||
windowReference,
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
@ -271,7 +274,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
|
||||
|
||||
const result = await client.createCredential(params, tab);
|
||||
const result = await client.createCredential(params, windowReference);
|
||||
|
||||
expect(result.extensions.credProps?.rk).toBe(true);
|
||||
});
|
||||
@ -283,7 +286,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
|
||||
|
||||
const result = await client.createCredential(params, tab);
|
||||
const result = await client.createCredential(params, windowReference);
|
||||
|
||||
expect(result.extensions.credProps?.rk).toBe(false);
|
||||
});
|
||||
@ -295,7 +298,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
|
||||
|
||||
const result = await client.createCredential(params, tab);
|
||||
const result = await client.createCredential(params, windowReference);
|
||||
|
||||
expect(result.extensions.credProps).toBeUndefined();
|
||||
});
|
||||
@ -307,7 +310,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState),
|
||||
);
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "InvalidStateError" });
|
||||
@ -319,7 +322,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
const params = createParams();
|
||||
authenticator.makeCredential.mockRejectedValue(new Error("unknown error"));
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "NotAllowedError" });
|
||||
@ -330,7 +333,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
const params = createParams();
|
||||
vaultSettingsService.enablePasskeys$ = of(false);
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toThrow(FallbackRequestedError);
|
||||
@ -340,7 +343,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
const params = createParams();
|
||||
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toThrow(FallbackRequestedError);
|
||||
@ -349,7 +352,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => {
|
||||
const params = createParams({ origin: VaultUrl });
|
||||
|
||||
const result = async () => await client.createCredential(params, tab);
|
||||
const result = async () => await client.createCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toThrow(FallbackRequestedError);
|
||||
@ -408,7 +411,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
origin: "invalid-domain-name",
|
||||
});
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
const result = async () => await client.assertCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "SecurityError" });
|
||||
@ -423,7 +426,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
rpId: "bitwarden.com",
|
||||
});
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
const result = async () => await client.assertCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "SecurityError" });
|
||||
@ -437,7 +440,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
// `params` actually has a valid rp.id, but we're mocking the function to return false
|
||||
isValidRpId.mockReturnValue(false);
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
const result = async () => await client.assertCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "SecurityError" });
|
||||
@ -451,7 +454,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
|
||||
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
const result = async () => await client.assertCredential(params, windowReference);
|
||||
|
||||
await expect(result).rejects.toThrow(FallbackRequestedError);
|
||||
});
|
||||
@ -462,7 +465,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
rpId: "bitwarden.com",
|
||||
});
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
const result = async () => await client.assertCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "SecurityError" });
|
||||
@ -477,7 +480,8 @@ describe("FidoAuthenticatorService", () => {
|
||||
const abortController = new AbortController();
|
||||
abortController.abort();
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab, abortController);
|
||||
const result = async () =>
|
||||
await client.assertCredential(params, windowReference, abortController);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "AbortError" });
|
||||
@ -493,7 +497,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState),
|
||||
);
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
const result = async () => await client.assertCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "InvalidStateError" });
|
||||
@ -505,7 +509,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
const params = createParams();
|
||||
authenticator.getAssertion.mockRejectedValue(new Error("unknown error"));
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
const result = async () => await client.assertCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toMatchObject({ name: "NotAllowedError" });
|
||||
@ -516,7 +520,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
const params = createParams();
|
||||
vaultSettingsService.enablePasskeys$ = of(false);
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
const result = async () => await client.assertCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toThrow(FallbackRequestedError);
|
||||
@ -526,7 +530,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
const params = createParams();
|
||||
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
const result = async () => await client.assertCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toThrow(FallbackRequestedError);
|
||||
@ -535,7 +539,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => {
|
||||
const params = createParams({ origin: VaultUrl });
|
||||
|
||||
const result = async () => await client.assertCredential(params, tab);
|
||||
const result = async () => await client.assertCredential(params, windowReference);
|
||||
|
||||
const rejects = expect(result).rejects;
|
||||
await rejects.toThrow(FallbackRequestedError);
|
||||
@ -555,7 +559,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
|
||||
|
||||
await client.assertCredential(params, tab);
|
||||
await client.assertCredential(params, windowReference);
|
||||
|
||||
expect(authenticator.getAssertion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@ -573,7 +577,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
}),
|
||||
],
|
||||
}),
|
||||
tab,
|
||||
windowReference,
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
@ -585,7 +589,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
params.rpId = undefined;
|
||||
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
|
||||
|
||||
await client.assertCredential(params, tab);
|
||||
await client.assertCredential(params, windowReference);
|
||||
});
|
||||
});
|
||||
|
||||
@ -597,7 +601,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
|
||||
|
||||
await client.assertCredential(params, tab);
|
||||
await client.assertCredential(params, windowReference);
|
||||
|
||||
expect(authenticator.getAssertion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@ -605,7 +609,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
rpId: RpId,
|
||||
allowCredentialDescriptorList: [],
|
||||
}),
|
||||
tab,
|
||||
windowReference,
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
@ -627,7 +631,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
|
||||
it("creates an active mediated conditional request", async () => {
|
||||
await client.assertCredential(params, tab);
|
||||
await client.assertCredential(params, windowReference);
|
||||
|
||||
expect(requestManager.newActiveRequest).toHaveBeenCalled();
|
||||
expect(authenticator.getAssertion).toHaveBeenCalledWith(
|
||||
@ -635,14 +639,14 @@ describe("FidoAuthenticatorService", () => {
|
||||
assumeUserPresence: true,
|
||||
rpId: RpId,
|
||||
}),
|
||||
tab,
|
||||
windowReference,
|
||||
);
|
||||
});
|
||||
|
||||
it("restarts the mediated conditional request if a user aborts the request", async () => {
|
||||
authenticator.getAssertion.mockRejectedValueOnce(new Error());
|
||||
|
||||
await client.assertCredential(params, tab);
|
||||
await client.assertCredential(params, windowReference);
|
||||
|
||||
expect(authenticator.getAssertion).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@ -652,7 +656,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
abortController.abort();
|
||||
authenticator.getAssertion.mockRejectedValueOnce(new DOMException("AbortError"));
|
||||
|
||||
await client.assertCredential(params, tab);
|
||||
await client.assertCredential(params, windowReference);
|
||||
|
||||
expect(authenticator.getAssertion).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
@ -47,7 +47,9 @@ import { guidToRawFormat } from "./guid-utils";
|
||||
*
|
||||
* It is highly recommended that the W3C specification is used a reference when reading this code.
|
||||
*/
|
||||
export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
export class Fido2ClientService<ParentWindowReference>
|
||||
implements Fido2ClientServiceAbstraction<ParentWindowReference>
|
||||
{
|
||||
private timeoutAbortController: AbortController;
|
||||
private readonly TIMEOUTS = {
|
||||
NO_VERIFICATION: {
|
||||
@ -63,7 +65,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
};
|
||||
|
||||
constructor(
|
||||
private authenticator: Fido2AuthenticatorService,
|
||||
private authenticator: Fido2AuthenticatorService<ParentWindowReference>,
|
||||
private configService: ConfigService,
|
||||
private authService: AuthService,
|
||||
private vaultSettingsService: VaultSettingsService,
|
||||
@ -102,7 +104,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
|
||||
async createCredential(
|
||||
params: CreateCredentialParams,
|
||||
tab: chrome.tabs.Tab,
|
||||
window: ParentWindowReference,
|
||||
abortController = new AbortController(),
|
||||
): Promise<CreateCredentialResult> {
|
||||
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
|
||||
@ -201,7 +203,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
try {
|
||||
makeCredentialResult = await this.authenticator.makeCredential(
|
||||
makeCredentialParams,
|
||||
tab,
|
||||
window,
|
||||
abortController,
|
||||
);
|
||||
} catch (error) {
|
||||
@ -256,7 +258,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
|
||||
async assertCredential(
|
||||
params: AssertCredentialParams,
|
||||
tab: chrome.tabs.Tab,
|
||||
window: ParentWindowReference,
|
||||
abortController = new AbortController(),
|
||||
): Promise<AssertCredentialResult> {
|
||||
const parsedOrigin = parse(params.origin, { allowPrivateDomains: true });
|
||||
@ -300,7 +302,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
if (params.mediation === "conditional") {
|
||||
return this.handleMediatedConditionalRequest(
|
||||
params,
|
||||
tab,
|
||||
window,
|
||||
abortController,
|
||||
clientDataJSONBytes,
|
||||
);
|
||||
@ -324,7 +326,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
try {
|
||||
getAssertionResult = await this.authenticator.getAssertion(
|
||||
getAssertionParams,
|
||||
tab,
|
||||
window,
|
||||
abortController,
|
||||
);
|
||||
} catch (error) {
|
||||
@ -363,7 +365,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
|
||||
private async handleMediatedConditionalRequest(
|
||||
params: AssertCredentialParams,
|
||||
tab: chrome.tabs.Tab,
|
||||
tab: ParentWindowReference,
|
||||
abortController: AbortController,
|
||||
clientDataJSONBytes: Uint8Array,
|
||||
): Promise<AssertCredentialResult> {
|
||||
@ -379,7 +381,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
|
||||
`[Fido2Client] started mediated request, available credentials: ${availableCredentials.length}`,
|
||||
);
|
||||
const requestResult = await this.requestManager.newActiveRequest(
|
||||
tab.id,
|
||||
// TODO: This isn't correct, but this.requestManager.newActiveRequest expects a number,
|
||||
// while this class is currently generic over ParentWindowReference.
|
||||
// Consider moving requestManager into browser and adding support for ParentWindowReference => tab.id
|
||||
(tab as any).id,
|
||||
availableCredentials,
|
||||
abortController,
|
||||
);
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
* Noop implementation of the {@link Fido2UserInterfaceService}.
|
||||
* This implementation does not provide any user interface.
|
||||
*/
|
||||
export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
|
||||
export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction<void> {
|
||||
newSession(): Promise<Fido2UserInterfaceSession> {
|
||||
throw new Error("Not implemented exception");
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { CipherId } from "../../types/guid";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
|
||||
/**
|
||||
* The ViewPasswordHistoryService is responsible for displaying the password history for a cipher.
|
||||
*/
|
||||
export abstract class ViewPasswordHistoryService {
|
||||
abstract viewPasswordHistory(cipherId?: CipherId): Promise<void>;
|
||||
abstract viewPasswordHistory(cipher: CipherView): Promise<void>;
|
||||
}
|
||||
|
@ -290,7 +290,9 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
private async initializeOrganizations() {
|
||||
this.organizations$ = concat(
|
||||
this.organizationService.memberOrganizations$.pipe(
|
||||
map((orgs) => orgs.filter((org) => org.canAccessImport)),
|
||||
// Import is an alternative way to create collections during onboarding, so import from Password Manager
|
||||
// is available to any user who can create collections in the organization.
|
||||
map((orgs) => orgs.filter((org) => org.canAccessImport || org.canCreateNewCollections)),
|
||||
map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))),
|
||||
),
|
||||
);
|
||||
|
@ -5,7 +5,6 @@ import { Component, Input } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@ -45,6 +44,6 @@ export class ItemHistoryV2Component {
|
||||
* View the password history for the cipher.
|
||||
*/
|
||||
async viewPasswordHistory() {
|
||||
await this.viewPasswordHistoryService.viewPasswordHistory(this.cipher?.id as CipherId);
|
||||
await this.viewPasswordHistoryService.viewPasswordHistory(this.cipher);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<p class="tw-text-center" bitTypography="body1">
|
||||
{{ "newDeviceVerificationNoticeContentPage1" | i18n }}
|
||||
</p>
|
||||
|
||||
<bit-card
|
||||
class="tw-pb-0"
|
||||
[ngClass]="{
|
||||
'tw-flex tw-flex-col tw-items-center !tw-rounded-b-none': isDesktop,
|
||||
'md:tw-flex md:tw-flex-col md:tw-items-center md:!tw-rounded-b-none': !isDesktop,
|
||||
}"
|
||||
>
|
||||
<p bitTypography="body2" class="text-muted md:tw-w-9/12">
|
||||
{{ "newDeviceVerificationNoticePageOneFormContent" | i18n: this.currentEmail }}
|
||||
</p>
|
||||
|
||||
<bit-radio-group formControlName="hasEmailAccess" class="md:tw-w-9/12">
|
||||
<bit-radio-button id="option_A" [value]="0">
|
||||
<bit-label>{{ "newDeviceVerificationNoticePageOneEmailAccessNo" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="option_B" [value]="1">
|
||||
<bit-label>{{ "newDeviceVerificationNoticePageOneEmailAccessYes" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</bit-card>
|
||||
|
||||
<button bitButton type="submit" buttonType="primary" class="tw-w-full tw-mt-4">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
</form>
|
@ -0,0 +1,173 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { Router } from "@angular/router";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service";
|
||||
|
||||
import { NewDeviceVerificationNoticePageOneComponent } from "./new-device-verification-notice-page-one.component";
|
||||
|
||||
describe("NewDeviceVerificationNoticePageOneComponent", () => {
|
||||
let component: NewDeviceVerificationNoticePageOneComponent;
|
||||
let fixture: ComponentFixture<NewDeviceVerificationNoticePageOneComponent>;
|
||||
|
||||
const activeAccount$ = new BehaviorSubject({ email: "test@example.com", id: "acct-1" });
|
||||
const navigate = jest.fn().mockResolvedValue(null);
|
||||
const updateNewDeviceVerificationNoticeState = jest.fn().mockResolvedValue(null);
|
||||
const getFeatureFlag = jest.fn().mockResolvedValue(null);
|
||||
|
||||
beforeEach(async () => {
|
||||
navigate.mockClear();
|
||||
updateNewDeviceVerificationNoticeState.mockClear();
|
||||
getFeatureFlag.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (...key: string[]) => key.join(" ") } },
|
||||
{ provide: Router, useValue: { navigate } },
|
||||
{ provide: AccountService, useValue: { activeAccount$ } },
|
||||
{
|
||||
provide: NewDeviceVerificationNoticeService,
|
||||
useValue: { updateNewDeviceVerificationNoticeState },
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: { getClientType: () => false } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NewDeviceVerificationNoticePageOneComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("sets initial properties", () => {
|
||||
expect(component["currentEmail"]).toBe("test@example.com");
|
||||
expect(component["currentUserId"]).toBe("acct-1");
|
||||
});
|
||||
|
||||
describe("temporary flag submission", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationTemporaryDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no email access", () => {
|
||||
beforeEach(() => {
|
||||
component["formGroup"].controls.hasEmailAccess.setValue(0);
|
||||
fixture.detectChanges();
|
||||
|
||||
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
|
||||
submit.nativeElement.click();
|
||||
});
|
||||
|
||||
it("redirects to step two ", () => {
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["new-device-notice/setup"]);
|
||||
});
|
||||
|
||||
it("does not update notice state", () => {
|
||||
expect(getFeatureFlag).not.toHaveBeenCalled();
|
||||
expect(updateNewDeviceVerificationNoticeState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("has email access", () => {
|
||||
beforeEach(() => {
|
||||
component["formGroup"].controls.hasEmailAccess.setValue(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2024-03-03T00:00:00.000Z"));
|
||||
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
|
||||
submit.nativeElement.click();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("redirects to the vault", () => {
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["/vault"]);
|
||||
});
|
||||
|
||||
it("updates notice state with a new date", () => {
|
||||
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledWith("acct-1", {
|
||||
last_dismissal: new Date("2024-03-03T00:00:00.000Z"),
|
||||
permanent_dismissal: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("permanent flag submission", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no email access", () => {
|
||||
beforeEach(() => {
|
||||
component["formGroup"].controls.hasEmailAccess.setValue(0);
|
||||
fixture.detectChanges();
|
||||
|
||||
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
|
||||
submit.nativeElement.click();
|
||||
});
|
||||
|
||||
it("redirects to step two", () => {
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["new-device-notice/setup"]);
|
||||
});
|
||||
|
||||
it("does not update notice state", () => {
|
||||
expect(getFeatureFlag).not.toHaveBeenCalled();
|
||||
expect(updateNewDeviceVerificationNoticeState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("has email access", () => {
|
||||
beforeEach(() => {
|
||||
component["formGroup"].controls.hasEmailAccess.setValue(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2024-04-04T00:00:00.000Z"));
|
||||
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
|
||||
submit.nativeElement.click();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("redirects to the vault ", () => {
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["/vault"]);
|
||||
});
|
||||
|
||||
it("updates notice state with a new date", () => {
|
||||
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledWith("acct-1", {
|
||||
last_dismissal: new Date("2024-04-04T00:00:00.000Z"),
|
||||
permanent_dismissal: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,114 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CardComponent,
|
||||
FormFieldModule,
|
||||
RadioButtonModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
NewDeviceVerificationNotice,
|
||||
NewDeviceVerificationNoticeService,
|
||||
} from "./../../services/new-device-verification-notice.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-new-device-verification-notice-page-one",
|
||||
templateUrl: "./new-device-verification-notice-page-one.component.html",
|
||||
imports: [
|
||||
CardComponent,
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
TypographyModule,
|
||||
ButtonModule,
|
||||
RadioButtonModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class NewDeviceVerificationNoticePageOneComponent implements OnInit {
|
||||
protected formGroup = this.formBuilder.group({
|
||||
hasEmailAccess: new FormControl(0),
|
||||
});
|
||||
protected isDesktop: boolean;
|
||||
readonly currentAcct$: Observable<Account | null> = this.accountService.activeAccount$;
|
||||
protected currentEmail: string = "";
|
||||
private currentUserId: UserId | null = null;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const currentAcct = await firstValueFrom(this.currentAcct$);
|
||||
if (!currentAcct) {
|
||||
return;
|
||||
}
|
||||
this.currentEmail = currentAcct.email;
|
||||
this.currentUserId = currentAcct.id;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const doesNotHaveEmailAccess = this.formGroup.controls.hasEmailAccess.value === 0;
|
||||
|
||||
if (doesNotHaveEmailAccess) {
|
||||
await this.router.navigate(["new-device-notice/setup"]);
|
||||
return;
|
||||
}
|
||||
|
||||
const tempNoticeFlag = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.NewDeviceVerificationTemporaryDismiss,
|
||||
);
|
||||
const permNoticeFlag = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.NewDeviceVerificationPermanentDismiss,
|
||||
);
|
||||
|
||||
let newNoticeState: NewDeviceVerificationNotice | null = null;
|
||||
|
||||
// When the temporary flag is enabled, only update the `last_dismissal`
|
||||
if (tempNoticeFlag) {
|
||||
newNoticeState = {
|
||||
last_dismissal: new Date(),
|
||||
permanent_dismissal: false,
|
||||
};
|
||||
} else if (permNoticeFlag) {
|
||||
// When the per flag is enabled, only update the `last_dismissal`
|
||||
newNoticeState = {
|
||||
last_dismissal: new Date(),
|
||||
permanent_dismissal: true,
|
||||
};
|
||||
}
|
||||
|
||||
// This shouldn't occur as the user shouldn't get here unless one of the flags is active.
|
||||
if (newNoticeState) {
|
||||
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState(
|
||||
this.currentUserId!,
|
||||
newNoticeState,
|
||||
);
|
||||
}
|
||||
|
||||
await this.router.navigate(["/vault"]);
|
||||
};
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<p class="tw-text-center" bitTypography="body1">
|
||||
{{ "newDeviceVerificationNoticeContentPage2" | i18n }}
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
bitButton
|
||||
(click)="navigateToTwoStepLogin($event)"
|
||||
buttonType="primary"
|
||||
class="tw-w-full tw-mt-4"
|
||||
data-testid="two-factor"
|
||||
>
|
||||
{{ "turnOnTwoStepLogin" | i18n }}
|
||||
<i
|
||||
class="bwi bwi-external-link bwi-lg bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'md:tw-hidden': !isDesktop }"
|
||||
>
|
||||
</i>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
bitButton
|
||||
(click)="navigateToChangeAcctEmail($event)"
|
||||
buttonType="secondary"
|
||||
class="tw-w-full tw-mt-4"
|
||||
data-testid="change-email"
|
||||
>
|
||||
{{ "changeAcctEmail" | i18n }}
|
||||
<i
|
||||
class="bwi bwi-external-link bwi-lg bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'md:tw-hidden': !isDesktop }"
|
||||
></i>
|
||||
</a>
|
||||
|
||||
<div class="tw-flex tw-justify-center tw-mt-6" *ngIf="!permanentFlagEnabled">
|
||||
<a bitLink linkType="primary" (click)="remindMeLaterSelect()" data-testid="remind-me-later">
|
||||
{{ "remindMeLater" | i18n }}
|
||||
</a>
|
||||
</div>
|
@ -0,0 +1,175 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { Router } from "@angular/router";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service";
|
||||
|
||||
import { NewDeviceVerificationNoticePageTwoComponent } from "./new-device-verification-notice-page-two.component";
|
||||
|
||||
describe("NewDeviceVerificationNoticePageTwoComponent", () => {
|
||||
let component: NewDeviceVerificationNoticePageTwoComponent;
|
||||
let fixture: ComponentFixture<NewDeviceVerificationNoticePageTwoComponent>;
|
||||
|
||||
const activeAccount$ = new BehaviorSubject({ email: "test@example.com", id: "acct-1" });
|
||||
const environment$ = new BehaviorSubject({ getWebVaultUrl: () => "vault.bitwarden.com" });
|
||||
const navigate = jest.fn().mockResolvedValue(null);
|
||||
const updateNewDeviceVerificationNoticeState = jest.fn().mockResolvedValue(null);
|
||||
const getFeatureFlag = jest.fn().mockResolvedValue(false);
|
||||
const getClientType = jest.fn().mockReturnValue(ClientType.Browser);
|
||||
const launchUri = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
navigate.mockClear();
|
||||
updateNewDeviceVerificationNoticeState.mockClear();
|
||||
getFeatureFlag.mockClear();
|
||||
getClientType.mockClear();
|
||||
launchUri.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (...key: string[]) => key.join(" ") } },
|
||||
{ provide: Router, useValue: { navigate } },
|
||||
{ provide: AccountService, useValue: { activeAccount$ } },
|
||||
{ provide: EnvironmentService, useValue: { environment$ } },
|
||||
{
|
||||
provide: NewDeviceVerificationNoticeService,
|
||||
useValue: { updateNewDeviceVerificationNoticeState },
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: { getClientType, launchUri } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NewDeviceVerificationNoticePageTwoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("sets initial properties", () => {
|
||||
expect(component["currentUserId"]).toBe("acct-1");
|
||||
expect(component["permanentFlagEnabled"]).toBe(false);
|
||||
});
|
||||
|
||||
describe("change email", () => {
|
||||
const changeEmailButton = () =>
|
||||
fixture.debugElement.query(By.css('[data-testid="change-email"]'));
|
||||
|
||||
describe("web", () => {
|
||||
beforeEach(() => {
|
||||
component["isWeb"] = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("navigates to settings", () => {
|
||||
changeEmailButton().nativeElement.click();
|
||||
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["/settings/account"], {
|
||||
queryParams: { fromNewDeviceVerification: true },
|
||||
});
|
||||
expect(launchUri).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("browser/desktop", () => {
|
||||
beforeEach(() => {
|
||||
component["isWeb"] = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("launches to settings", () => {
|
||||
changeEmailButton().nativeElement.click();
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
expect(launchUri).toHaveBeenCalledWith(
|
||||
"vault.bitwarden.com/#/settings/account/?fromNewDeviceVerification=true",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("enable 2fa", () => {
|
||||
const changeEmailButton = () =>
|
||||
fixture.debugElement.query(By.css('[data-testid="two-factor"]'));
|
||||
|
||||
describe("web", () => {
|
||||
beforeEach(() => {
|
||||
component["isWeb"] = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("navigates to two factor settings", () => {
|
||||
changeEmailButton().nativeElement.click();
|
||||
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["/settings/security/two-factor"], {
|
||||
queryParams: { fromNewDeviceVerification: true },
|
||||
});
|
||||
expect(launchUri).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("browser/desktop", () => {
|
||||
beforeEach(() => {
|
||||
component["isWeb"] = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("launches to two factor settings", () => {
|
||||
changeEmailButton().nativeElement.click();
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
expect(launchUri).toHaveBeenCalledWith(
|
||||
"vault.bitwarden.com/#/settings/security/two-factor/?fromNewDeviceVerification=true",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("remind me later", () => {
|
||||
const remindMeLater = () =>
|
||||
fixture.debugElement.query(By.css('[data-testid="remind-me-later"]'));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2024-02-02T00:00:00.000Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("navigates to the vault", () => {
|
||||
remindMeLater().nativeElement.click();
|
||||
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["/vault"]);
|
||||
});
|
||||
|
||||
it("updates notice state", () => {
|
||||
remindMeLater().nativeElement.click();
|
||||
|
||||
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledTimes(1);
|
||||
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledWith("acct-1", {
|
||||
last_dismissal: new Date("2024-02-02T00:00:00.000Z"),
|
||||
permanent_dismissal: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("is hidden when the permanent flag is enabled", async () => {
|
||||
getFeatureFlag.mockResolvedValueOnce(true);
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(remindMeLater()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,103 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components";
|
||||
|
||||
import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-new-device-verification-notice-page-two",
|
||||
templateUrl: "./new-device-verification-notice-page-two.component.html",
|
||||
imports: [CommonModule, JslibModule, TypographyModule, ButtonModule, LinkModule],
|
||||
})
|
||||
export class NewDeviceVerificationNoticePageTwoComponent implements OnInit {
|
||||
protected isWeb: boolean;
|
||||
protected isDesktop: boolean;
|
||||
protected permanentFlagEnabled = false;
|
||||
readonly currentAcct$: Observable<Account | null> = this.accountService.activeAccount$;
|
||||
private currentUserId: UserId | null = null;
|
||||
private env$: Observable<Environment> = this.environmentService.environment$;
|
||||
|
||||
constructor(
|
||||
private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private environmentService: EnvironmentService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.isWeb = this.platformUtilsService.getClientType() === ClientType.Web;
|
||||
this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.permanentFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.NewDeviceVerificationPermanentDismiss,
|
||||
);
|
||||
|
||||
const currentAcct = await firstValueFrom(this.currentAcct$);
|
||||
if (!currentAcct) {
|
||||
return;
|
||||
}
|
||||
this.currentUserId = currentAcct.id;
|
||||
}
|
||||
|
||||
async navigateToTwoStepLogin(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
const env = await firstValueFrom(this.env$);
|
||||
const url = env.getWebVaultUrl();
|
||||
|
||||
if (this.isWeb) {
|
||||
await this.router.navigate(["/settings/security/two-factor"], {
|
||||
queryParams: { fromNewDeviceVerification: true },
|
||||
});
|
||||
} else {
|
||||
this.platformUtilsService.launchUri(
|
||||
url + "/#/settings/security/two-factor/?fromNewDeviceVerification=true",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async navigateToChangeAcctEmail(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
const env = await firstValueFrom(this.env$);
|
||||
const url = env.getWebVaultUrl();
|
||||
if (this.isWeb) {
|
||||
await this.router.navigate(["/settings/account"], {
|
||||
queryParams: { fromNewDeviceVerification: true },
|
||||
});
|
||||
} else {
|
||||
this.platformUtilsService.launchUri(
|
||||
url + "/#/settings/account/?fromNewDeviceVerification=true",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async remindMeLaterSelect() {
|
||||
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState(
|
||||
this.currentUserId!,
|
||||
{
|
||||
last_dismissal: new Date(),
|
||||
permanent_dismissal: false,
|
||||
},
|
||||
);
|
||||
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
}
|
@ -46,6 +46,7 @@ describe("PasswordHistoryViewComponent", () => {
|
||||
|
||||
fixture = TestBed.createComponent(PasswordHistoryViewComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.cipher = mockCipher;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@ -60,8 +61,8 @@ describe("PasswordHistoryViewComponent", () => {
|
||||
beforeEach(async () => {
|
||||
mockCipher.passwordHistory = [password1, password2];
|
||||
|
||||
mockCipherService.get.mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) });
|
||||
await component.ngOnInit();
|
||||
component.cipher = mockCipher;
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
@ -2,13 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { OnInit, Component, Input } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view";
|
||||
import { ItemModule, ColorPasswordModule, IconButtonModule } from "@bitwarden/components";
|
||||
|
||||
@ -20,39 +16,14 @@ import { ItemModule, ColorPasswordModule, IconButtonModule } from "@bitwarden/co
|
||||
})
|
||||
export class PasswordHistoryViewComponent implements OnInit {
|
||||
/**
|
||||
* The ID of the cipher to display the password history for.
|
||||
* Optional cipher view. When included `cipherId` is ignored.
|
||||
*/
|
||||
@Input({ required: true }) cipherId: CipherId;
|
||||
@Input({ required: true }) cipher: CipherView;
|
||||
|
||||
/** The password history for the cipher. */
|
||||
history: PasswordHistoryView[] = [];
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected i18nService: I18nService,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
/** Retrieve the password history for the given cipher */
|
||||
protected async init() {
|
||||
const cipher = await this.cipherService.get(this.cipherId);
|
||||
const activeAccount = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)),
|
||||
);
|
||||
|
||||
if (!activeAccount?.id) {
|
||||
throw new Error("Active account is not available.");
|
||||
}
|
||||
|
||||
const activeUserId = activeAccount.id as UserId;
|
||||
const decCipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
|
||||
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
|
||||
ngOnInit() {
|
||||
this.history = this.cipher.passwordHistory == null ? [] : this.cipher.passwordHistory;
|
||||
}
|
||||
}
|
||||
|
7
libs/vault/src/icons/exclamation-triangle.ts
Normal file
7
libs/vault/src/icons/exclamation-triangle.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const ExclamationTriangle = svgIcon`
|
||||
<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M91.0871 85.1224H28.913C27.5592 85.1349 26.2271 84.7737 25.0549 84.0713C23.8828 83.3688 22.914 82.3578 22.248 81.1386C21.5868 79.9571 21.2405 78.6149 21.2502 77.2502C21.2599 75.8855 21.6207 74.5484 22.2964 73.3768L53.3835 18.7683C54.0665 17.5817 55.0352 16.6008 56.1953 15.9184C57.3554 15.2361 58.6656 14.8773 60 14.8773C61.3345 14.8773 62.6447 15.2361 63.8048 15.9184C64.9649 16.6008 65.9336 17.5817 66.6166 18.7683L97.7036 73.3768C98.3793 74.5484 98.7426 75.8855 98.7499 77.2502C98.7571 78.6149 98.4132 79.9571 97.7521 81.1386C97.0861 82.3578 96.1173 83.3713 94.9451 84.0713C93.773 84.7712 92.4409 85.1349 91.0871 85.1224ZM60 19.8972C59.5084 19.8896 59.0216 20.0176 58.5929 20.2659C58.1643 20.5143 57.8058 20.878 57.554 21.3171L26.4717 75.9256C26.2344 76.3371 26.1085 76.8087 26.1085 77.2878C26.1085 77.767 26.2344 78.2386 26.4717 78.65C26.7188 79.0991 27.0772 79.4704 27.5107 79.7263C27.9442 79.9821 28.4359 80.1126 28.9324 80.1051H91.0871C91.586 80.1126 92.0776 79.9821 92.5087 79.7263C92.9398 79.4704 93.3007 79.0991 93.5477 78.65C93.7851 78.2386 93.911 77.767 93.911 77.2878C93.911 76.8087 93.7851 76.3371 93.5477 75.9256L62.4461 21.3171C62.1943 20.878 61.8358 20.5168 61.4071 20.2659C60.9785 20.0151 60.4917 19.8896 60 19.8972ZM60 62.8705C59.3582 62.8705 58.7407 62.6071 58.2878 62.1355C57.8349 61.6639 57.5782 61.0267 57.5782 60.3619V37.4177C57.5782 36.7529 57.8325 36.1132 58.2878 35.644C58.7431 35.1749 59.3582 34.909 60 34.909C60.6418 34.909 61.2594 35.1724 61.7123 35.644C62.1652 36.1157 62.4219 36.7529 62.4219 37.4177V60.3619C62.4219 61.0267 62.1676 61.6664 61.7123 62.1355C61.257 62.6046 60.6418 62.8705 60 62.8705ZM60 75.2734C61.5864 75.2734 62.8724 73.9413 62.8724 72.2981C62.8724 70.6549 61.5864 69.3228 60 69.3228C58.4137 69.3228 57.1277 70.6549 57.1277 72.2981C57.1277 73.9413 58.4137 75.2734 60 75.2734Z" class="tw-fill-warning-600" />
|
||||
</svg>
|
||||
`;
|
@ -2,3 +2,5 @@ export * from "./deactivated-org";
|
||||
export * from "./no-folders";
|
||||
export * from "./vault";
|
||||
export * from "./empty-trash";
|
||||
export * from "./exclamation-triangle";
|
||||
export * from "./user-lock";
|
||||
|
17
libs/vault/src/icons/user-lock.ts
Normal file
17
libs/vault/src/icons/user-lock.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const UserLock = svgIcon`
|
||||
<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M0 19.6127C0 14.9806 3.71508 11.2256 8.29787 11.2256H89.5153C94.098 11.2256 97.8131 14.9806 97.8131 19.6127V28.0844H95.2599V19.6127C95.2599 16.4059 92.688 13.8062 89.5153 13.8062H8.29787C5.12517 13.8062 2.55319 16.4059 2.55319 19.6127V68.6449C2.55319 71.8518 5.12517 74.4514 8.29787 74.4514H16.2389V77.032H8.29787C3.71509 77.032 0 73.277 0 68.6449V19.6127ZM50.9015 74.4514H63.2653V77.032H50.9015V74.4514Z" />
|
||||
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M88.7235 60.2578C97.5366 60.2578 104.681 53.0366 104.681 44.1287C104.681 35.2209 97.5366 27.9997 88.7235 27.9997C79.9105 27.9997 72.7661 35.2209 72.7661 44.1287C72.7661 53.0366 79.9105 60.2578 88.7235 60.2578ZM88.7235 62.8384C98.9467 62.8384 107.234 54.4618 107.234 44.1287C107.234 33.7957 98.9467 25.4191 88.7235 25.4191C78.5004 25.4191 70.2129 33.7957 70.2129 44.1287C70.2129 54.4618 78.5004 62.8384 88.7235 62.8384Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M33.8298 46.0642C28.7286 46.0642 24.2553 51.0589 24.2553 57.6771H21.7021C21.7021 50.0428 26.9452 43.4835 33.8298 43.4835C40.7144 43.4835 45.9575 50.0428 45.9575 57.6771H43.4043C43.4043 51.0589 38.931 46.0642 33.8298 46.0642Z" />
|
||||
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M60.8742 88.6448H115.299C116.596 88.6448 117.089 88.3032 117.227 88.1603C117.3 88.0845 117.558 87.7852 117.396 86.7815C115.225 73.2724 103.259 62.8383 88.7118 62.8383C74.1651 62.8383 62.1986 73.2724 60.0274 86.7815C59.9451 87.2938 60.0532 87.888 60.2785 88.2731C60.379 88.445 60.4746 88.5285 60.5381 88.5682C60.5886 88.5998 60.6806 88.6448 60.8742 88.6448ZM60.8742 91.2254H115.299C118.653 91.2254 120.416 89.4793 119.916 86.3677C117.539 71.5724 104.473 60.2577 88.7118 60.2577C72.9505 60.2577 59.8851 71.5724 57.5073 86.3677C57.1687 88.4743 58.2526 91.2254 60.8742 91.2254Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M44.6808 58.9675H22.9787C20.1585 58.9675 17.8723 61.2783 17.8723 64.1288V79.6126C17.8723 82.4631 20.1585 84.7739 22.9787 84.7739H44.6808C47.501 84.7739 49.7872 82.4631 49.7872 79.6126V64.1288C49.7872 61.2783 47.501 58.9675 44.6808 58.9675ZM22.9787 56.3868C18.7484 56.3868 15.3191 59.853 15.3191 64.1288V79.6126C15.3191 83.8884 18.7484 87.3546 22.9787 87.3546H44.6808C48.9111 87.3546 52.3404 83.8884 52.3404 79.6126V64.1288C52.3404 59.853 48.9111 56.3868 44.6808 56.3868H22.9787Z"/>
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M33.8301 71.8707C34.5351 71.8707 35.1067 72.4484 35.1067 73.1611L35.1067 77.6923C35.1067 78.4049 34.5351 78.9826 33.8301 78.9826C33.125 78.9826 32.5535 78.4049 32.5535 77.6923L32.5535 73.1611C32.5535 72.4484 33.125 71.8707 33.8301 71.8707Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M78.315 18.3374C77.6099 18.3374 77.0384 17.7597 77.0384 17.0471V16.5436C77.0384 15.831 77.6099 15.2533 78.315 15.2533C79.02 15.2533 79.5916 15.831 79.5916 16.5436V17.0471C79.5916 17.7597 79.02 18.3374 78.315 18.3374Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M33.8299 74.3097C32.4198 74.3097 31.2767 73.1543 31.2767 71.729V71.2256C31.2767 69.8003 32.4198 68.6449 33.8299 68.6449C35.24 68.6449 36.3831 69.8003 36.3831 71.2256V71.729C36.3831 73.1543 35.24 74.3097 33.8299 74.3097Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M84.2903 18.3374C83.5853 18.3374 83.0137 17.7597 83.0137 17.0471V16.5436C83.0137 15.831 83.5853 15.2533 84.2903 15.2533C84.9953 15.2533 85.5669 15.831 85.5669 16.5436V17.0471C85.5669 17.7597 84.9953 18.3374 84.2903 18.3374Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M90.2644 18.3374C89.5594 18.3374 88.9878 17.7597 88.9878 17.0471V16.5436C88.9878 15.831 89.5594 15.2533 90.2644 15.2533C90.9695 15.2533 91.541 15.831 91.541 16.5436V17.0471C91.541 17.7597 90.9695 18.3374 90.2644 18.3374Z"/>
|
||||
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M95.7422 22.0817H0.638428V20.7914H95.7422V22.0817Z" />
|
||||
</svg>
|
||||
`;
|
@ -14,5 +14,7 @@ export {
|
||||
|
||||
export { DownloadAttachmentComponent } from "./components/download-attachment/download-attachment.component";
|
||||
export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component";
|
||||
export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component";
|
||||
export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component";
|
||||
|
||||
export * as VaultIcons from "./icons";
|
||||
|
@ -57,7 +57,7 @@ export class NewDeviceVerificationNoticeService {
|
||||
}
|
||||
|
||||
async updateNewDeviceVerificationNoticeState(
|
||||
userId: UserId,
|
||||
userId: UserId | null,
|
||||
newState: NewDeviceVerificationNotice,
|
||||
): Promise<void> {
|
||||
await this.noticeState(userId).update(() => {
|
||||
|
18
package-lock.json
generated
18
package-lock.json
generated
@ -68,7 +68,7 @@
|
||||
"qrious": "4.0.2",
|
||||
"rxjs": "7.8.1",
|
||||
"tabbable": "6.2.0",
|
||||
"tldts": "6.1.66",
|
||||
"tldts": "6.1.69",
|
||||
"utf-8-validate": "6.0.5",
|
||||
"zone.js": "0.14.10",
|
||||
"zxcvbn": "4.4.2"
|
||||
@ -221,7 +221,7 @@
|
||||
"papaparse": "5.4.1",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"rxjs": "7.8.1",
|
||||
"tldts": "6.1.66",
|
||||
"tldts": "6.1.69",
|
||||
"zxcvbn": "4.4.2"
|
||||
},
|
||||
"bin": {
|
||||
@ -31135,21 +31135,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.66",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.66.tgz",
|
||||
"integrity": "sha512-l3ciXsYFel/jSRfESbyKYud1nOw7WfhrBEF9I3UiarYk/qEaOOwu3qXNECHw4fHGHGTEOuhf/VdKgoDX5M/dhQ==",
|
||||
"version": "6.1.69",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.69.tgz",
|
||||
"integrity": "sha512-Oh/CqRQ1NXNY7cy9NkTPUauOWiTro0jEYZTioGbOmcQh6EC45oribyIMJp0OJO3677r13tO6SKdWoGZUx2BDFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.66"
|
||||
"tldts-core": "^6.1.69"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "6.1.66",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.66.tgz",
|
||||
"integrity": "sha512-s07jJruSwndD2X8bVjwioPfqpIc1pDTzszPe9pL1Skbh4bjytL85KNQ3tolqLbCvpQHawIsGfFi9dgerWjqW4g==",
|
||||
"version": "6.1.69",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.69.tgz",
|
||||
"integrity": "sha512-nygxy9n2PBUFQUtAXAc122gGo+04/j5qr5TGQFZTHafTKYvmARVXt2cA5rgero2/dnXUfkdPtiJoKmrd3T+wdA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
|
@ -198,7 +198,7 @@
|
||||
"qrious": "4.0.2",
|
||||
"rxjs": "7.8.1",
|
||||
"tabbable": "6.2.0",
|
||||
"tldts": "6.1.66",
|
||||
"tldts": "6.1.69",
|
||||
"utf-8-validate": "6.0.5",
|
||||
"zone.js": "0.14.10",
|
||||
"zxcvbn": "4.4.2"
|
||||
|
Loading…
Reference in New Issue
Block a user