1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-24748][PM-24072] Chromium importer (#16100)

* Add importer dummy lib, add cargo deps for win/mac

* Add Chromium importer source from bitwarden/password-access

* Mod crypto is no more

* Expose some Chromium importer functions via NAPI, replace home with home_dir crate

* Add Chromium importer to the main <-> renderer IPC, export all functions from Rust

* Add password and notes fields to the imported logins

* Fix windows to use homedir instead of home

* Return success/failure results

* Import from account logins and join

* Linux v10 support

* Use mod util on Windows

* Use mod util on macOS

* Refactor to move shared code into chromium.rs

* Fix windows

* Fix Linux as well

* Linux v11 support for Chrome/Gnome, everything is async now

* Support multiple browsers on Linux v11

* Move oo7 to Linux

* Fix Windows

* Fix macOS

* Add support for Brave browser in Linux configuration

* Add support for Opera browser in Linux configuration

* Fix Edge and add Arc on macOS

* Add Opera on macOS

* Add support for Vivaldi browser in macOS configuration

* Add support for Chromium browser in macOS configuration

* Fix Edge on Windows

* Add Opera on Windows

* Add Vivaldi on windows

* Add Chromium to supported browsers on Windows

* stub out UI options for chromium direct import

* call IPC funcs from import-desktop

* add notes to chrome csv importer

* remove (csv) from import tool names and format item names as hostnames

* Add ABE/v20 encryption support

* ABE/v20 architecture description

* Add a build step to produce admin.exe and service.exe

* Add Windows v20/ABE configuration functionality to specify the full path to the admin.exe and service.exe. Use ipc.platform.chromiumImporter.configureWindowsCryptoService to configure the Chromium importer on Windows.

* rename ARCHITECTURE.md to README.md

* aligns with guidance from architecture re: in-repository documentation.
* also fixes a failing lint.

* cargo fmt

* cargo clippy fix

* Declare feature flag for using chromium importer

* Linter fix after executing npm run prettier

* Use feature flag to guard the use of the chromium importer

* Added temporary logging to further debug, why the Angular change detection isn't working as expected

* introduce importer metadata; host metadata from service; includes tests

* fix cli build

* Register autotype module in lib.rs
introduce by a bad merge

* Fix web build

* Fix issue with loaders being undefined and the feature flag turned off

* Add missing Chromium support when selecting chromecsv

* debugging

* remove chromium support from chromecsv metadata

* fix default loader selection

* [PM-24753] cargo lib file (#16090)

* Add new modules

* Fix chromium importer

* Fix compile bugs for toolchain

* remove importer folder

* remove IPC code

* undo setting change

* clippy fixes

* cargo fmt

* clippy fixes

* clippy fixes

* clippy fixes

* clippy fixes

* lint fix

* fix release build

* Add files in CODEOWNERS

* Create tools owned preload.ts

* Move chromium-importer.service under tools-ownership

* Fix typeError
When accessing the Chromium direct import options the file button is hidden, so trying to access it's values will fail

* Fix tools owned preload

* Remove dead code and redundant truncation

* Remove configureWindowsCryptoService function/methods

* Clean up cargo files

* Fix unused async

* Update apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Fix napi deps

* fix lints

* format

* fix linux lint

* fix windows lints

* format

* fix missing `?`

* fix a different missing `?`

---------

Co-authored-by: Dmitry Yakimenko <detunized@gmail.com>
Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by:  Audrey  <ajensen@bitwarden.com>
Co-authored-by:  Audrey  <audrey@audreyality.com>
Co-authored-by: adudek-bw <adudek@bitwarden.com>
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
Daniel James Smith
2025-09-04 11:21:57 +02:00
committed by GitHub
parent b957a0c28f
commit 66f5700a75
46 changed files with 2436 additions and 90 deletions

View File

@@ -0,0 +1,8 @@
<div [formGroup]="formGroup">
<bit-form-field>
<bit-label>{{ "browserProfile" | i18n }}</bit-label>
<bit-select formControlName="profile">
<bit-option *ngFor="let p of profileList" [value]="p.id" [label]="p.name" />
</bit-select>
</bit-form-field>
</div>

View File

@@ -0,0 +1,167 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import {
AsyncValidatorFn,
ControlContainer,
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
import * as papa from "papaparse";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
CalloutModule,
CheckboxModule,
FormFieldModule,
IconButtonModule,
SelectModule,
TypographyModule,
} from "@bitwarden/components";
import { ImportType } from "../../models";
@Component({
selector: "import-chrome",
templateUrl: "import-chrome.component.html",
standalone: true,
imports: [
CommonModule,
JslibModule,
CalloutModule,
TypographyModule,
FormFieldModule,
ReactiveFormsModule,
IconButtonModule,
CheckboxModule,
SelectModule,
],
})
export class ImportChromeComponent implements OnInit, OnDestroy {
private _parentFormGroup: FormGroup;
protected formGroup = this.formBuilder.group({
profile: [
"",
{
nonNullable: true,
validators: [Validators.required],
asyncValidators: [this.validateAndEmitData()],
updateOn: "submit",
},
],
});
profileList: { id: string; name: string }[] = [];
@Input()
format: ImportType;
@Input()
onLoadProfilesFromBrowser: (browser: string) => Promise<any[]>;
@Input()
onImportFromBrowser: (browser: string, profile: string) => Promise<any[]>;
@Output() csvDataLoaded = new EventEmitter<string>();
constructor(
private formBuilder: FormBuilder,
private controlContainer: ControlContainer,
private logService: LogService,
private i18nService: I18nService,
) {}
async ngOnInit(): Promise<void> {
this._parentFormGroup = this.controlContainer.control as FormGroup;
this._parentFormGroup.addControl("chromeOptions", this.formGroup);
this.profileList = await this.onLoadProfilesFromBrowser(this.getBrowserName());
}
ngOnDestroy(): void {
this._parentFormGroup.removeControl("chromeOptions");
}
/**
* Attempts to login to the provided Chrome email and retrieve account contents.
* Will return a validation error if unable to login or fetch.
* Emits account contents to `csvDataLoaded`
*/
validateAndEmitData(): AsyncValidatorFn {
return async () => {
try {
const logins = await this.onImportFromBrowser(
this.getBrowserName(),
this.formGroup.controls.profile.value,
);
if (logins.length === 0) {
throw "nothing to import";
}
const chromeLogins: ChromeLogin[] = [];
for (const l of logins) {
if (l.login != null) {
chromeLogins.push(new ChromeLogin(l.login));
}
}
const csvData = papa.unparse(chromeLogins);
this.csvDataLoaded.emit(csvData);
return null;
} catch (error) {
this.logService.error(`Chromium importer error: ${error}`);
return {
errors: {
message: this.i18nService.t(this.getValidationErrorI18nKey(error)),
},
};
}
};
}
private getValidationErrorI18nKey(error: any): string {
const message = typeof error === "string" ? error : error?.message;
switch (message) {
default:
return "errorOccurred";
}
}
private getBrowserName(): string {
if (this.format === "edgecsv") {
return "Microsoft Edge";
} else if (this.format === "operacsv") {
return "Opera";
} else if (this.format === "bravecsv") {
return "Brave";
} else if (this.format === "vivaldicsv") {
return "Vivaldi";
}
return "Chrome";
}
}
class ChromeLogin {
name: string;
url: string;
username: string;
password: string;
note: string;
constructor(login: any) {
const url = Utils.getUrl(login?.url);
if (url != null) {
this.name = new URL(url).hostname;
}
if (this.name == null) {
this.name = login.url;
}
this.url = login.url;
this.username = login.username;
this.password = login.password;
this.note = login.note;
}
}

View File

@@ -0,0 +1 @@
export { ImportChromeComponent } from "./import-chrome.component";

View File

@@ -169,27 +169,41 @@
"Export" to save the JSON file.
</ng-container>
-->
<ng-container
*ngIf="
format === 'chromecsv' ||
format === 'operacsv' ||
format === 'vivaldicsv' ||
format === 'edgecsv'
"
>
<ng-container *ngIf="showChromiumInstructions$ | async">
<span *ngIf="format !== 'chromecsv'">
The process is exactly the same as importing from Google Chrome.
</span>
See detailed instructions on our help site at
<a
bitLink
linkType="primary"
target="_blank"
rel="noreferrer"
href="https://bitwarden.com/help/import-from-chrome/"
>
https://bitwarden.com/help/import-from-chrome/</a
<p>
See detailed instructions on our help site at
<a
bitLink
linkType="primary"
target="_blank"
rel="noreferrer"
href="https://bitwarden.com/help/import-from-chrome/"
>
https://bitwarden.com/help/import-from-chrome/</a
>
</p>
<bit-radio-group
[hidden]="!(browserImporterAvailable$ | async)"
formControlName="chromiumLoader"
>
<bit-radio-button
class="tw-block"
id="import_bit-radio-button_chrome-browser"
value="chromium"
>
<bit-label>{{ "importDirectlyFromBrowser" | i18n }}</bit-label>
</bit-radio-button>
<bit-radio-button
class="tw-block"
id="import_bit-radio-button_chrome-file"
value="file"
>
<bit-label>{{ "importFromCSV" | i18n }}</bit-label>
</bit-radio-button>
</bit-radio-group>
</ng-container>
<ng-container *ngIf="format === 'firefoxcsv'">
See detailed instructions on our help site at
@@ -440,12 +454,20 @@
previously chosen.
</ng-container>
</bit-callout>
<import-lastpass
*ngIf="showLastPassOptions"
[formGroup]="formGroup"
(csvDataLoaded)="this.formGroup.controls.fileContents.setValue($event)"
></import-lastpass>
<div [hidden]="showLastPassOptions">
@if (showLastPassOptions) {
<import-lastpass
[formGroup]="formGroup"
(csvDataLoaded)="this.formGroup.controls.fileContents.setValue($event)"
></import-lastpass>
} @else if (showChromiumOptions$ | async) {
<import-chrome
[formGroup]="formGroup"
[onImportFromBrowser]="this.onImportFromBrowser"
[onLoadProfilesFromBrowser]="this.onLoadProfilesFromBrowser"
[format]="this.format"
(csvDataLoaded)="this.formGroup.controls.fileContents.setValue($event)"
></import-chrome>
} @else {
<bit-form-field>
<bit-label>{{ "selectImportFile" | i18n }}</bit-label>
<div class="file-selector tw-pt-2 tw-pb-1 tw-break-words">
@@ -473,7 +495,7 @@
formControlName="fileContents"
></textarea>
</bit-form-field>
</div>
}
</bit-card>
</bit-section>
</form>

View File

@@ -4,6 +4,7 @@ import { CommonModule } from "@angular/common";
import {
AfterViewInit,
Component,
DestroyRef,
EventEmitter,
Inject,
Input,
@@ -13,17 +14,23 @@ import {
Output,
ViewChild,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import * as JSZip from "jszip";
import { Observable, Subject, lastValueFrom, combineLatest, firstValueFrom } from "rxjs";
import {
Observable,
Subject,
lastValueFrom,
combineLatest,
firstValueFrom,
BehaviorSubject,
} from "rxjs";
import { combineLatestWith, filter, map, switchMap, takeUntil } from "rxjs/operators";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { safeProvider, SafeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import {
getOrganizationById,
OrganizationService,
@@ -34,14 +41,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ClientType } from "@bitwarden/common/enums";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
@@ -62,49 +65,20 @@ import {
ToastService,
LinkModule,
} from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { ImporterMetadata, DataLoader, Loader, Instructions } from "../metadata";
import { ImportOption, ImportResult, ImportType } from "../models";
import {
ImportApiService,
ImportApiServiceAbstraction,
ImportCollectionServiceAbstraction,
ImportService,
ImportServiceAbstraction,
} from "../services";
import { ImportCollectionServiceAbstraction, ImportServiceAbstraction } from "../services";
import { ImportChromeComponent } from "./chrome";
import {
FilePasswordPromptComponent,
ImportErrorDialogComponent,
ImportSuccessDialogComponent,
} from "./dialog";
import { ImporterProviders } from "./importer-providers";
import { ImportLastPassComponent } from "./lastpass";
const safeProviders: SafeProvider[] = [
safeProvider({
provide: ImportApiServiceAbstraction,
useClass: ImportApiService,
deps: [ApiService],
}),
safeProvider({
provide: ImportServiceAbstraction,
useClass: ImportService,
deps: [
CipherService,
FolderService,
ImportApiServiceAbstraction,
I18nService,
CollectionService,
KeyService,
EncryptService,
PinServiceAbstraction,
AccountService,
SdkService,
RestrictedItemTypesService,
],
}),
];
@Component({
selector: "tools-import",
templateUrl: "import.component.html",
@@ -118,6 +92,7 @@ const safeProviders: SafeProvider[] = [
SelectModule,
CalloutModule,
ReactiveFormsModule,
ImportChromeComponent,
ImportLastPassComponent,
RadioButtonModule,
CardComponent,
@@ -125,7 +100,7 @@ const safeProviders: SafeProvider[] = [
SectionComponent,
LinkModule,
],
providers: safeProviders,
providers: ImporterProviders,
})
export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
featuredImportOptions: ImportOption[];
@@ -160,6 +135,12 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
});
}
@Input()
onLoadProfilesFromBrowser: (browser: string) => Promise<any[]>;
@Input()
onImportFromBrowser: (browser: string, profile: string) => Promise<any[]>;
protected organization: Organization;
protected destroy$ = new Subject<void>();
@@ -184,6 +165,8 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
fileContents: [],
file: [],
lastPassType: ["direct" as "csv" | "direct"],
// FIXME: once the flag is disabled this should initialize to `Strategy.browser`
chromiumLoader: [Loader.file as DataLoader],
});
@ViewChild(BitSubmitDirective)
@@ -208,6 +191,26 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
});
}
private importer$ = new BehaviorSubject<ImporterMetadata | undefined>(undefined);
/** emits `true` when the chromium instruction block should be visible. */
protected readonly showChromiumInstructions$ = this.importer$.pipe(
map((importer) => importer?.instructions === Instructions.chromium),
);
/** emits `true` when direct browser import is available. */
// FIXME: use the capabilities list to populate `chromiumLoader` and replace the explicit
// strategy check with a check for multiple loaders
protected readonly browserImporterAvailable$ = this.importer$.pipe(
map((importer) => (importer?.loaders ?? []).includes(Loader.chromium)),
);
/** emits `true` when the chromium loader is selected. */
protected readonly showChromiumOptions$ =
this.formGroup.controls.chromiumLoader.valueChanges.pipe(
map((chromiumLoader) => chromiumLoader === Loader.chromium),
);
constructor(
protected i18nService: I18nService,
protected importService: ImportServiceAbstraction,
@@ -226,6 +229,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
protected toastService: ToastService,
protected accountService: AccountService,
private restrictedItemTypesService: RestrictedItemTypesService,
private destroyRef: DestroyRef,
) {}
protected get importBlockedByPolicy(): boolean {
@@ -246,6 +250,23 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
async ngOnInit() {
this.setImportOptions();
this.importService
.metadata$(this.formGroup.controls.format.valueChanges)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (importer) => {
this.importer$.next(importer);
// when an importer is defined, the loader needs to be set to a value from
// its list.
const loader = importer.loaders.includes(Loader.chromium)
? Loader.chromium
: importer.loaders?.[0];
this.formGroup.controls.chromiumLoader.setValue(loader ?? Loader.file);
},
error: (err: unknown) => this.logService.error("an error occurred", err),
});
if (this.organizationId) {
await this.handleOrganizationImportInit();
} else {
@@ -578,7 +599,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
private async setImportContents(): Promise<string> {
const fileEl = document.getElementById("import_input_file") as HTMLInputElement;
const files = fileEl.files;
const files = fileEl?.files;
let fileContents = this.formGroup.controls.fileContents.value;
if (files != null && files.length > 0) {

View File

@@ -0,0 +1,91 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CollectionService } from "@bitwarden/admin-console/common";
import { safeProvider, SafeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
import { ExtensionRegistry } from "@bitwarden/common/tools/extension/extension-registry.abstraction";
import { buildExtensionRegistry } from "@bitwarden/common/tools/extension/factory";
import {
createSystemServiceProvider,
SystemServiceProvider,
} from "@bitwarden/common/tools/providers";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { KeyService } from "@bitwarden/key-management";
import { StateProvider } from "@bitwarden/state";
import { SafeInjectionToken } from "@bitwarden/ui-common";
import {
ImportApiService,
ImportApiServiceAbstraction,
ImportService,
ImportServiceAbstraction,
} from "../services";
// FIXME: unify with `SYSTEM_SERVICE_PROVIDER` when migrating it from the generator component module
// to a general module.
const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>("SystemServices");
/** Import service factories */
export const ImporterProviders: SafeProvider[] = [
safeProvider({
provide: ImportApiServiceAbstraction,
useClass: ImportApiService,
deps: [ApiService],
}),
safeProvider({
provide: LegacyEncryptorProvider,
useClass: KeyServiceLegacyEncryptorProvider,
deps: [EncryptService, KeyService],
}),
safeProvider({
provide: ExtensionRegistry,
useFactory: () => {
return buildExtensionRegistry();
},
deps: [],
}),
safeProvider({
provide: SYSTEM_SERVICE_PROVIDER,
useFactory: createSystemServiceProvider,
deps: [
LegacyEncryptorProvider,
StateProvider,
PolicyService,
ExtensionRegistry,
LogService,
PlatformUtilsService,
ConfigService,
],
}),
safeProvider({
provide: ImportServiceAbstraction,
useClass: ImportService,
deps: [
CipherService,
FolderService,
ImportApiServiceAbstraction,
I18nService,
CollectionService,
KeyService,
EncryptService,
PinServiceAbstraction,
AccountService,
RestrictedItemTypesService,
SYSTEM_SERVICE_PROVIDER,
],
}),
];