mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
Merge branch 'PM-12985-Reports' of github.com:bitwarden/clients into PM-12985-Reports
This commit is contained in:
@@ -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,8 +9,46 @@ 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.
|
||||
When the user selects a credential from your app, this method will be called with the
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user