1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-04 09:33: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

@@ -1,11 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
// 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 { CollectionView } from "@bitwarden/admin-console/common";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { Importer } from "../importers/importer";
import { ImporterMetadata } from "../metadata";
import { ImportOption, ImportType } from "../models/import-options";
import { ImportResult } from "../models/import-result";
@@ -13,6 +16,10 @@ export abstract class ImportServiceAbstraction {
featuredImportOptions: readonly ImportOption[];
regularImportOptions: readonly ImportOption[];
getImportOptions: () => ImportOption[];
/** describes the features supported by a format */
metadata$: (type$: Observable<ImportType>) => Observable<ImporterMetadata>;
import: (
importer: Importer,
fileContents: string,

View File

@@ -1,14 +1,20 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, Subject, firstValueFrom } from "rxjs";
// 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 { ClientType } from "@bitwarden/client-type";
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { MockSdkService } from "@bitwarden/common/platform/spec/mock-sdk.service";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@@ -19,6 +25,8 @@ import { KeyService } from "@bitwarden/key-management";
import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
import { Importer } from "../importers/importer";
import { ImporterMetadata, Instructions, Loader } from "../metadata";
import { ImportType } from "../models";
import { ImportResult } from "../models/import-result";
import { ImportApiServiceAbstraction } from "./import-api.service.abstraction";
@@ -35,8 +43,8 @@ describe("ImportService", () => {
let encryptService: MockProxy<EncryptService>;
let pinService: MockProxy<PinServiceAbstraction>;
let accountService: MockProxy<AccountService>;
let sdkService: MockSdkService;
let restrictedItemTypesService: MockProxy<RestrictedItemTypesService>;
let systemServiceProvider: MockProxy<SystemServiceProvider>;
beforeEach(() => {
cipherService = mock<CipherService>();
@@ -47,9 +55,20 @@ describe("ImportService", () => {
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
pinService = mock<PinServiceAbstraction>();
sdkService = new MockSdkService();
restrictedItemTypesService = mock<RestrictedItemTypesService>();
const configService = mock<ConfigService>();
configService.getFeatureFlag$.mockReturnValue(new BehaviorSubject(false));
const environment = mock<PlatformUtilsService>();
environment.getClientType.mockReturnValue(ClientType.Desktop);
systemServiceProvider = mock<SystemServiceProvider>({
configService,
environment,
log: jest.fn().mockReturnValue({ debug: jest.fn() }),
});
importService = new ImportService(
cipherService,
folderService,
@@ -60,8 +79,8 @@ describe("ImportService", () => {
encryptService,
pinService,
accountService,
sdkService,
restrictedItemTypesService,
systemServiceProvider,
);
});
@@ -249,6 +268,170 @@ describe("ImportService", () => {
expect(importResult.folderRelationships[1]).toEqual([0, 1]);
});
});
describe("metadata$", () => {
let featureFlagSubject: BehaviorSubject<boolean>;
let typeSubject: Subject<ImportType>;
let mockLogger: { debug: jest.Mock };
beforeEach(() => {
featureFlagSubject = new BehaviorSubject(false);
typeSubject = new Subject<ImportType>();
mockLogger = { debug: jest.fn() };
const configService = mock<ConfigService>();
configService.getFeatureFlag$.mockReturnValue(featureFlagSubject);
const environment = mock<PlatformUtilsService>();
environment.getClientType.mockReturnValue(ClientType.Desktop);
systemServiceProvider = mock<SystemServiceProvider>({
configService,
environment,
log: jest.fn().mockReturnValue(mockLogger),
});
// Recreate the service with the updated mocks for logging tests
importService = new ImportService(
cipherService,
folderService,
importApiService,
i18nService,
collectionService,
keyService,
encryptService,
pinService,
accountService,
restrictedItemTypesService,
systemServiceProvider,
);
});
afterEach(() => {
featureFlagSubject.complete();
typeSubject.complete();
});
it("should emit metadata when type$ emits", async () => {
const testType: ImportType = "chromecsv";
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
const result = await metadataPromise;
expect(result).toEqual({
type: testType,
loaders: expect.any(Array),
instructions: Instructions.chromium,
});
expect(result.type).toBe(testType);
});
it("should include all loaders when chromium feature flag is enabled", async () => {
const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders
featureFlagSubject.next(true);
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
const result = await metadataPromise;
expect(result.loaders).toContain(Loader.chromium);
expect(result.loaders).toContain(Loader.file);
});
it("should exclude chromium loader when feature flag is disabled", async () => {
const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders
featureFlagSubject.next(false);
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
const result = await metadataPromise;
expect(result.loaders).not.toContain(Loader.chromium);
expect(result.loaders).toContain(Loader.file);
});
it("should update when type$ changes", async () => {
const emissions: ImporterMetadata[] = [];
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
emissions.push(metadata);
});
typeSubject.next("chromecsv");
typeSubject.next("bravecsv");
// Wait for emissions
await new Promise((resolve) => setTimeout(resolve, 0));
expect(emissions).toHaveLength(2);
expect(emissions[0].type).toBe("chromecsv");
expect(emissions[1].type).toBe("bravecsv");
subscription.unsubscribe();
});
it("should update when feature flag changes", async () => {
const testType: ImportType = "bravecsv"; // Use bravecsv which supports chromium loader
const emissions: ImporterMetadata[] = [];
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
emissions.push(metadata);
});
typeSubject.next(testType);
featureFlagSubject.next(true);
// Wait for emissions
await new Promise((resolve) => setTimeout(resolve, 0));
expect(emissions).toHaveLength(2);
expect(emissions[0].loaders).not.toContain(Loader.chromium);
expect(emissions[1].loaders).toContain(Loader.chromium);
subscription.unsubscribe();
});
it("should update when both type$ and feature flag change", async () => {
const emissions: ImporterMetadata[] = [];
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
emissions.push(metadata);
});
// Initial emission
typeSubject.next("chromecsv");
// Change both at the same time
featureFlagSubject.next(true);
typeSubject.next("bravecsv");
// Wait for emissions
await new Promise((resolve) => setTimeout(resolve, 0));
expect(emissions.length).toBeGreaterThanOrEqual(2);
const lastEmission = emissions[emissions.length - 1];
expect(lastEmission.type).toBe("bravecsv");
subscription.unsubscribe();
});
it("should log debug information with correct data", async () => {
const testType: ImportType = "chromecsv";
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
await metadataPromise;
expect(mockLogger.debug).toHaveBeenCalledWith(
{ importType: testType, capabilities: expect.any(Object) },
"capabilities updated",
);
});
});
});
function createCipher(options: Partial<CipherView> = {}) {

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, map } from "rxjs";
import { combineLatest, firstValueFrom, map, Observable } from "rxjs";
// 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
@@ -10,6 +10,7 @@ import {
CollectionView,
} from "@bitwarden/admin-console/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
@@ -17,8 +18,9 @@ import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/reque
import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SemanticLogger } from "@bitwarden/common/tools/log";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@@ -95,6 +97,7 @@ import {
PasswordDepot17XmlImporter,
} from "../importers";
import { Importer } from "../importers/importer";
import { ImporterMetadata, Importers, Loader } from "../metadata";
import {
featuredImportOptions,
ImportOption,
@@ -104,12 +107,15 @@ import {
import { ImportResult } from "../models/import-result";
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
import { availableLoaders as availableLoaders } from "../util";
export class ImportService implements ImportServiceAbstraction {
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
regularImportOptions = regularImportOptions as readonly ImportOption[];
private logger: SemanticLogger;
constructor(
private cipherService: CipherService,
private folderService: FolderService,
@@ -120,14 +126,42 @@ export class ImportService implements ImportServiceAbstraction {
private encryptService: EncryptService,
private pinService: PinServiceAbstraction,
private accountService: AccountService,
private sdkService: SdkService,
private restrictedItemTypesService: RestrictedItemTypesService,
) {}
private system: SystemServiceProvider,
) {
this.logger = system.log({ type: "ImportService" });
}
getImportOptions(): ImportOption[] {
return this.featuredImportOptions.concat(this.regularImportOptions);
}
metadata$(type$: Observable<ImportType>): Observable<ImporterMetadata> {
const browserEnabled$ = this.system.configService.getFeatureFlag$(
FeatureFlag.UseChromiumImporter,
);
const client = this.system.environment.getClientType();
const capabilities$ = combineLatest([type$, browserEnabled$]).pipe(
map(([type, enabled]) => {
let loaders = availableLoaders(type, client);
if (!enabled) {
loaders = loaders?.filter((loader) => loader !== Loader.chromium);
}
const capabilities: ImporterMetadata = { type, loaders };
if (type in Importers) {
capabilities.instructions = Importers[type].instructions;
}
this.logger.debug({ importType: type, capabilities }, "capabilities updated");
return capabilities;
}),
);
return capabilities$;
}
async import(
importer: Importer,
fileContents: string,
@@ -260,6 +294,7 @@ export class ImportService implements ImportServiceAbstraction {
case "chromecsv":
case "operacsv":
case "vivaldicsv":
case "bravecsv":
return new ChromeCsvImporter();
case "firefoxcsv":
return new FirefoxCsvImporter();