mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +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:
committed by
GitHub
parent
b957a0c28f
commit
66f5700a75
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -29,6 +29,7 @@ libs/common/src/auth @bitwarden/team-auth-dev
|
|||||||
apps/browser/src/tools @bitwarden/team-tools-dev
|
apps/browser/src/tools @bitwarden/team-tools-dev
|
||||||
apps/cli/src/tools @bitwarden/team-tools-dev
|
apps/cli/src/tools @bitwarden/team-tools-dev
|
||||||
apps/desktop/src/app/tools @bitwarden/team-tools-dev
|
apps/desktop/src/app/tools @bitwarden/team-tools-dev
|
||||||
|
apps/desktop/desktop_native/bitwarden_chromium_importer @bitwarden/team-tools-dev
|
||||||
apps/web/src/app/tools @bitwarden/team-tools-dev
|
apps/web/src/app/tools @bitwarden/team-tools-dev
|
||||||
libs/angular/src/tools @bitwarden/team-tools-dev
|
libs/angular/src/tools @bitwarden/team-tools-dev
|
||||||
libs/common/src/models/export @bitwarden/team-tools-dev
|
libs/common/src/models/export @bitwarden/team-tools-dev
|
||||||
|
|||||||
@@ -158,10 +158,13 @@ import { ApiService } from "@bitwarden/common/services/api.service";
|
|||||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||||
|
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
|
||||||
|
import { buildExtensionRegistry } from "@bitwarden/common/tools/extension/factory";
|
||||||
import {
|
import {
|
||||||
PasswordStrengthService,
|
PasswordStrengthService,
|
||||||
PasswordStrengthServiceAbstraction,
|
PasswordStrengthServiceAbstraction,
|
||||||
} from "@bitwarden/common/tools/password-strength";
|
} from "@bitwarden/common/tools/password-strength";
|
||||||
|
import { createSystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
||||||
import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||||
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
||||||
@@ -1056,8 +1059,16 @@ export default class MainBackground {
|
|||||||
this.encryptService,
|
this.encryptService,
|
||||||
this.pinService,
|
this.pinService,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.sdkService,
|
|
||||||
this.restrictedItemTypesService,
|
this.restrictedItemTypesService,
|
||||||
|
createSystemServiceProvider(
|
||||||
|
new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService),
|
||||||
|
this.stateProvider,
|
||||||
|
this.policyService,
|
||||||
|
buildExtensionRegistry(),
|
||||||
|
this.logService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.configService,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.individualVaultExportService = new IndividualVaultExportService(
|
this.individualVaultExportService = new IndividualVaultExportService(
|
||||||
|
|||||||
@@ -113,10 +113,13 @@ import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
|
|||||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||||
|
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
|
||||||
|
import { buildExtensionRegistry } from "@bitwarden/common/tools/extension/factory";
|
||||||
import {
|
import {
|
||||||
PasswordStrengthService,
|
PasswordStrengthService,
|
||||||
PasswordStrengthServiceAbstraction,
|
PasswordStrengthServiceAbstraction,
|
||||||
} from "@bitwarden/common/tools/password-strength";
|
} from "@bitwarden/common/tools/password-strength";
|
||||||
|
import { createSystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
||||||
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
||||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
|
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
|
||||||
@@ -816,8 +819,16 @@ export class ServiceContainer {
|
|||||||
this.encryptService,
|
this.encryptService,
|
||||||
this.pinService,
|
this.pinService,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.sdkService,
|
|
||||||
this.restrictedItemTypesService,
|
this.restrictedItemTypesService,
|
||||||
|
createSystemServiceProvider(
|
||||||
|
new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService),
|
||||||
|
this.stateProvider,
|
||||||
|
this.policyService,
|
||||||
|
buildExtensionRegistry(),
|
||||||
|
this.logService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.configService,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.individualExportService = new IndividualVaultExportService(
|
this.individualExportService = new IndividualVaultExportService(
|
||||||
|
|||||||
106
apps/desktop/desktop_native/Cargo.lock
generated
106
apps/desktop/desktop_native/Cargo.lock
generated
@@ -447,6 +447,32 @@ dependencies = [
|
|||||||
"tokio-util",
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitwarden_chromium_importer"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"aes-gcm",
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"base64",
|
||||||
|
"cbc",
|
||||||
|
"hex",
|
||||||
|
"homedir",
|
||||||
|
"log",
|
||||||
|
"oo7",
|
||||||
|
"pbkdf2",
|
||||||
|
"rand 0.9.1",
|
||||||
|
"rusqlite",
|
||||||
|
"security-framework",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha1",
|
||||||
|
"tokio",
|
||||||
|
"winapi",
|
||||||
|
"windows 0.61.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -586,9 +612,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.38"
|
version = "4.5.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
|
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -596,9 +622,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.38"
|
version = "4.5.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
|
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -608,9 +634,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.32"
|
version = "4.5.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -922,6 +948,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"autotype",
|
"autotype",
|
||||||
"base64",
|
"base64",
|
||||||
|
"bitwarden_chromium_importer",
|
||||||
"desktop_core",
|
"desktop_core",
|
||||||
"hex",
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
@@ -1165,6 +1192,18 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -1424,6 +1463,18 @@ name = "hashbrown"
|
|||||||
version = "0.15.3"
|
version = "0.15.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.15.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
@@ -1689,6 +1740,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.33.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "link-cplusplus"
|
name = "link-cplusplus"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
@@ -2642,6 +2704,20 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.35.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "russh-cryptovec"
|
name = "russh-cryptovec"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -2846,6 +2922,17 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.8"
|
version = "0.10.8"
|
||||||
@@ -3178,6 +3265,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
@@ -3496,6 +3584,12 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"autotype",
|
"autotype",
|
||||||
|
"bitwarden_chromium_importer",
|
||||||
"core",
|
"core",
|
||||||
"macos_provider",
|
"macos_provider",
|
||||||
"napi",
|
"napi",
|
||||||
@@ -21,7 +22,6 @@ anyhow = "=1.0.94"
|
|||||||
arboard = { version = "=3.6.0", default-features = false }
|
arboard = { version = "=3.6.0", default-features = false }
|
||||||
ashpd = "=0.11.0"
|
ashpd = "=0.11.0"
|
||||||
base64 = "=0.22.1"
|
base64 = "=0.22.1"
|
||||||
bindgen = "=0.72.0"
|
|
||||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }
|
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }
|
||||||
byteorder = "=1.5.0"
|
byteorder = "=1.5.0"
|
||||||
bytes = "=1.10.1"
|
bytes = "=1.10.1"
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[package]
|
||||||
|
name = "bitwarden_chromium_importer"
|
||||||
|
edition = { workspace = true }
|
||||||
|
license = { workspace = true }
|
||||||
|
version = { workspace = true }
|
||||||
|
publish = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aes = { workspace = true }
|
||||||
|
aes-gcm = "=0.10.3"
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
async-trait = "=0.1.88"
|
||||||
|
base64 = { workspace = true }
|
||||||
|
cbc = { workspace = true, features = ["alloc"] }
|
||||||
|
hex = { workspace = true }
|
||||||
|
homedir = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
pbkdf2 = "=0.12.2"
|
||||||
|
rand = { workspace = true }
|
||||||
|
rusqlite = { version = "=0.35.0", features = ["bundled"] }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
sha1 = "=0.10.6"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
security-framework = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
winapi = { version = "=0.3.9", features = ["dpapi", "memoryapi"] }
|
||||||
|
windows = { workspace = true, features = ["Win32_Security", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_Services", "Win32_System_Threading", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
oo7 = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Windows ABE Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Windows Application Bound Encryption (ABE) consists of three main components that work together:
|
||||||
|
|
||||||
|
- **client library** -- Library that is part of the desktop client application
|
||||||
|
- **admin.exe** -- Service launcher running as ADMINISTRATOR
|
||||||
|
- **service.exe** -- Background Windows service running as SYSTEM
|
||||||
|
|
||||||
|
_(The names of the binaries will be changed for the released product.)_
|
||||||
|
|
||||||
|
## The goal
|
||||||
|
|
||||||
|
The goal of this subsystem is to decrypt the master encryption key with which the login information
|
||||||
|
is encrypted on the local system in Windows. This applies to the most recent versions of Chrome and
|
||||||
|
Edge (untested yet) that are using the ABE/v20 encryption scheme for some of the local profiles.
|
||||||
|
|
||||||
|
The general idea of this encryption scheme is that Chrome generates a unique random encryption key,
|
||||||
|
then encrypts it at the user level with a fixed key. It then sends it to the Windows Data Protection
|
||||||
|
API at the user level, and then, using an installed service, encrypts it with the Windows Data
|
||||||
|
Protection API at the system level on top of that. This triply encrypted key is later stored in the
|
||||||
|
`Local State` file.
|
||||||
|
|
||||||
|
The next paragraphs describe what is done at each level to decrypt the key.
|
||||||
|
|
||||||
|
## 1. Client library
|
||||||
|
|
||||||
|
This is a Rust module that is part of the Chromium importer. It only compiles and runs on Windows
|
||||||
|
(see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges
|
||||||
|
by presenting the user with the UAC screen. See the `abe::decrypt_with_admin_and_service` invocation
|
||||||
|
in `windows.rs`.
|
||||||
|
|
||||||
|
This function takes three arguments:
|
||||||
|
|
||||||
|
1. Absolute path to `admin.exe`
|
||||||
|
2. Absolute path to `service.exe`
|
||||||
|
3. Base64 string of the ABE key extracted from the browser's local state
|
||||||
|
|
||||||
|
It's not possible to install the service from the user-level executable. So first, we have to
|
||||||
|
elevate the privileges and run `admin.exe` as ADMINISTRATOR. This is done by calling `ShellExecute`
|
||||||
|
with the `runas` verb. Since it's not trivial to read the standard output from an application
|
||||||
|
launched in this way, a named pipe server is created at the user level, which waits for the response
|
||||||
|
from `admin.exe` after it has been launched.
|
||||||
|
|
||||||
|
The name of the service executable and the data to be decrypted are passed via the command line to
|
||||||
|
`admin.exe` like this:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
admin.exe --service-exe "c:\temp\service.exe" --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..."
|
||||||
|
```
|
||||||
|
|
||||||
|
**At this point, the user must permit the action to be performed on the UAC screen.**
|
||||||
|
|
||||||
|
## 2. Admin executable
|
||||||
|
|
||||||
|
This executable receives the full path of `service.exe` and the data to be decrypted.
|
||||||
|
|
||||||
|
First, it installs the service to run as SYSTEM and waits for it to start running. The service
|
||||||
|
creates a named pipe server that the admin-level executable communicates with (see the `service.exe`
|
||||||
|
description further down).
|
||||||
|
|
||||||
|
It sends the base64 string to the pipe server in a raw message and waits for the answer. The answer
|
||||||
|
could be a success or a failure. In case of success, it's a base64 string decrypted at the system
|
||||||
|
level. In case of failure, it's an error message prefixed with an `!`. In either case, the response
|
||||||
|
is sent to the named pipe server created by the user. The user responds with `ok` (ignored).
|
||||||
|
|
||||||
|
After that, the executable stops and uninstalls the service and then exits.
|
||||||
|
|
||||||
|
## 3. System service
|
||||||
|
|
||||||
|
The service starts and creates a named pipe server for communication between `admin.exe` and the
|
||||||
|
system service. Please note that it is not possible to communicate between the user and the system
|
||||||
|
service directly via a named pipe. Thus, this three-layered approach is necessary.
|
||||||
|
|
||||||
|
Once the service is started, it waits for the incoming message via the named pipe. The expected
|
||||||
|
message is a base64 string to be decrypted. The data is decrypted via the Windows Data Protection
|
||||||
|
API `CryptUnprotectData` and sent back in response to this incoming message in base64 encoding. In
|
||||||
|
case of an error, the error message is sent back prefixed with an `!`.
|
||||||
|
|
||||||
|
The service keeps running and servicing more requests if there are any, until it's stopped and
|
||||||
|
removed from the system. Even though we send only one request, the service is designed to handle as
|
||||||
|
many clients with as many messages as needed and could be installed on the system permanently if
|
||||||
|
necessary.
|
||||||
|
|
||||||
|
## 4. Back to client library
|
||||||
|
|
||||||
|
The decrypted base64-encoded string comes back from the admin executable to the named pipe server at
|
||||||
|
the user level. At this point, it has been decrypted only once at the system level.
|
||||||
|
|
||||||
|
In the next step, the string is decrypted at the user level with the same Windows Data Protection
|
||||||
|
API.
|
||||||
|
|
||||||
|
And as the third step, it's decrypted with a hard-coded key found in the `elevation_service.exe`
|
||||||
|
from the Chrome installation. Based on the version of the encrypted string (encoded in the string
|
||||||
|
itself), it's either AES-256-GCM or ChaCha20Poly1305 encryption scheme. The details can be found in
|
||||||
|
`windows.rs`.
|
||||||
|
|
||||||
|
After all of these steps, we have the master key which can be used to decrypt the password
|
||||||
|
information stored in the local database.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The Windows ABE decryption process involves a three-tier architecture with named pipe communication:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client as Client Library (User)
|
||||||
|
participant Admin as admin.exe (Administrator)
|
||||||
|
participant Service as service.exe (System)
|
||||||
|
|
||||||
|
Client->>Client: Create named pipe server
|
||||||
|
Note over Client: \\.\pipe\BitwardenEncryptionService-admin-user
|
||||||
|
|
||||||
|
Client->>Admin: Launch with UAC elevation
|
||||||
|
Note over Client,Admin: --service-exe c:\path\to\service.exe
|
||||||
|
Note over Client,Admin: --encrypted QVBQQgEAAADQjJ3fARXRE...
|
||||||
|
|
||||||
|
Client->>Client: Wait for response
|
||||||
|
|
||||||
|
Admin->>Service: Install & start service
|
||||||
|
Note over Admin,Service: c:\path\to\service.exe
|
||||||
|
|
||||||
|
Service->>Service: Create named pipe server
|
||||||
|
Note over Service: \\.\pipe\BitwardenEncryptionService-service-admin
|
||||||
|
|
||||||
|
Service->>Service: Wait for message
|
||||||
|
|
||||||
|
Admin->>Service: Send encrypted data via admin-service pipe
|
||||||
|
Note over Admin,Service: QVBQQgEAAADQjJ3fARXRE...
|
||||||
|
|
||||||
|
Admin->>Admin: Wait for response
|
||||||
|
|
||||||
|
Service->>Service: Decrypt with system-level DPAPI
|
||||||
|
|
||||||
|
Service->>Admin: Return decrypted data via admin-service pipe
|
||||||
|
Note over Service,Admin: EjRWeXN0ZW0gU2VydmljZQ...
|
||||||
|
|
||||||
|
Admin->>Client: Send result via named user-admin pipe
|
||||||
|
Note over Client,Admin: EjRWeXN0ZW0gU2VydmljZQ...
|
||||||
|
|
||||||
|
Client->>Admin: Send ACK to admin
|
||||||
|
Note over Client,Admin: ok
|
||||||
|
|
||||||
|
Admin->>Service: Stop & uninstall service
|
||||||
|
Service-->>Admin: Exit
|
||||||
|
|
||||||
|
Admin-->>Client: Exit
|
||||||
|
|
||||||
|
Client->>Client: Decrypt with user-level DPAPI
|
||||||
|
|
||||||
|
Client->>Client: Decrypt with hardcoded key
|
||||||
|
Note over Client: AES-256-GCM or ChaCha20Poly1305
|
||||||
|
|
||||||
|
Client->>Client: Done
|
||||||
|
```
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use hex::decode;
|
||||||
|
use homedir::my_home;
|
||||||
|
use rusqlite::{params, Connection};
|
||||||
|
|
||||||
|
// Platform-specific code
|
||||||
|
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||||
|
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||||
|
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||||
|
mod platform;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Public API
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ProfileInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub folder: String,
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub account_name: Option<String>,
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub account_email: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Login {
|
||||||
|
pub url: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub note: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LoginImportFailure {
|
||||||
|
pub url: String,
|
||||||
|
pub username: String,
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LoginImportResult {
|
||||||
|
Success(Login),
|
||||||
|
Failure(LoginImportFailure),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make thus async
|
||||||
|
pub fn get_installed_browsers() -> Result<Vec<String>> {
|
||||||
|
let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len());
|
||||||
|
|
||||||
|
for (browser, config) in SUPPORTED_BROWSER_MAP.iter() {
|
||||||
|
let data_dir = get_browser_data_dir(config)?;
|
||||||
|
if data_dir.exists() {
|
||||||
|
browsers.push((*browser).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(browsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make thus async
|
||||||
|
pub fn get_available_profiles(browser_name: &String) -> Result<Vec<ProfileInfo>> {
|
||||||
|
let (_, local_state) = load_local_state_for_browser(browser_name)?;
|
||||||
|
Ok(get_profile_info(&local_state))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn import_logins(
|
||||||
|
browser_name: &String,
|
||||||
|
profile_id: &String,
|
||||||
|
) -> Result<Vec<LoginImportResult>> {
|
||||||
|
let (data_dir, local_state) = load_local_state_for_browser(browser_name)?;
|
||||||
|
|
||||||
|
let mut crypto_service = platform::get_crypto_service(browser_name, &local_state)
|
||||||
|
.map_err(|e| anyhow!("Failed to get crypto service: {}", e))?;
|
||||||
|
|
||||||
|
let local_logins = get_logins(&data_dir, profile_id, "Login Data")
|
||||||
|
.map_err(|e| anyhow!("Failed to query logins: {}", e))?;
|
||||||
|
|
||||||
|
// This is not available in all browsers, but there's no harm in trying. If the file doesn't exist we just get an empty vector.
|
||||||
|
let account_logins = get_logins(&data_dir, profile_id, "Login Data For Account")
|
||||||
|
.map_err(|e| anyhow!("Failed to query logins: {}", e))?;
|
||||||
|
|
||||||
|
// TODO: Do we need a better merge strategy? Maybe ignore duplicates at least?
|
||||||
|
// TODO: Should we also ignore an error from one of the two imports? If one is successful and the other fails,
|
||||||
|
// should we still return the successful ones? At the moment it doesn't fail for a missing file, only when
|
||||||
|
// something goes really wrong.
|
||||||
|
let all_logins = local_logins
|
||||||
|
.into_iter()
|
||||||
|
.chain(account_logins.into_iter())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let results = decrypt_logins(all_logins, &mut crypto_service).await;
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Private
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct BrowserConfig {
|
||||||
|
name: &'static str,
|
||||||
|
data_dir: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
static SUPPORTED_BROWSER_MAP: LazyLock<
|
||||||
|
std::collections::HashMap<&'static str, &'static BrowserConfig>,
|
||||||
|
> = LazyLock::new(|| {
|
||||||
|
platform::SUPPORTED_BROWSERS
|
||||||
|
.iter()
|
||||||
|
.map(|b| (b.name, b))
|
||||||
|
.collect::<std::collections::HashMap<_, _>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
fn get_browser_data_dir(config: &BrowserConfig) -> Result<PathBuf> {
|
||||||
|
let dir = my_home()
|
||||||
|
.map_err(|_| anyhow!("Home directory not found"))?
|
||||||
|
.ok_or_else(|| anyhow!("Home directory not found"))?
|
||||||
|
.join(config.data_dir);
|
||||||
|
Ok(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// CryptoService
|
||||||
|
//
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
trait CryptoService: Send {
|
||||||
|
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Clone)]
|
||||||
|
struct LocalState {
|
||||||
|
profile: AllProfiles,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
os_crypt: Option<OsCrypt>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Clone)]
|
||||||
|
struct AllProfiles {
|
||||||
|
info_cache: std::collections::HashMap<String, OneProfile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Clone)]
|
||||||
|
struct OneProfile {
|
||||||
|
name: String,
|
||||||
|
gaia_name: Option<String>,
|
||||||
|
user_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Clone)]
|
||||||
|
struct OsCrypt {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
encrypted_key: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
app_bound_encrypted_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_local_state_for_browser(browser_name: &String) -> Result<(PathBuf, LocalState)> {
|
||||||
|
let config = SUPPORTED_BROWSER_MAP
|
||||||
|
.get(browser_name.as_str())
|
||||||
|
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
|
||||||
|
|
||||||
|
let data_dir = get_browser_data_dir(config)?;
|
||||||
|
if !data_dir.exists() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Browser user data directory '{}' not found",
|
||||||
|
data_dir.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_state = load_local_state(&data_dir)?;
|
||||||
|
|
||||||
|
Ok((data_dir, local_state))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_local_state(browser_dir: &Path) -> Result<LocalState> {
|
||||||
|
let local_state = std::fs::read_to_string(browser_dir.join("Local State"))
|
||||||
|
.map_err(|e| anyhow!("Failed to read local state file: {}", e))?;
|
||||||
|
|
||||||
|
serde_json::from_str(&local_state)
|
||||||
|
.map_err(|e| anyhow!("Failed to parse local state JSON: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_profile_info(local_state: &LocalState) -> Vec<ProfileInfo> {
|
||||||
|
let mut profile_infos = Vec::new();
|
||||||
|
for (name, info) in local_state.profile.info_cache.iter() {
|
||||||
|
profile_infos.push(ProfileInfo {
|
||||||
|
name: info.name.clone(),
|
||||||
|
folder: name.clone(),
|
||||||
|
account_name: info.gaia_name.clone(),
|
||||||
|
account_email: info.user_name.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
profile_infos
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EncryptedLogin {
|
||||||
|
url: String,
|
||||||
|
username: String,
|
||||||
|
encrypted_password: Vec<u8>,
|
||||||
|
encrypted_note: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_logins(
|
||||||
|
browser_dir: &Path,
|
||||||
|
profile_id: &String,
|
||||||
|
filename: &str,
|
||||||
|
) -> Result<Vec<EncryptedLogin>> {
|
||||||
|
let login_data_path = browser_dir.join(profile_id).join(filename);
|
||||||
|
|
||||||
|
// Sometimes database files are not present, so nothing to import
|
||||||
|
if !login_data_path.exists() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the browser with the current profile is open the database file is locked.
|
||||||
|
// To access it we need to copy it to a temporary location.
|
||||||
|
let tmp_db_path = std::env::temp_dir().join(format!(
|
||||||
|
"tmp-logins-{}-{}.db",
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map_err(|e| anyhow!("Failed to retrieve system time: {}", e))?
|
||||||
|
.as_millis(),
|
||||||
|
rand::random::<u32>()
|
||||||
|
));
|
||||||
|
|
||||||
|
std::fs::copy(&login_data_path, &tmp_db_path).map_err(|e| {
|
||||||
|
anyhow!(
|
||||||
|
"Failed to copy the password database file at {:?}: {}",
|
||||||
|
login_data_path,
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let tmp_db_path = tmp_db_path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("Failed to locate database."))?;
|
||||||
|
let maybe_logins =
|
||||||
|
query_logins(tmp_db_path).map_err(|e| anyhow!("Failed to query logins: {}", e))?;
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
let _ = std::fs::remove_file(tmp_db_path);
|
||||||
|
|
||||||
|
Ok(maybe_logins)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_to_bytes(hex: &str) -> Vec<u8> {
|
||||||
|
decode(hex).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn does_table_exist(conn: &Connection, table_name: &str) -> Result<bool, rusqlite::Error> {
|
||||||
|
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")?;
|
||||||
|
let exists = stmt.exists(params![table_name])?;
|
||||||
|
Ok(exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_logins(db_path: &str) -> Result<Vec<EncryptedLogin>, rusqlite::Error> {
|
||||||
|
let conn = Connection::open(db_path)?;
|
||||||
|
|
||||||
|
let have_logins = does_table_exist(&conn, "logins")?;
|
||||||
|
let have_password_notes = does_table_exist(&conn, "password_notes")?;
|
||||||
|
if !have_logins || !have_password_notes {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
l.origin_url AS url,
|
||||||
|
l.username_value AS username,
|
||||||
|
hex(l.password_value) AS encryptedPasswordHex,
|
||||||
|
hex(pn.value) AS encryptedNoteHex
|
||||||
|
FROM
|
||||||
|
logins l
|
||||||
|
LEFT JOIN
|
||||||
|
password_notes pn ON l.id = pn.parent_id
|
||||||
|
WHERE
|
||||||
|
l.blacklisted_by_user = 0
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let logins_iter = stmt.query_map((), |row| {
|
||||||
|
let url: String = row.get("url")?;
|
||||||
|
let username: String = row.get("username")?;
|
||||||
|
let encrypted_password_hex: String = row.get("encryptedPasswordHex")?;
|
||||||
|
let encrypted_note_hex: String = row.get("encryptedNoteHex")?;
|
||||||
|
Ok(EncryptedLogin {
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
encrypted_password: hex_to_bytes(&encrypted_password_hex),
|
||||||
|
encrypted_note: hex_to_bytes(&encrypted_note_hex),
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut logins = Vec::new();
|
||||||
|
for login in logins_iter {
|
||||||
|
logins.push(login?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(logins)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn decrypt_logins(
|
||||||
|
encrypted_logins: Vec<EncryptedLogin>,
|
||||||
|
crypto_service: &mut Box<dyn CryptoService>,
|
||||||
|
) -> Vec<LoginImportResult> {
|
||||||
|
let mut results = Vec::with_capacity(encrypted_logins.len());
|
||||||
|
for encrypted_login in encrypted_logins {
|
||||||
|
let result = decrypt_login(encrypted_login, crypto_service).await;
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn decrypt_login(
|
||||||
|
encrypted_login: EncryptedLogin,
|
||||||
|
crypto_service: &mut Box<dyn CryptoService>,
|
||||||
|
) -> LoginImportResult {
|
||||||
|
let maybe_password = crypto_service
|
||||||
|
.decrypt_to_string(&encrypted_login.encrypted_password)
|
||||||
|
.await;
|
||||||
|
match maybe_password {
|
||||||
|
Ok(password) => {
|
||||||
|
let note = crypto_service
|
||||||
|
.decrypt_to_string(&encrypted_login.encrypted_note)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
LoginImportResult::Success(Login {
|
||||||
|
url: encrypted_login.url,
|
||||||
|
username: encrypted_login.username,
|
||||||
|
password,
|
||||||
|
note,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => LoginImportResult::Failure(LoginImportFailure {
|
||||||
|
url: encrypted_login.url,
|
||||||
|
username: encrypted_login.username,
|
||||||
|
error: e.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//! Cryptographic primitives used in the SDK
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
|
||||||
|
use aes::cipher::{
|
||||||
|
block_padding::Pkcs7, generic_array::GenericArray, typenum::U32, BlockDecryptMut, KeyIvInit,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn decrypt_aes256(iv: &[u8; 16], data: &[u8], key: GenericArray<u8, U32>) -> Result<Vec<u8>> {
|
||||||
|
let iv = GenericArray::from_slice(iv);
|
||||||
|
let mut data = data.to_vec();
|
||||||
|
return cbc::Decryptor::<aes::Aes256>::new(&key, iv)
|
||||||
|
.decrypt_padded_mut::<Pkcs7>(&mut data)
|
||||||
|
.map_err(|_| anyhow!("Failed to decrypt data"))?;
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod chromium;
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use oo7::XDG_SCHEMA_ATTRIBUTE;
|
||||||
|
|
||||||
|
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||||
|
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Public API
|
||||||
|
//
|
||||||
|
|
||||||
|
// TODO: It's possible that there might be multiple possible data directories, depending on the installation method (e.g., snap, flatpak, etc.).
|
||||||
|
pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Chrome",
|
||||||
|
data_dir: ".config/google-chrome",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Chromium",
|
||||||
|
data_dir: "snap/chromium/common/chromium",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Brave",
|
||||||
|
data_dir: "snap/brave/current/.config/BraveSoftware/Brave-Browser",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Opera",
|
||||||
|
data_dir: "snap/opera/current/.config/opera",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn get_crypto_service(
|
||||||
|
browser_name: &String,
|
||||||
|
_local_state: &LocalState,
|
||||||
|
) -> Result<Box<dyn CryptoService>> {
|
||||||
|
let config = KEYRING_CONFIG
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.browser == browser_name)
|
||||||
|
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
|
||||||
|
let service = LinuxCryptoService::new(config);
|
||||||
|
Ok(Box::new(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Private
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct KeyringConfig {
|
||||||
|
browser: &'static str,
|
||||||
|
application_id: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYRING_CONFIG: [KeyringConfig; SUPPORTED_BROWSERS.len()] = [
|
||||||
|
KeyringConfig {
|
||||||
|
browser: "Chrome",
|
||||||
|
application_id: "chrome",
|
||||||
|
},
|
||||||
|
KeyringConfig {
|
||||||
|
browser: "Chromium",
|
||||||
|
application_id: "chromium",
|
||||||
|
},
|
||||||
|
KeyringConfig {
|
||||||
|
browser: "Brave",
|
||||||
|
application_id: "brave",
|
||||||
|
},
|
||||||
|
KeyringConfig {
|
||||||
|
browser: "Opera",
|
||||||
|
application_id: "opera",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const IV: [u8; 16] = [0x20; 16];
|
||||||
|
const V10_KEY: [u8; 16] = [
|
||||||
|
0xfd, 0x62, 0x1f, 0xe5, 0xa2, 0xb4, 0x02, 0x53, 0x9d, 0xfa, 0x14, 0x7c, 0xa9, 0x27, 0x27, 0x78,
|
||||||
|
];
|
||||||
|
|
||||||
|
struct LinuxCryptoService {
|
||||||
|
config: &'static KeyringConfig,
|
||||||
|
v11_key: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxCryptoService {
|
||||||
|
fn new(config: &'static KeyringConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
v11_key: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_v10(&self, encrypted: &[u8]) -> Result<String> {
|
||||||
|
decrypt(&V10_KEY, encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn decrypt_v11(&mut self, encrypted: &[u8]) -> Result<String> {
|
||||||
|
if self.v11_key.is_none() {
|
||||||
|
let master_password = get_master_password(self.config.application_id).await?;
|
||||||
|
self.v11_key = Some(util::derive_saltysalt(&master_password, 1)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = self
|
||||||
|
.v11_key
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
|
||||||
|
decrypt(key, encrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CryptoService for LinuxCryptoService {
|
||||||
|
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String> {
|
||||||
|
let (version, password) =
|
||||||
|
util::split_encrypted_string_and_validate(encrypted, &["v10", "v11"])?;
|
||||||
|
|
||||||
|
let result = match version {
|
||||||
|
"v10" => self.decrypt_v10(password),
|
||||||
|
"v11" => self.decrypt_v11(password).await,
|
||||||
|
_ => Err(anyhow!("Logic error: unreachable code")),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt(key: &[u8], encrypted: &[u8]) -> Result<String> {
|
||||||
|
let plaintext = util::decrypt_aes_128_cbc(key, &IV, encrypted)?;
|
||||||
|
String::from_utf8(plaintext).map_err(|e| anyhow!("UTF-8 error: {:?}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_master_password(application_tag: &str) -> Result<Vec<u8>> {
|
||||||
|
let keyring = oo7::Keyring::new().await?;
|
||||||
|
keyring.unlock().await?;
|
||||||
|
|
||||||
|
let attributes = HashMap::from([
|
||||||
|
(
|
||||||
|
XDG_SCHEMA_ATTRIBUTE,
|
||||||
|
"chrome_libsecret_os_crypt_password_v2",
|
||||||
|
),
|
||||||
|
("application", application_tag),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let results = keyring.search_items(&attributes).await?;
|
||||||
|
match results.first() {
|
||||||
|
Some(r) => {
|
||||||
|
let secret = r.secret().await?;
|
||||||
|
Ok(secret.to_vec())
|
||||||
|
}
|
||||||
|
None => Err(anyhow!("The master password not found in the keyring")),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use security_framework::passwords::get_generic_password;
|
||||||
|
|
||||||
|
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||||
|
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Public API
|
||||||
|
//
|
||||||
|
|
||||||
|
pub const SUPPORTED_BROWSERS: [BrowserConfig; 7] = [
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Chrome",
|
||||||
|
data_dir: "Library/Application Support/Google/Chrome",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Chromium",
|
||||||
|
data_dir: "Library/Application Support/Chromium",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Microsoft Edge",
|
||||||
|
data_dir: "Library/Application Support/Microsoft Edge",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Brave",
|
||||||
|
data_dir: "Library/Application Support/BraveSoftware/Brave-Browser",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Arc",
|
||||||
|
data_dir: "Library/Application Support/Arc/User Data",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Opera",
|
||||||
|
data_dir: "Library/Application Support/com.operasoftware.Opera",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Vivaldi",
|
||||||
|
data_dir: "Library/Application Support/Vivaldi",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn get_crypto_service(
|
||||||
|
browser_name: &String,
|
||||||
|
_local_state: &LocalState,
|
||||||
|
) -> Result<Box<dyn CryptoService>> {
|
||||||
|
let config = KEYCHAIN_CONFIG
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.browser == browser_name)
|
||||||
|
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
|
||||||
|
|
||||||
|
Ok(Box::new(MacCryptoService::new(config)))
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Private
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct KeychainConfig {
|
||||||
|
browser: &'static str,
|
||||||
|
service: &'static str,
|
||||||
|
account: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYCHAIN_CONFIG: [KeychainConfig; SUPPORTED_BROWSERS.len()] = [
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Chrome",
|
||||||
|
service: "Chrome Safe Storage",
|
||||||
|
account: "Chrome",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Chromium",
|
||||||
|
service: "Chromium Safe Storage",
|
||||||
|
account: "Chromium",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Microsoft Edge",
|
||||||
|
service: "Microsoft Edge Safe Storage",
|
||||||
|
account: "Microsoft Edge",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Brave",
|
||||||
|
service: "Brave Safe Storage",
|
||||||
|
account: "Brave",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Arc",
|
||||||
|
service: "Arc Safe Storage",
|
||||||
|
account: "Arc",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Opera",
|
||||||
|
service: "Opera Safe Storage",
|
||||||
|
account: "Opera",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Vivaldi",
|
||||||
|
service: "Vivaldi Safe Storage",
|
||||||
|
account: "Vivaldi",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const IV: [u8; 16] = [0x20; 16]; // 16 bytes of 0x20 (space character)
|
||||||
|
|
||||||
|
//
|
||||||
|
// CryptoService
|
||||||
|
//
|
||||||
|
|
||||||
|
struct MacCryptoService {
|
||||||
|
config: &'static KeychainConfig,
|
||||||
|
master_key: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MacCryptoService {
|
||||||
|
fn new(config: &'static KeychainConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
master_key: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CryptoService for MacCryptoService {
|
||||||
|
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String> {
|
||||||
|
if encrypted.is_empty() {
|
||||||
|
return Ok(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// On macOS only v10 is supported
|
||||||
|
let (_, no_prefix) = util::split_encrypted_string_and_validate(encrypted, &["v10"])?;
|
||||||
|
|
||||||
|
// This might bring up the admin password prompt
|
||||||
|
if self.master_key.is_none() {
|
||||||
|
self.master_key = Some(get_master_key(self.config.service, self.config.account)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = self
|
||||||
|
.master_key
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
|
||||||
|
let plaintext = util::decrypt_aes_128_cbc(key, &IV, no_prefix)
|
||||||
|
.map_err(|e| anyhow!("Failed to decrypt: {}", e))?;
|
||||||
|
let plaintext =
|
||||||
|
String::from_utf8(plaintext).map_err(|e| anyhow!("Invalid UTF-8: {}", e))?;
|
||||||
|
|
||||||
|
Ok(plaintext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_master_key(service: &str, account: &str) -> Result<Vec<u8>> {
|
||||||
|
let master_password = get_master_password(service, account)?;
|
||||||
|
let key = util::derive_saltysalt(&master_password, 1003)?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_master_password(service: &str, account: &str) -> Result<Vec<u8>> {
|
||||||
|
let password = get_generic_password(service, account)
|
||||||
|
.map_err(|e| anyhow!("Failed to get password from keychain: {}", e))?;
|
||||||
|
|
||||||
|
Ok(password)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use pbkdf2::{hmac::Hmac, pbkdf2};
|
||||||
|
use sha1::Sha1;
|
||||||
|
|
||||||
|
pub fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> {
|
||||||
|
if encrypted.len() < 3 {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Corrupted entry: invalid encrypted string length, expected at least 3 bytes, got {}",
|
||||||
|
encrypted.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (version, password) = encrypted.split_at(3);
|
||||||
|
Ok((std::str::from_utf8(version)?, password))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn split_encrypted_string_and_validate<'a>(
|
||||||
|
encrypted: &'a [u8],
|
||||||
|
supported_versions: &[&str],
|
||||||
|
) -> Result<(&'a str, &'a [u8])> {
|
||||||
|
let (version, password) = split_encrypted_string(encrypted)?;
|
||||||
|
if !supported_versions.contains(&version) {
|
||||||
|
return Err(anyhow!("Unsupported encryption version: {}", version));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((version, password))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let decryptor = cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv)?;
|
||||||
|
let plaintext = decryptor
|
||||||
|
.decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
|
||||||
|
.map_err(|e| anyhow!("Failed to decrypt: {}", e))?;
|
||||||
|
Ok(plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
|
||||||
|
let mut key = vec![0u8; 16];
|
||||||
|
pbkdf2::<Hmac<Sha1>>(password, b"saltysalt", iterations, &mut key)
|
||||||
|
.map_err(|e| anyhow!("Failed to derive master key: {}", e))?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
use aes_gcm::aead::Aead;
|
||||||
|
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||||
|
use winapi::shared::minwindef::{BOOL, BYTE, DWORD};
|
||||||
|
use winapi::um::{dpapi::CryptUnprotectData, wincrypt::DATA_BLOB};
|
||||||
|
use windows::Win32::Foundation::{LocalFree, HLOCAL};
|
||||||
|
|
||||||
|
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Public API
|
||||||
|
//
|
||||||
|
|
||||||
|
pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Chrome",
|
||||||
|
data_dir: "AppData/Local/Google/Chrome/User Data",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Chromium",
|
||||||
|
data_dir: "AppData/Local/Chromium/User Data",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Microsoft Edge",
|
||||||
|
data_dir: "AppData/Local/Microsoft/Edge/User Data",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Brave",
|
||||||
|
data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Opera",
|
||||||
|
data_dir: "AppData/Roaming/Opera Software/Opera Stable",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Vivaldi",
|
||||||
|
data_dir: "AppData/Local/Vivaldi/User Data",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn get_crypto_service(
|
||||||
|
_browser_name: &str,
|
||||||
|
local_state: &LocalState,
|
||||||
|
) -> Result<Box<dyn CryptoService>> {
|
||||||
|
Ok(Box::new(WindowsCryptoService::new(local_state)))
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// CryptoService
|
||||||
|
//
|
||||||
|
struct WindowsCryptoService {
|
||||||
|
master_key: Option<Vec<u8>>,
|
||||||
|
encrypted_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowsCryptoService {
|
||||||
|
pub(crate) fn new(local_state: &LocalState) -> Self {
|
||||||
|
Self {
|
||||||
|
master_key: None,
|
||||||
|
encrypted_key: local_state
|
||||||
|
.os_crypt
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.encrypted_key.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CryptoService for WindowsCryptoService {
|
||||||
|
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String> {
|
||||||
|
if encrypted.is_empty() {
|
||||||
|
return Ok(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows only v10 and v20 are supported at the moment
|
||||||
|
let (version, no_prefix) =
|
||||||
|
util::split_encrypted_string_and_validate(encrypted, &["v10", "v20"])?;
|
||||||
|
|
||||||
|
// v10 is already stripped; Windows Chrome uses AES-GCM: [12 bytes IV][ciphertext][16 bytes auth tag]
|
||||||
|
const IV_SIZE: usize = 12;
|
||||||
|
const TAG_SIZE: usize = 16;
|
||||||
|
const MIN_LENGTH: usize = IV_SIZE + TAG_SIZE;
|
||||||
|
|
||||||
|
if no_prefix.len() < MIN_LENGTH {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Corrupted entry: expected at least {} bytes, got {} bytes",
|
||||||
|
MIN_LENGTH,
|
||||||
|
no_prefix.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow empty passwords
|
||||||
|
if no_prefix.len() == MIN_LENGTH {
|
||||||
|
return Ok(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.master_key.is_none() {
|
||||||
|
self.master_key = Some(self.get_master_key(version)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = self
|
||||||
|
.master_key
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
|
||||||
|
let key = Key::<Aes256Gcm>::from_slice(key);
|
||||||
|
let cipher = Aes256Gcm::new(key);
|
||||||
|
let nonce = Nonce::from_slice(&no_prefix[..IV_SIZE]);
|
||||||
|
|
||||||
|
let decrypted_bytes = cipher
|
||||||
|
.decrypt(nonce, no_prefix[IV_SIZE..].as_ref())
|
||||||
|
.map_err(|e| anyhow!("Decryption failed: {}", e))?;
|
||||||
|
|
||||||
|
let plaintext = String::from_utf8(decrypted_bytes)
|
||||||
|
.map_err(|e| anyhow!("Failed to convert decrypted data to UTF-8: {}", e))?;
|
||||||
|
|
||||||
|
Ok(plaintext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowsCryptoService {
|
||||||
|
fn get_master_key(&mut self, version: &str) -> Result<Vec<u8>> {
|
||||||
|
match version {
|
||||||
|
"v10" => self.get_master_key_v10(),
|
||||||
|
_ => Err(anyhow!("Unsupported version: {}", version)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_master_key_v10(&mut self) -> Result<Vec<u8>> {
|
||||||
|
if self.encrypted_key.is_none() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Encrypted master key is not found in the local browser state"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = self
|
||||||
|
.encrypted_key
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
|
||||||
|
let key_bytes = BASE64_STANDARD
|
||||||
|
.decode(key)
|
||||||
|
.map_err(|e| anyhow!("Encrypted master key is not a valid base64 string: {}", e))?;
|
||||||
|
|
||||||
|
if key_bytes.len() <= 5 || &key_bytes[..5] != b"DPAPI" {
|
||||||
|
return Err(anyhow!("Encrypted master key is not encrypted with DPAPI"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = unprotect_data_win(&key_bytes[5..])
|
||||||
|
.map_err(|e| anyhow!("Failed to unprotect the master key: {}", e))?;
|
||||||
|
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unprotect_data_win(data: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data_in = DATA_BLOB {
|
||||||
|
cbData: data.len() as DWORD,
|
||||||
|
pbData: data.as_ptr() as *mut BYTE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut data_out = DATA_BLOB {
|
||||||
|
cbData: 0,
|
||||||
|
pbData: std::ptr::null_mut(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result: BOOL = unsafe {
|
||||||
|
// BOOL from winapi (i32)
|
||||||
|
CryptUnprotectData(
|
||||||
|
&mut data_in,
|
||||||
|
std::ptr::null_mut(), // ppszDataDescr: *mut LPWSTR (*mut *mut u16)
|
||||||
|
std::ptr::null_mut(), // pOptionalEntropy: *mut DATA_BLOB
|
||||||
|
std::ptr::null_mut(), // pvReserved: LPVOID (*mut c_void)
|
||||||
|
std::ptr::null_mut(), // pPromptStruct: *mut CRYPTPROTECT_PROMPTSTRUCT
|
||||||
|
0, // dwFlags: DWORD
|
||||||
|
&mut data_out,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result == 0 {
|
||||||
|
return Err(anyhow!("CryptUnprotectData failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if data_out.pbData.is_null() || data_out.cbData == 0 {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_slice =
|
||||||
|
unsafe { std::slice::from_raw_parts(data_out.pbData, data_out.cbData as usize) };
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
if !data_out.pbData.is_null() {
|
||||||
|
LocalFree(Some(HLOCAL(data_out.pbData as *mut std::ffi::c_void)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output_slice.to_vec())
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ function buildProxyBin(target, release = true) {
|
|||||||
const targetArg = target ? `--target ${target}` : "";
|
const targetArg = target ? `--target ${target}` : "";
|
||||||
const releaseArg = release ? "--release" : "";
|
const releaseArg = release ? "--release" : "";
|
||||||
child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")});
|
child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")});
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
// Copy the resulting binary to the dist folder
|
// Copy the resulting binary to the dist folder
|
||||||
const targetFolder = release ? "release" : "debug";
|
const targetFolder = release ? "release" : "debug";
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ manual_test = []
|
|||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
autotype = { path = "../autotype" }
|
autotype = { path = "../autotype" }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
|
bitwarden_chromium_importer = { path = "../bitwarden_chromium_importer" }
|
||||||
desktop_core = { path = "../core" }
|
desktop_core = { path = "../core" }
|
||||||
hex = { workspace = true }
|
hex = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
|
|||||||
24
apps/desktop/desktop_native/napi/index.d.ts
vendored
24
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -208,6 +208,30 @@ export declare namespace logging {
|
|||||||
}
|
}
|
||||||
export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void
|
export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void
|
||||||
}
|
}
|
||||||
|
export declare namespace chromium_importer {
|
||||||
|
export interface ProfileInfo {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
export interface Login {
|
||||||
|
url: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
note: string
|
||||||
|
}
|
||||||
|
export interface LoginImportFailure {
|
||||||
|
url: string
|
||||||
|
username: string
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
export interface LoginImportResult {
|
||||||
|
login?: Login
|
||||||
|
failure?: LoginImportFailure
|
||||||
|
}
|
||||||
|
export function getInstalledBrowsers(): Promise<Array<string>>
|
||||||
|
export function getAvailableProfiles(browser: string): Promise<Array<ProfileInfo>>
|
||||||
|
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||||
|
}
|
||||||
export declare namespace autotype {
|
export declare namespace autotype {
|
||||||
export function getForegroundWindowTitle(): string
|
export function getForegroundWindowTitle(): string
|
||||||
export function typeInput(input: Array<number>): void
|
export function typeInput(input: Array<number>): void
|
||||||
|
|||||||
@@ -879,6 +879,96 @@ pub mod logging {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub mod chromium_importer {
|
||||||
|
use bitwarden_chromium_importer::chromium::LoginImportResult as _LoginImportResult;
|
||||||
|
use bitwarden_chromium_importer::chromium::ProfileInfo as _ProfileInfo;
|
||||||
|
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct ProfileInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct Login {
|
||||||
|
pub url: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub note: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct LoginImportFailure {
|
||||||
|
pub url: String,
|
||||||
|
pub username: String,
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct LoginImportResult {
|
||||||
|
pub login: Option<Login>,
|
||||||
|
pub failure: Option<LoginImportFailure>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<_LoginImportResult> for LoginImportResult {
|
||||||
|
fn from(l: _LoginImportResult) -> Self {
|
||||||
|
match l {
|
||||||
|
_LoginImportResult::Success(l) => LoginImportResult {
|
||||||
|
login: Some(Login {
|
||||||
|
url: l.url,
|
||||||
|
username: l.username,
|
||||||
|
password: l.password,
|
||||||
|
note: l.note,
|
||||||
|
}),
|
||||||
|
failure: None,
|
||||||
|
},
|
||||||
|
_LoginImportResult::Failure(l) => LoginImportResult {
|
||||||
|
login: None,
|
||||||
|
failure: Some(LoginImportFailure {
|
||||||
|
url: l.url,
|
||||||
|
username: l.username,
|
||||||
|
error: l.error,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<_ProfileInfo> for ProfileInfo {
|
||||||
|
fn from(p: _ProfileInfo) -> Self {
|
||||||
|
ProfileInfo {
|
||||||
|
id: p.folder,
|
||||||
|
name: p.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_installed_browsers() -> napi::Result<Vec<String>> {
|
||||||
|
bitwarden_chromium_importer::chromium::get_installed_browsers()
|
||||||
|
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_available_profiles(browser: String) -> napi::Result<Vec<ProfileInfo>> {
|
||||||
|
bitwarden_chromium_importer::chromium::get_available_profiles(&browser)
|
||||||
|
.map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect())
|
||||||
|
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub async fn import_logins(
|
||||||
|
browser: String,
|
||||||
|
profile_id: String,
|
||||||
|
) -> napi::Result<Vec<LoginImportResult>> {
|
||||||
|
bitwarden_chromium_importer::chromium::import_logins(&browser, &profile_id)
|
||||||
|
.await
|
||||||
|
.map(|logins| logins.into_iter().map(LoginImportResult::from).collect())
|
||||||
|
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub mod autotype {
|
pub mod autotype {
|
||||||
#[napi]
|
#[napi]
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
|
import { chromium_importer } from "@bitwarden/desktop-napi";
|
||||||
|
|
||||||
|
export class ChromiumImporterService {
|
||||||
|
constructor() {
|
||||||
|
ipcMain.handle("chromium_importer.getInstalledBrowsers", async (event) => {
|
||||||
|
return await chromium_importer.getInstalledBrowsers();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("chromium_importer.getAvailableProfiles", async (event, browser: string) => {
|
||||||
|
return await chromium_importer.getAvailableProfiles(browser);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
"chromium_importer.importLogins",
|
||||||
|
async (event, browser: string, profileId: string) => {
|
||||||
|
return await chromium_importer.importLogins(browser, profileId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
(formLoading)="this.loading = $event"
|
(formLoading)="this.loading = $event"
|
||||||
(formDisabled)="this.disabled = $event"
|
(formDisabled)="this.disabled = $event"
|
||||||
(onSuccessfulImport)="this.onSuccessfulImport($event)"
|
(onSuccessfulImport)="this.onSuccessfulImport($event)"
|
||||||
|
[onImportFromBrowser]="this.onImportFromBrowser"
|
||||||
|
[onLoadProfilesFromBrowser]="this.onLoadProfilesFromBrowser"
|
||||||
></tools-import>
|
></tools-import>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container bitDialogFooter>
|
<ng-container bitDialogFooter>
|
||||||
|
|||||||
@@ -28,4 +28,12 @@ export class ImportDesktopComponent {
|
|||||||
protected async onSuccessfulImport(organizationId: string): Promise<void> {
|
protected async onSuccessfulImport(organizationId: string): Promise<void> {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected onLoadProfilesFromBrowser(browser: string): Promise<any[]> {
|
||||||
|
return ipc.tools.chromiumImporter.getAvailableProfiles(browser);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onImportFromBrowser(browser: string, profile: string): Promise<any[]> {
|
||||||
|
return ipc.tools.chromiumImporter.importLogins(browser, profile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
apps/desktop/src/app/tools/preload.ts
Normal file
14
apps/desktop/src/app/tools/preload.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
const chromiumImporter = {
|
||||||
|
getInstalledBrowsers: (): Promise<string[]> =>
|
||||||
|
ipcRenderer.invoke("chromium_importer.getInstalledBrowsers"),
|
||||||
|
getAvailableProfiles: (browser: string): Promise<any[]> =>
|
||||||
|
ipcRenderer.invoke("chromium_importer.getAvailableProfiles", browser),
|
||||||
|
importLogins: (browser: string, profileId: string): Promise<any[]> =>
|
||||||
|
ipcRenderer.invoke("chromium_importer.importLogins", browser, profileId),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
chromiumImporter,
|
||||||
|
};
|
||||||
@@ -3588,6 +3588,12 @@
|
|||||||
"awaitingSSODesc": {
|
"awaitingSSODesc": {
|
||||||
"message": "Please continue to log in using your company credentials."
|
"message": "Please continue to log in using your company credentials."
|
||||||
},
|
},
|
||||||
|
"importDirectlyFromBrowser": {
|
||||||
|
"message": "Import directly from browser"
|
||||||
|
},
|
||||||
|
"browserProfile": {
|
||||||
|
"message": "Browser Profile"
|
||||||
|
},
|
||||||
"seeDetailedInstructions": {
|
"seeDetailedInstructions": {
|
||||||
"message": "See detailed instructions on our help site at",
|
"message": "See detailed instructions on our help site at",
|
||||||
"description": "This is followed a by a hyperlink to the help website."
|
"description": "This is followed a by a hyperlink to the help website."
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
} from "@bitwarden/state-internal";
|
} from "@bitwarden/state-internal";
|
||||||
import { SerializedMemoryStorageService, StorageServiceProvider } from "@bitwarden/storage-core";
|
import { SerializedMemoryStorageService, StorageServiceProvider } from "@bitwarden/storage-core";
|
||||||
|
|
||||||
|
import { ChromiumImporterService } from "./app/tools/import/chromium-importer.service";
|
||||||
import { MainDesktopAutotypeService } from "./autofill/main/main-desktop-autotype.service";
|
import { MainDesktopAutotypeService } from "./autofill/main/main-desktop-autotype.service";
|
||||||
import { MainSshAgentService } from "./autofill/main/main-ssh-agent.service";
|
import { MainSshAgentService } from "./autofill/main/main-ssh-agent.service";
|
||||||
import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service";
|
import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service";
|
||||||
@@ -300,6 +301,8 @@ export class Main {
|
|||||||
this.ssoUrlService,
|
this.ssoUrlService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
new ChromiumImporterService();
|
||||||
|
|
||||||
this.nativeAutofillMain = new NativeAutofillMain(this.logService, this.windowMain);
|
this.nativeAutofillMain = new NativeAutofillMain(this.logService, this.windowMain);
|
||||||
void this.nativeAutofillMain.init();
|
void this.nativeAutofillMain.init();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { contextBridge } from "electron";
|
import { contextBridge } from "electron";
|
||||||
|
|
||||||
|
import tools from "./app/tools/preload";
|
||||||
import auth from "./auth/preload";
|
import auth from "./auth/preload";
|
||||||
import autofill from "./autofill/preload";
|
import autofill from "./autofill/preload";
|
||||||
import keyManagement from "./key-management/preload";
|
import keyManagement from "./key-management/preload";
|
||||||
@@ -21,6 +22,7 @@ export const ipc = {
|
|||||||
autofill,
|
autofill,
|
||||||
platform,
|
platform,
|
||||||
keyManagement,
|
keyManagement,
|
||||||
|
tools,
|
||||||
};
|
};
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("ipc", ipc);
|
contextBridge.exposeInMainWorld("ipc", ipc);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export enum FeatureFlag {
|
|||||||
/* Tools */
|
/* Tools */
|
||||||
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
||||||
UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators",
|
UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators",
|
||||||
|
UseChromiumImporter = "pm-23982-chromium-importer",
|
||||||
|
|
||||||
/* DIRT */
|
/* DIRT */
|
||||||
EventBasedOrganizationIntegrations = "event-based-organization-integrations",
|
EventBasedOrganizationIntegrations = "event-based-organization-integrations",
|
||||||
@@ -79,6 +80,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
/* Tools */
|
/* Tools */
|
||||||
[FeatureFlag.DesktopSendUIRefresh]: FALSE,
|
[FeatureFlag.DesktopSendUIRefresh]: FALSE,
|
||||||
[FeatureFlag.UseSdkPasswordGenerators]: FALSE,
|
[FeatureFlag.UseSdkPasswordGenerators]: FALSE,
|
||||||
|
[FeatureFlag.UseChromiumImporter]: FALSE,
|
||||||
|
|
||||||
/* DIRT */
|
/* DIRT */
|
||||||
[FeatureFlag.EventBasedOrganizationIntegrations]: FALSE,
|
[FeatureFlag.EventBasedOrganizationIntegrations]: FALSE,
|
||||||
|
|||||||
178
libs/common/src/tools/providers.spec.ts
Normal file
178
libs/common/src/tools/providers.spec.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { PolicyService } from "../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { ConfigService } from "../platform/abstractions/config/config.service";
|
||||||
|
import { LogService } from "../platform/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
|
||||||
|
import { StateProvider } from "../platform/state";
|
||||||
|
|
||||||
|
import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider";
|
||||||
|
import { ExtensionRegistry } from "./extension/extension-registry.abstraction";
|
||||||
|
import { ExtensionService } from "./extension/extension.service";
|
||||||
|
import { disabledSemanticLoggerProvider } from "./log";
|
||||||
|
import { createSystemServiceProvider } from "./providers";
|
||||||
|
|
||||||
|
describe("SystemServiceProvider", () => {
|
||||||
|
let mockEncryptor: LegacyEncryptorProvider;
|
||||||
|
let mockState: StateProvider;
|
||||||
|
let mockPolicy: PolicyService;
|
||||||
|
let mockRegistry: ExtensionRegistry;
|
||||||
|
let mockLogger: LogService;
|
||||||
|
let mockEnvironment: MockProxy<PlatformUtilsService>;
|
||||||
|
let mockConfigService: ConfigService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
mockEncryptor = mock<LegacyEncryptorProvider>();
|
||||||
|
mockState = mock<StateProvider>();
|
||||||
|
mockPolicy = mock<PolicyService>();
|
||||||
|
mockRegistry = mock<ExtensionRegistry>();
|
||||||
|
mockLogger = mock<LogService>();
|
||||||
|
mockEnvironment = mock<PlatformUtilsService>();
|
||||||
|
mockConfigService = mock<ConfigService>();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createSystemServiceProvider", () => {
|
||||||
|
it("returns object with all required services when called with valid parameters", () => {
|
||||||
|
mockEnvironment.isDev.mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = createSystemServiceProvider(
|
||||||
|
mockEncryptor,
|
||||||
|
mockState,
|
||||||
|
mockPolicy,
|
||||||
|
mockRegistry,
|
||||||
|
mockLogger,
|
||||||
|
mockEnvironment,
|
||||||
|
mockConfigService,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toHaveProperty("policy", mockPolicy);
|
||||||
|
expect(result).toHaveProperty("extension");
|
||||||
|
expect(result).toHaveProperty("log");
|
||||||
|
expect(result).toHaveProperty("configService", mockConfigService);
|
||||||
|
expect(result).toHaveProperty("environment", mockEnvironment);
|
||||||
|
expect(result.extension).toBeInstanceOf(ExtensionService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates ExtensionService with correct dependencies when called", () => {
|
||||||
|
mockEnvironment.isDev.mockReturnValue(true);
|
||||||
|
|
||||||
|
const result = createSystemServiceProvider(
|
||||||
|
mockEncryptor,
|
||||||
|
mockState,
|
||||||
|
mockPolicy,
|
||||||
|
mockRegistry,
|
||||||
|
mockLogger,
|
||||||
|
mockEnvironment,
|
||||||
|
mockConfigService,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.extension).toBeInstanceOf(ExtensionService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given development environment", () => {
|
||||||
|
it("uses enableLogForTypes when environment.isDev() returns true", () => {
|
||||||
|
mockEnvironment.isDev.mockReturnValue(true);
|
||||||
|
|
||||||
|
const result = createSystemServiceProvider(
|
||||||
|
mockEncryptor,
|
||||||
|
mockState,
|
||||||
|
mockPolicy,
|
||||||
|
mockRegistry,
|
||||||
|
mockLogger,
|
||||||
|
mockEnvironment,
|
||||||
|
mockConfigService,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockEnvironment.isDev).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.log).not.toBe(disabledSemanticLoggerProvider);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given production environment", () => {
|
||||||
|
it("uses disabledSemanticLoggerProvider when environment.isDev() returns false", () => {
|
||||||
|
mockEnvironment.isDev.mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = createSystemServiceProvider(
|
||||||
|
mockEncryptor,
|
||||||
|
mockState,
|
||||||
|
mockPolicy,
|
||||||
|
mockRegistry,
|
||||||
|
mockLogger,
|
||||||
|
mockEnvironment,
|
||||||
|
mockConfigService,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockEnvironment.isDev).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.log).toBe(disabledSemanticLoggerProvider);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("configures ExtensionService with encryptor, state, log provider, and now function when called", () => {
|
||||||
|
mockEnvironment.isDev.mockReturnValue(false);
|
||||||
|
const dateSpy = jest.spyOn(Date, "now");
|
||||||
|
|
||||||
|
const result = createSystemServiceProvider(
|
||||||
|
mockEncryptor,
|
||||||
|
mockState,
|
||||||
|
mockPolicy,
|
||||||
|
mockRegistry,
|
||||||
|
mockLogger,
|
||||||
|
mockEnvironment,
|
||||||
|
mockConfigService,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.extension).toBeInstanceOf(ExtensionService);
|
||||||
|
expect(dateSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through policy service correctly when called", () => {
|
||||||
|
mockEnvironment.isDev.mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = createSystemServiceProvider(
|
||||||
|
mockEncryptor,
|
||||||
|
mockState,
|
||||||
|
mockPolicy,
|
||||||
|
mockRegistry,
|
||||||
|
mockLogger,
|
||||||
|
mockEnvironment,
|
||||||
|
mockConfigService,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.policy).toBe(mockPolicy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through configService correctly when called", () => {
|
||||||
|
mockEnvironment.isDev.mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = createSystemServiceProvider(
|
||||||
|
mockEncryptor,
|
||||||
|
mockState,
|
||||||
|
mockPolicy,
|
||||||
|
mockRegistry,
|
||||||
|
mockLogger,
|
||||||
|
mockEnvironment,
|
||||||
|
mockConfigService,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.configService).toBe(mockConfigService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through environment service correctly when called", () => {
|
||||||
|
mockEnvironment.isDev.mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = createSystemServiceProvider(
|
||||||
|
mockEncryptor,
|
||||||
|
mockState,
|
||||||
|
mockPolicy,
|
||||||
|
mockRegistry,
|
||||||
|
mockLogger,
|
||||||
|
mockEnvironment,
|
||||||
|
mockConfigService,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.environment).toBe(mockEnvironment);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import { LogService } from "@bitwarden/logging";
|
||||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||||
|
import { StateProvider } from "@bitwarden/state";
|
||||||
|
|
||||||
import { PolicyService } from "../admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { ConfigService } from "../platform/abstractions/config/config.service";
|
import { ConfigService } from "../platform/abstractions/config/config.service";
|
||||||
|
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
|
import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider";
|
||||||
|
import { ExtensionRegistry } from "./extension/extension-registry.abstraction";
|
||||||
import { ExtensionService } from "./extension/extension.service";
|
import { ExtensionService } from "./extension/extension.service";
|
||||||
import { LogProvider } from "./log";
|
import { disabledSemanticLoggerProvider, enableLogForTypes, LogProvider } from "./log";
|
||||||
|
|
||||||
/** Provides access to commonly-used cross-cutting services. */
|
/** Provides access to commonly-used cross-cutting services. */
|
||||||
export type SystemServiceProvider = {
|
export type SystemServiceProvider = {
|
||||||
@@ -20,6 +25,42 @@ export type SystemServiceProvider = {
|
|||||||
/** Config Service to determine flag features */
|
/** Config Service to determine flag features */
|
||||||
readonly configService: ConfigService;
|
readonly configService: ConfigService;
|
||||||
|
|
||||||
|
/** Platform Service to inspect runtime environment */
|
||||||
|
readonly environment: PlatformUtilsService;
|
||||||
|
|
||||||
/** SDK Service */
|
/** SDK Service */
|
||||||
readonly sdk: BitwardenClient;
|
readonly sdk?: BitwardenClient;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Constructs a system service provider. */
|
||||||
|
export function createSystemServiceProvider(
|
||||||
|
encryptor: LegacyEncryptorProvider,
|
||||||
|
state: StateProvider,
|
||||||
|
policy: PolicyService,
|
||||||
|
registry: ExtensionRegistry,
|
||||||
|
logger: LogService,
|
||||||
|
environment: PlatformUtilsService,
|
||||||
|
configService: ConfigService,
|
||||||
|
): SystemServiceProvider {
|
||||||
|
let log: LogProvider;
|
||||||
|
if (environment.isDev()) {
|
||||||
|
log = enableLogForTypes(logger, []);
|
||||||
|
} else {
|
||||||
|
log = disabledSemanticLoggerProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = new ExtensionService(registry, {
|
||||||
|
encryptor,
|
||||||
|
state,
|
||||||
|
log,
|
||||||
|
now: Date.now,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
policy,
|
||||||
|
extension,
|
||||||
|
log,
|
||||||
|
configService,
|
||||||
|
environment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
167
libs/importer/src/components/chrome/import-chrome.component.ts
Normal file
167
libs/importer/src/components/chrome/import-chrome.component.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/importer/src/components/chrome/index.ts
Normal file
1
libs/importer/src/components/chrome/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ImportChromeComponent } from "./import-chrome.component";
|
||||||
@@ -169,27 +169,41 @@
|
|||||||
"Export" to save the JSON file.
|
"Export" to save the JSON file.
|
||||||
</ng-container>
|
</ng-container>
|
||||||
-->
|
-->
|
||||||
<ng-container
|
<ng-container *ngIf="showChromiumInstructions$ | async">
|
||||||
*ngIf="
|
|
||||||
format === 'chromecsv' ||
|
|
||||||
format === 'operacsv' ||
|
|
||||||
format === 'vivaldicsv' ||
|
|
||||||
format === 'edgecsv'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<span *ngIf="format !== 'chromecsv'">
|
<span *ngIf="format !== 'chromecsv'">
|
||||||
The process is exactly the same as importing from Google Chrome.
|
The process is exactly the same as importing from Google Chrome.
|
||||||
</span>
|
</span>
|
||||||
See detailed instructions on our help site at
|
<p>
|
||||||
<a
|
See detailed instructions on our help site at
|
||||||
bitLink
|
<a
|
||||||
linkType="primary"
|
bitLink
|
||||||
target="_blank"
|
linkType="primary"
|
||||||
rel="noreferrer"
|
target="_blank"
|
||||||
href="https://bitwarden.com/help/import-from-chrome/"
|
rel="noreferrer"
|
||||||
>
|
href="https://bitwarden.com/help/import-from-chrome/"
|
||||||
https://bitwarden.com/help/import-from-chrome/</a
|
>
|
||||||
|
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>
|
||||||
<ng-container *ngIf="format === 'firefoxcsv'">
|
<ng-container *ngIf="format === 'firefoxcsv'">
|
||||||
See detailed instructions on our help site at
|
See detailed instructions on our help site at
|
||||||
@@ -440,12 +454,20 @@
|
|||||||
previously chosen.
|
previously chosen.
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</bit-callout>
|
</bit-callout>
|
||||||
<import-lastpass
|
@if (showLastPassOptions) {
|
||||||
*ngIf="showLastPassOptions"
|
<import-lastpass
|
||||||
[formGroup]="formGroup"
|
[formGroup]="formGroup"
|
||||||
(csvDataLoaded)="this.formGroup.controls.fileContents.setValue($event)"
|
(csvDataLoaded)="this.formGroup.controls.fileContents.setValue($event)"
|
||||||
></import-lastpass>
|
></import-lastpass>
|
||||||
<div [hidden]="showLastPassOptions">
|
} @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-form-field>
|
||||||
<bit-label>{{ "selectImportFile" | i18n }}</bit-label>
|
<bit-label>{{ "selectImportFile" | i18n }}</bit-label>
|
||||||
<div class="file-selector tw-pt-2 tw-pb-1 tw-break-words">
|
<div class="file-selector tw-pt-2 tw-pb-1 tw-break-words">
|
||||||
@@ -473,7 +495,7 @@
|
|||||||
formControlName="fileContents"
|
formControlName="fileContents"
|
||||||
></textarea>
|
></textarea>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</div>
|
}
|
||||||
</bit-card>
|
</bit-card>
|
||||||
</bit-section>
|
</bit-section>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CommonModule } from "@angular/common";
|
|||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
Component,
|
Component,
|
||||||
|
DestroyRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Inject,
|
Inject,
|
||||||
Input,
|
Input,
|
||||||
@@ -13,17 +14,23 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||||
import * as JSZip from "jszip";
|
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";
|
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.
|
// 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
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
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 {
|
import {
|
||||||
getOrganizationById,
|
getOrganizationById,
|
||||||
OrganizationService,
|
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { 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 { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
@@ -62,49 +65,20 @@ import {
|
|||||||
ToastService,
|
ToastService,
|
||||||
LinkModule,
|
LinkModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
|
||||||
|
|
||||||
|
import { ImporterMetadata, DataLoader, Loader, Instructions } from "../metadata";
|
||||||
import { ImportOption, ImportResult, ImportType } from "../models";
|
import { ImportOption, ImportResult, ImportType } from "../models";
|
||||||
import {
|
import { ImportCollectionServiceAbstraction, ImportServiceAbstraction } from "../services";
|
||||||
ImportApiService,
|
|
||||||
ImportApiServiceAbstraction,
|
|
||||||
ImportCollectionServiceAbstraction,
|
|
||||||
ImportService,
|
|
||||||
ImportServiceAbstraction,
|
|
||||||
} from "../services";
|
|
||||||
|
|
||||||
|
import { ImportChromeComponent } from "./chrome";
|
||||||
import {
|
import {
|
||||||
FilePasswordPromptComponent,
|
FilePasswordPromptComponent,
|
||||||
ImportErrorDialogComponent,
|
ImportErrorDialogComponent,
|
||||||
ImportSuccessDialogComponent,
|
ImportSuccessDialogComponent,
|
||||||
} from "./dialog";
|
} from "./dialog";
|
||||||
|
import { ImporterProviders } from "./importer-providers";
|
||||||
import { ImportLastPassComponent } from "./lastpass";
|
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({
|
@Component({
|
||||||
selector: "tools-import",
|
selector: "tools-import",
|
||||||
templateUrl: "import.component.html",
|
templateUrl: "import.component.html",
|
||||||
@@ -118,6 +92,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
SelectModule,
|
SelectModule,
|
||||||
CalloutModule,
|
CalloutModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
ImportChromeComponent,
|
||||||
ImportLastPassComponent,
|
ImportLastPassComponent,
|
||||||
RadioButtonModule,
|
RadioButtonModule,
|
||||||
CardComponent,
|
CardComponent,
|
||||||
@@ -125,7 +100,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
SectionComponent,
|
SectionComponent,
|
||||||
LinkModule,
|
LinkModule,
|
||||||
],
|
],
|
||||||
providers: safeProviders,
|
providers: ImporterProviders,
|
||||||
})
|
})
|
||||||
export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
featuredImportOptions: ImportOption[];
|
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 organization: Organization;
|
||||||
protected destroy$ = new Subject<void>();
|
protected destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@@ -184,6 +165,8 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
fileContents: [],
|
fileContents: [],
|
||||||
file: [],
|
file: [],
|
||||||
lastPassType: ["direct" as "csv" | "direct"],
|
lastPassType: ["direct" as "csv" | "direct"],
|
||||||
|
// FIXME: once the flag is disabled this should initialize to `Strategy.browser`
|
||||||
|
chromiumLoader: [Loader.file as DataLoader],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ViewChild(BitSubmitDirective)
|
@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(
|
constructor(
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
protected importService: ImportServiceAbstraction,
|
protected importService: ImportServiceAbstraction,
|
||||||
@@ -226,6 +229,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
protected toastService: ToastService,
|
protected toastService: ToastService,
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
|
private destroyRef: DestroyRef,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
protected get importBlockedByPolicy(): boolean {
|
protected get importBlockedByPolicy(): boolean {
|
||||||
@@ -246,6 +250,23 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.setImportOptions();
|
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) {
|
if (this.organizationId) {
|
||||||
await this.handleOrganizationImportInit();
|
await this.handleOrganizationImportInit();
|
||||||
} else {
|
} else {
|
||||||
@@ -578,7 +599,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
|
|
||||||
private async setImportContents(): Promise<string> {
|
private async setImportContents(): Promise<string> {
|
||||||
const fileEl = document.getElementById("import_input_file") as HTMLInputElement;
|
const fileEl = document.getElementById("import_input_file") as HTMLInputElement;
|
||||||
const files = fileEl.files;
|
const files = fileEl?.files;
|
||||||
let fileContents = this.formGroup.controls.fileContents.value;
|
let fileContents = this.formGroup.controls.fileContents.value;
|
||||||
|
|
||||||
if (files != null && files.length > 0) {
|
if (files != null && files.length > 0) {
|
||||||
|
|||||||
91
libs/importer/src/components/importer-providers.ts
Normal file
91
libs/importer/src/components/importer-providers.ts
Normal 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,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
@@ -24,6 +24,7 @@ export class ChromeCsvImporter extends BaseImporter implements Importer {
|
|||||||
cipher.login.username = this.getValueOrDefault(value.username);
|
cipher.login.username = this.getValueOrDefault(value.username);
|
||||||
cipher.login.password = this.getValueOrDefault(value.password);
|
cipher.login.password = this.getValueOrDefault(value.password);
|
||||||
cipher.login.uris = this.makeUriArray(value.url);
|
cipher.login.uris = this.makeUriArray(value.url);
|
||||||
|
cipher.notes = this.getValueOrDefault(value.note);
|
||||||
this.cleanupCipher(cipher);
|
this.cleanupCipher(cipher);
|
||||||
result.ciphers.push(cipher);
|
result.ciphers.push(cipher);
|
||||||
});
|
});
|
||||||
|
|||||||
15
libs/importer/src/metadata/availability.ts
Normal file
15
libs/importer/src/metadata/availability.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ClientType } from "@bitwarden/client-type";
|
||||||
|
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||||
|
|
||||||
|
import { Loader } from "./data";
|
||||||
|
import { DataLoader } from "./types";
|
||||||
|
|
||||||
|
/** Describes which loaders are supported on each client */
|
||||||
|
export const LoaderAvailability: Record<DataLoader, ClientType[]> = deepFreeze({
|
||||||
|
[Loader.chromium]: [ClientType.Desktop],
|
||||||
|
[Loader.download]: [ClientType.Browser],
|
||||||
|
[Loader.file]: [ClientType.Browser, ClientType.Desktop, ClientType.Web, ClientType.Cli],
|
||||||
|
|
||||||
|
// FIXME: enable IPC importer on `ClientType.Desktop` once it's ready
|
||||||
|
[Loader.ipc]: [],
|
||||||
|
});
|
||||||
27
libs/importer/src/metadata/data.ts
Normal file
27
libs/importer/src/metadata/data.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/** Mechanisms that load data into the importer. */
|
||||||
|
export const Loader = Object.freeze({
|
||||||
|
/** Data loaded from a file provided by the user/ */
|
||||||
|
file: "file",
|
||||||
|
|
||||||
|
/** Data loaded directly from the chromium browser's data store */
|
||||||
|
chromium: "chromium",
|
||||||
|
|
||||||
|
/** Data provided through an importer ipc channel (e.g. Bitwarden bridge) */
|
||||||
|
ipc: "ipc",
|
||||||
|
|
||||||
|
/** Data provided through direct file download (e.g. a LastPass export) */
|
||||||
|
download: "download",
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Re-branded products often leave their exporters unaltered; when that occurs,
|
||||||
|
* `Instructions` lets us group them together.
|
||||||
|
*
|
||||||
|
* @remarks Instructions values must be mutually exclusive from Loader's values.
|
||||||
|
*/
|
||||||
|
export const Instructions = Object.freeze({
|
||||||
|
/** the instructions are unique to the import type */
|
||||||
|
unique: "unique",
|
||||||
|
|
||||||
|
/** shared chromium instructions */
|
||||||
|
chromium: "chromium",
|
||||||
|
});
|
||||||
27
libs/importer/src/metadata/importers.ts
Normal file
27
libs/importer/src/metadata/importers.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||||
|
|
||||||
|
import { ImportType } from "../models";
|
||||||
|
|
||||||
|
import { Loader, Instructions } from "./data";
|
||||||
|
import { ImporterMetadata } from "./types";
|
||||||
|
|
||||||
|
// FIXME: load this data from rust code
|
||||||
|
const importers = [
|
||||||
|
// chromecsv import depends upon operating system, so ironically it doesn't support chromium
|
||||||
|
{ id: "chromecsv", loaders: [Loader.file], instructions: Instructions.chromium },
|
||||||
|
{ id: "operacsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||||
|
{
|
||||||
|
id: "vivaldicsv",
|
||||||
|
loaders: [Loader.file, Loader.chromium],
|
||||||
|
instructions: Instructions.chromium,
|
||||||
|
},
|
||||||
|
{ id: "bravecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||||
|
{ id: "edgecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||||
|
|
||||||
|
// FIXME: add other formats and remove `Partial` from export
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Describes which loaders are available for each import type */
|
||||||
|
export const Importers: Partial<Record<ImportType, ImporterMetadata>> = deepFreeze(
|
||||||
|
Object.fromEntries(importers.map((i) => [i.id, i])),
|
||||||
|
);
|
||||||
4
libs/importer/src/metadata/index.ts
Normal file
4
libs/importer/src/metadata/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./availability";
|
||||||
|
export * from "./data";
|
||||||
|
export * from "./types";
|
||||||
|
export * from "./importers";
|
||||||
20
libs/importer/src/metadata/types.ts
Normal file
20
libs/importer/src/metadata/types.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ImportType } from "../models";
|
||||||
|
|
||||||
|
import { Instructions, Loader } from "./data";
|
||||||
|
|
||||||
|
/** Mechanisms that load data into the importer. */
|
||||||
|
export type DataLoader = (typeof Loader)[keyof typeof Loader];
|
||||||
|
|
||||||
|
export type InstructionLink = (typeof Instructions)[keyof typeof Instructions];
|
||||||
|
|
||||||
|
/** Mechanisms that load data into the importer. */
|
||||||
|
export type ImporterMetadata = {
|
||||||
|
/** Identifies the importer */
|
||||||
|
type: ImportType;
|
||||||
|
|
||||||
|
/** Identifies the instructions for the importer; this defaults to `unique`. */
|
||||||
|
instructions?: InstructionLink;
|
||||||
|
|
||||||
|
/** Describes the strategies used to obtain imported data */
|
||||||
|
loaders: DataLoader[];
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ export interface ImportOption {
|
|||||||
export const featuredImportOptions = [
|
export const featuredImportOptions = [
|
||||||
{ id: "bitwardenjson", name: "Bitwarden (json)" },
|
{ id: "bitwardenjson", name: "Bitwarden (json)" },
|
||||||
{ id: "bitwardencsv", name: "Bitwarden (csv)" },
|
{ id: "bitwardencsv", name: "Bitwarden (csv)" },
|
||||||
{ id: "chromecsv", name: "Chrome (csv)" },
|
{ id: "chromecsv", name: "Chrome" },
|
||||||
{ id: "dashlanecsv", name: "Dashlane (csv)" },
|
{ id: "dashlanecsv", name: "Dashlane (csv)" },
|
||||||
{ id: "firefoxcsv", name: "Firefox (csv)" },
|
{ id: "firefoxcsv", name: "Firefox (csv)" },
|
||||||
{ id: "keepass2xml", name: "KeePass 2 (xml)" },
|
{ id: "keepass2xml", name: "KeePass 2 (xml)" },
|
||||||
@@ -46,9 +46,10 @@ export const regularImportOptions = [
|
|||||||
{ id: "ascendocsv", name: "Ascendo DataVault (csv)" },
|
{ id: "ascendocsv", name: "Ascendo DataVault (csv)" },
|
||||||
{ id: "meldiumcsv", name: "Meldium (csv)" },
|
{ id: "meldiumcsv", name: "Meldium (csv)" },
|
||||||
{ id: "passkeepcsv", name: "PassKeep (csv)" },
|
{ id: "passkeepcsv", name: "PassKeep (csv)" },
|
||||||
{ id: "edgecsv", name: "Edge (csv)" },
|
{ id: "edgecsv", name: "Edge" },
|
||||||
{ id: "operacsv", name: "Opera (csv)" },
|
{ id: "operacsv", name: "Opera" },
|
||||||
{ id: "vivaldicsv", name: "Vivaldi (csv)" },
|
{ id: "vivaldicsv", name: "Vivaldi" },
|
||||||
|
{ id: "bravecsv", name: "Brave" },
|
||||||
{ id: "gnomejson", name: "GNOME Passwords and Keys/Seahorse (json)" },
|
{ id: "gnomejson", name: "GNOME Passwords and Keys/Seahorse (json)" },
|
||||||
{ id: "blurcsv", name: "Blur (csv)" },
|
{ id: "blurcsv", name: "Blur (csv)" },
|
||||||
{ id: "passwordagentcsv", name: "Password Agent (csv)" },
|
{ id: "passwordagentcsv", name: "Password Agent (csv)" },
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @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.
|
// 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
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
|
|
||||||
import { Importer } from "../importers/importer";
|
import { Importer } from "../importers/importer";
|
||||||
|
import { ImporterMetadata } from "../metadata";
|
||||||
import { ImportOption, ImportType } from "../models/import-options";
|
import { ImportOption, ImportType } from "../models/import-options";
|
||||||
import { ImportResult } from "../models/import-result";
|
import { ImportResult } from "../models/import-result";
|
||||||
|
|
||||||
@@ -13,6 +16,10 @@ export abstract class ImportServiceAbstraction {
|
|||||||
featuredImportOptions: readonly ImportOption[];
|
featuredImportOptions: readonly ImportOption[];
|
||||||
regularImportOptions: readonly ImportOption[];
|
regularImportOptions: readonly ImportOption[];
|
||||||
getImportOptions: () => ImportOption[];
|
getImportOptions: () => ImportOption[];
|
||||||
|
|
||||||
|
/** describes the features supported by a format */
|
||||||
|
metadata$: (type$: Observable<ImportType>) => Observable<ImporterMetadata>;
|
||||||
|
|
||||||
import: (
|
import: (
|
||||||
importer: Importer,
|
importer: Importer,
|
||||||
fileContents: string,
|
fileContents: string,
|
||||||
|
|||||||
@@ -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 { 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.
|
// 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
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||||
|
import { ClientType } from "@bitwarden/client-type";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
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 { 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 { 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 { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
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 { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
|
||||||
import { Importer } from "../importers/importer";
|
import { Importer } from "../importers/importer";
|
||||||
|
import { ImporterMetadata, Instructions, Loader } from "../metadata";
|
||||||
|
import { ImportType } from "../models";
|
||||||
import { ImportResult } from "../models/import-result";
|
import { ImportResult } from "../models/import-result";
|
||||||
|
|
||||||
import { ImportApiServiceAbstraction } from "./import-api.service.abstraction";
|
import { ImportApiServiceAbstraction } from "./import-api.service.abstraction";
|
||||||
@@ -35,8 +43,8 @@ describe("ImportService", () => {
|
|||||||
let encryptService: MockProxy<EncryptService>;
|
let encryptService: MockProxy<EncryptService>;
|
||||||
let pinService: MockProxy<PinServiceAbstraction>;
|
let pinService: MockProxy<PinServiceAbstraction>;
|
||||||
let accountService: MockProxy<AccountService>;
|
let accountService: MockProxy<AccountService>;
|
||||||
let sdkService: MockSdkService;
|
|
||||||
let restrictedItemTypesService: MockProxy<RestrictedItemTypesService>;
|
let restrictedItemTypesService: MockProxy<RestrictedItemTypesService>;
|
||||||
|
let systemServiceProvider: MockProxy<SystemServiceProvider>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cipherService = mock<CipherService>();
|
cipherService = mock<CipherService>();
|
||||||
@@ -47,9 +55,20 @@ describe("ImportService", () => {
|
|||||||
keyService = mock<KeyService>();
|
keyService = mock<KeyService>();
|
||||||
encryptService = mock<EncryptService>();
|
encryptService = mock<EncryptService>();
|
||||||
pinService = mock<PinServiceAbstraction>();
|
pinService = mock<PinServiceAbstraction>();
|
||||||
sdkService = new MockSdkService();
|
|
||||||
restrictedItemTypesService = mock<RestrictedItemTypesService>();
|
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(
|
importService = new ImportService(
|
||||||
cipherService,
|
cipherService,
|
||||||
folderService,
|
folderService,
|
||||||
@@ -60,8 +79,8 @@ describe("ImportService", () => {
|
|||||||
encryptService,
|
encryptService,
|
||||||
pinService,
|
pinService,
|
||||||
accountService,
|
accountService,
|
||||||
sdkService,
|
|
||||||
restrictedItemTypesService,
|
restrictedItemTypesService,
|
||||||
|
systemServiceProvider,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -249,6 +268,170 @@ describe("ImportService", () => {
|
|||||||
expect(importResult.folderRelationships[1]).toEqual([0, 1]);
|
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> = {}) {
|
function createCipher(options: Partial<CipherView> = {}) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @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.
|
// 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
|
// eslint-disable-next-line no-restricted-imports
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
CollectionView,
|
CollectionView,
|
||||||
} from "@bitwarden/admin-console/common";
|
} from "@bitwarden/admin-console/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||||
import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
|
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 { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
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 { 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 { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
@@ -95,6 +97,7 @@ import {
|
|||||||
PasswordDepot17XmlImporter,
|
PasswordDepot17XmlImporter,
|
||||||
} from "../importers";
|
} from "../importers";
|
||||||
import { Importer } from "../importers/importer";
|
import { Importer } from "../importers/importer";
|
||||||
|
import { ImporterMetadata, Importers, Loader } from "../metadata";
|
||||||
import {
|
import {
|
||||||
featuredImportOptions,
|
featuredImportOptions,
|
||||||
ImportOption,
|
ImportOption,
|
||||||
@@ -104,12 +107,15 @@ import {
|
|||||||
import { ImportResult } from "../models/import-result";
|
import { ImportResult } from "../models/import-result";
|
||||||
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
|
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
|
||||||
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
|
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
|
||||||
|
import { availableLoaders as availableLoaders } from "../util";
|
||||||
|
|
||||||
export class ImportService implements ImportServiceAbstraction {
|
export class ImportService implements ImportServiceAbstraction {
|
||||||
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
|
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
|
||||||
|
|
||||||
regularImportOptions = regularImportOptions as readonly ImportOption[];
|
regularImportOptions = regularImportOptions as readonly ImportOption[];
|
||||||
|
|
||||||
|
private logger: SemanticLogger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private folderService: FolderService,
|
private folderService: FolderService,
|
||||||
@@ -120,14 +126,42 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private pinService: PinServiceAbstraction,
|
private pinService: PinServiceAbstraction,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private sdkService: SdkService,
|
|
||||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
) {}
|
private system: SystemServiceProvider,
|
||||||
|
) {
|
||||||
|
this.logger = system.log({ type: "ImportService" });
|
||||||
|
}
|
||||||
|
|
||||||
getImportOptions(): ImportOption[] {
|
getImportOptions(): ImportOption[] {
|
||||||
return this.featuredImportOptions.concat(this.regularImportOptions);
|
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(
|
async import(
|
||||||
importer: Importer,
|
importer: Importer,
|
||||||
fileContents: string,
|
fileContents: string,
|
||||||
@@ -260,6 +294,7 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
case "chromecsv":
|
case "chromecsv":
|
||||||
case "operacsv":
|
case "operacsv":
|
||||||
case "vivaldicsv":
|
case "vivaldicsv":
|
||||||
|
case "bravecsv":
|
||||||
return new ChromeCsvImporter();
|
return new ChromeCsvImporter();
|
||||||
case "firefoxcsv":
|
case "firefoxcsv":
|
||||||
return new FirefoxCsvImporter();
|
return new FirefoxCsvImporter();
|
||||||
|
|||||||
60
libs/importer/src/util.spec.ts
Normal file
60
libs/importer/src/util.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { ClientType } from "@bitwarden/client-type";
|
||||||
|
|
||||||
|
import { Loader } from "./metadata";
|
||||||
|
import { availableLoaders } from "./util";
|
||||||
|
|
||||||
|
describe("availableLoaders", () => {
|
||||||
|
describe("given valid import types", () => {
|
||||||
|
it("returns available loaders when client supports all loaders", () => {
|
||||||
|
const result = availableLoaders("operacsv", ClientType.Desktop);
|
||||||
|
|
||||||
|
expect(result).toEqual([Loader.file, Loader.chromium]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns filtered loaders when client supports some loaders", () => {
|
||||||
|
const result = availableLoaders("operacsv", ClientType.Browser);
|
||||||
|
|
||||||
|
expect(result).toEqual([Loader.file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns single loader for import types with one loader", () => {
|
||||||
|
const result = availableLoaders("chromecsv", ClientType.Desktop);
|
||||||
|
|
||||||
|
expect(result).toEqual([Loader.file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns all supported loaders for multi-loader import types", () => {
|
||||||
|
const result = availableLoaders("bravecsv", ClientType.Desktop);
|
||||||
|
|
||||||
|
expect(result).toEqual([Loader.file, Loader.chromium]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given unknown import types", () => {
|
||||||
|
it("returns undefined when import type is not found in metadata", () => {
|
||||||
|
const result = availableLoaders("nonexistent" as any, ClientType.Desktop);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("given different client types", () => {
|
||||||
|
it("returns appropriate loaders for Browser client", () => {
|
||||||
|
const result = availableLoaders("operacsv", ClientType.Browser);
|
||||||
|
|
||||||
|
expect(result).toEqual([Loader.file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns appropriate loaders for Web client", () => {
|
||||||
|
const result = availableLoaders("chromecsv", ClientType.Web);
|
||||||
|
|
||||||
|
expect(result).toEqual([Loader.file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns appropriate loaders for CLI client", () => {
|
||||||
|
const result = availableLoaders("vivaldicsv", ClientType.Cli);
|
||||||
|
|
||||||
|
expect(result).toEqual([Loader.file]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
19
libs/importer/src/util.ts
Normal file
19
libs/importer/src/util.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { ClientType } from "@bitwarden/client-type";
|
||||||
|
|
||||||
|
import { LoaderAvailability, Importers } from "./metadata";
|
||||||
|
import { ImportType } from "./models";
|
||||||
|
|
||||||
|
/** Lookup the loaders supported by a specific client.
|
||||||
|
* WARNING: this method does not supply metadata for every import type.
|
||||||
|
* @returns `undefined` when metadata is not defined for the type, or
|
||||||
|
* an array identifying the supported clients.
|
||||||
|
*/
|
||||||
|
export function availableLoaders(type: ImportType, client: ClientType) {
|
||||||
|
if (!(type in Importers)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const capabilities = Importers[type]?.loaders ?? [];
|
||||||
|
const available = capabilities.filter((loader) => LoaderAvailability[loader].includes(client));
|
||||||
|
return available;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user