1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-27341] Chrome importer refactors (#16720)

Various refactors to the chrome importer
This commit is contained in:
Oscar Hinton
2025-10-27 17:24:50 +01:00
committed by GitHub
parent bd89c0ce6d
commit 42377a1533
20 changed files with 161 additions and 185 deletions

View File

@@ -440,33 +440,6 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "bitwarden_chromium_importer"
version = "0.0.0"
dependencies = [
"aes",
"aes-gcm",
"anyhow",
"async-trait",
"base64",
"cbc",
"hex",
"homedir",
"napi",
"napi-derive",
"oo7",
"pbkdf2",
"rand 0.9.1",
"rusqlite",
"security-framework",
"serde",
"serde_json",
"sha1",
"tokio",
"winapi",
"windows 0.61.1",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -606,6 +579,31 @@ dependencies = [
"zeroize",
]
[[package]]
name = "chromium_importer"
version = "0.0.0"
dependencies = [
"aes",
"aes-gcm",
"anyhow",
"async-trait",
"base64",
"cbc",
"hex",
"homedir",
"oo7",
"pbkdf2",
"rand 0.9.1",
"rusqlite",
"security-framework",
"serde",
"serde_json",
"sha1",
"tokio",
"winapi",
"windows 0.61.1",
]
[[package]]
name = "cipher"
version = "0.4.4"
@@ -968,7 +966,7 @@ dependencies = [
"anyhow",
"autotype",
"base64",
"bitwarden_chromium_importer",
"chromium_importer",
"desktop_core",
"hex",
"napi",

View File

@@ -2,7 +2,7 @@
resolver = "2"
members = [
"autotype",
"bitwarden_chromium_importer",
"chromium_importer",
"core",
"macos_provider",
"napi",

View File

@@ -1,48 +0,0 @@
//! Cryptographic primitives used in the SDK
use anyhow::{anyhow, Result};
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();
cbc::Decryptor::<aes::Aes256>::new(&key, iv)
.decrypt_padded_mut::<Pkcs7>(&mut data)
.map_err(|_| anyhow!("Failed to decrypt data"))?;
Ok(data)
}
#[cfg(test)]
mod tests {
use aes::cipher::{
generic_array::{sequence::GenericSequence, GenericArray},
ArrayLength,
};
use base64::{engine::general_purpose::STANDARD, Engine};
pub fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
(0..length).map(|i| offset + i as u8 * increment).collect()
}
pub fn generate_generic_array<N: ArrayLength<u8>>(
offset: u8,
increment: u8,
) -> GenericArray<u8, N> {
GenericArray::generate(|i| offset + i as u8 * increment)
}
#[test]
fn test_decrypt_aes256() {
let iv = generate_vec(16, 0, 1);
let iv: &[u8; 16] = iv.as_slice().try_into().unwrap();
let key = generate_generic_array(0, 1);
let data: Vec<u8> = STANDARD.decode("ByUF8vhyX4ddU9gcooznwA==").unwrap();
let decrypted = super::decrypt_aes256(iv, &data, key).unwrap();
assert_eq!(String::from_utf8(decrypted).unwrap(), "EncryptMe!\u{6}\u{6}\u{6}\u{6}\u{6}\u{6}");
}
}

View File

@@ -1,8 +0,0 @@
#[macro_use]
extern crate napi_derive;
pub mod chromium;
pub mod metadata;
pub mod util;
pub use crate::chromium::platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS;

View File

@@ -1,5 +1,5 @@
[package]
name = "bitwarden_chromium_importer"
name = "chromium_importer"
edition = { workspace = true }
license = { workspace = true }
version = { workspace = true }
@@ -14,8 +14,6 @@ base64 = { workspace = true }
cbc = { workspace = true, features = ["alloc"] }
hex = { workspace = true }
homedir = { workspace = true }
napi = { workspace = true }
napi-derive = { workspace = true }
pbkdf2 = "=0.12.2"
rand = { workspace = true }
rusqlite = { version = "=0.37.0", features = ["bundled"] }
@@ -36,4 +34,3 @@ oo7 = { workspace = true }
[lints]
workspace = true

View File

@@ -1,6 +1,13 @@
# Windows ABE Architecture
# Chromium Direct Importer
## Overview
A rust library that allows you to directly import credentials from Chromium-based browsers.
## Windows ABE Architecture
On Windows chrome has additional protection measurements which needs to be circumvented in order to
get access to the passwords.
### Overview
The Windows Application Bound Encryption (ABE) consists of three main components that work together:
@@ -10,7 +17,7 @@ The Windows Application Bound Encryption (ABE) consists of three main components
_(The names of the binaries will be changed for the released product.)_
## The goal
### 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
@@ -24,7 +31,7 @@ Protection API at the system level on top of that. This triply encrypted key is
The next paragraphs describe what is done at each level to decrypt the key.
## 1. Client library
### 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
@@ -52,7 +59,7 @@ admin.exe --service-exe "c:\temp\service.exe" --encrypted "QVBQQgEAAADQjJ3fARXRE
**At this point, the user must permit the action to be performed on the UAC screen.**
## 2. Admin executable
### 2. Admin executable
This executable receives the full path of `service.exe` and the data to be decrypted.
@@ -67,7 +74,7 @@ is sent to the named pipe server created by the user. The user responds with `ok
After that, the executable stops and uninstalls the service and then exits.
## 3. System service
### 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
@@ -83,7 +90,7 @@ removed from the system. Even though we send only one request, the service is de
many clients with as many messages as needed and could be installed on the system permanently if
necessary.
## 4. Back to client library
### 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.
@@ -99,7 +106,7 @@ itself), it's either AES-256-GCM or ChaCha20Poly1305 encryption scheme. The deta
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
### Summary
The Windows ABE decryption process involves a three-tier architecture with named pipe communication:

View File

@@ -7,11 +7,9 @@ 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")]
pub mod platform;
mod platform;
pub(crate) use platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS;
//
// Public API
@@ -22,10 +20,7 @@ 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>,
}
@@ -113,12 +108,12 @@ pub async fn import_logins(
//
#[derive(Debug, Clone, Copy)]
pub struct BrowserConfig {
pub(crate) struct BrowserConfig {
pub name: &'static str,
pub data_dir: &'static str,
}
pub static SUPPORTED_BROWSER_MAP: LazyLock<
pub(crate) static SUPPORTED_BROWSER_MAP: LazyLock<
std::collections::HashMap<&'static str, &'static BrowserConfig>,
> = LazyLock::new(|| {
platform::SUPPORTED_BROWSERS
@@ -140,12 +135,12 @@ fn get_browser_data_dir(config: &BrowserConfig) -> Result<PathBuf> {
//
#[async_trait]
pub trait CryptoService: Send {
pub(crate) trait CryptoService: Send {
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String>;
}
#[derive(serde::Deserialize, Clone)]
pub struct LocalState {
pub(crate) struct LocalState {
profile: AllProfiles,
#[allow(dead_code)]
os_crypt: Option<OsCrypt>,
@@ -198,16 +193,17 @@ fn load_local_state(browser_dir: &Path) -> Result<LocalState> {
}
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 {
local_state
.profile
.info_cache
.iter()
.map(|(name, info)| ProfileInfo {
name: info.name.clone(),
folder: name.clone(),
account_name: info.gaia_name.clone(),
account_email: info.user_name.clone(),
});
}
profile_infos
})
.collect()
}
struct EncryptedLogin {
@@ -264,17 +260,16 @@ 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 table_exist(conn: &Connection, table_name: &str) -> Result<bool, rusqlite::Error> {
conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")?
.exists(params![table_name])
}
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")?;
let have_logins = table_exist(&conn, "logins")?;
let have_password_notes = table_exist(&conn, "password_notes")?;
if !have_logins || !have_password_notes {
return Ok(vec![]);
}
@@ -308,10 +303,7 @@ fn query_logins(db_path: &str) -> Result<Vec<EncryptedLogin>, rusqlite::Error> {
})
})?;
let mut logins = Vec::new();
for login in logins_iter {
logins.push(login?);
}
let logins = logins_iter.collect::<Result<Vec<_>, _>>()?;
Ok(logins)
}

View File

@@ -13,7 +13,7 @@ use crate::util;
//
// 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] = [
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
BrowserConfig {
name: "Chrome",
data_dir: ".config/google-chrome",
@@ -32,7 +32,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [
},
];
pub fn get_crypto_service(
pub(crate) fn get_crypto_service(
browser_name: &String,
_local_state: &LocalState,
) -> Result<Box<dyn CryptoService>> {

View File

@@ -10,7 +10,7 @@ use crate::util;
// Public API
//
pub const SUPPORTED_BROWSERS: [BrowserConfig; 7] = [
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
BrowserConfig {
name: "Chrome",
data_dir: "Library/Application Support/Google/Chrome",
@@ -41,7 +41,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 7] = [
},
];
pub fn get_crypto_service(
pub(crate) fn get_crypto_service(
browser_name: &String,
_local_state: &LocalState,
) -> Result<Box<dyn CryptoService>> {

View File

@@ -0,0 +1,7 @@
// 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 native;
pub(crate) use native::*;

View File

@@ -15,8 +15,7 @@ use crate::util;
// Public API
//
// IMPORTANT adjust array size when enabling / disabling chromium importers here
pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
BrowserConfig {
name: "Brave",
data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data",
@@ -43,7 +42,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
},
];
pub fn get_crypto_service(
pub(crate) fn get_crypto_service(
_browser_name: &str,
local_state: &LocalState,
) -> Result<Box<dyn CryptoService>> {

View File

@@ -0,0 +1,5 @@
#![doc = include_str!("../README.md")]
pub mod chromium;
pub mod metadata;
mod util;

View File

@@ -1,8 +1,7 @@
use std::collections::{HashMap, HashSet};
use crate::{chromium::InstalledBrowserRetriever, PLATFORM_SUPPORTED_BROWSERS};
use crate::chromium::{InstalledBrowserRetriever, PLATFORM_SUPPORTED_BROWSERS};
#[napi(object)]
/// Mechanisms that load data into the importer
pub struct NativeImporterMetadata {
/// Identifies the importer
@@ -24,7 +23,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
// Check for installed browsers
let installed_browsers = T::get_installed_browsers().unwrap_or_default();
const IMPORTERS: [(&str, &str); 6] = [
const IMPORTERS: &[(&str, &str)] = &[
("chromecsv", "Chrome"),
("chromiumcsv", "Chromium"),
("bravecsv", "Brave"),
@@ -57,9 +56,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
map
}
/*
Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage
*/
// Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1,9 +1,6 @@
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])> {
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 {}",
@@ -15,7 +12,14 @@ pub fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> {
Ok((std::str::from_utf8(version)?, password))
}
pub fn split_encrypted_string_and_validate<'a>(
/// A Chromium password consists of three parts:
/// - Version (3 bytes): "v10", "v11", etc.
/// - Cipher text (chunks of 16 bytes)
/// - Padding (1-15 bytes)
///
/// This function splits the encrypted byte slice into version and cipher text.
/// Padding is included and handled by the underlying cryptographic library.
pub(crate) fn split_encrypted_string_and_validate<'a>(
encrypted: &'a [u8],
supported_versions: &[&str],
) -> Result<(&'a str, &'a [u8])> {
@@ -27,15 +31,22 @@ pub fn split_encrypted_string_and_validate<'a>(
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: Vec<u8> = decryptor
/// Decrypt using AES-128 in CBC mode.
#[cfg(any(target_os = "linux", target_os = "macos", test))]
pub(crate) fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv)?
.decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
.map_err(|e| anyhow!("Failed to decrypt: {}", e))?;
Ok(plaintext)
.map_err(|e| anyhow!("Failed to decrypt: {}", e))
}
pub fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
/// Derives a PBKDF2 key from the static "saltysalt" salt with the given password and iteration count.
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
use pbkdf2::{hmac::Hmac, pbkdf2};
use sha1::Sha1;
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))?;
@@ -44,16 +55,6 @@ pub fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
#[cfg(test)]
mod tests {
pub fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
(0..length).map(|i| offset + i as u8 * increment).collect()
}
pub fn generate_generic_array<N: ArrayLength<u8>>(
offset: u8,
increment: u8,
) -> GenericArray<u8, N> {
GenericArray::generate(|i| offset + i as u8 * increment)
}
use aes::cipher::{
block_padding::Pkcs7,
generic_array::{sequence::GenericSequence, GenericArray},
@@ -64,6 +65,17 @@ mod tests {
const LENGTH10: usize = 10;
const LENGTH0: usize = 0;
fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
(0..length).map(|i| offset + i as u8 * increment).collect()
}
fn generate_generic_array<N: ArrayLength<u8>>(
offset: u8,
increment: u8,
) -> GenericArray<u8, N> {
GenericArray::generate(|i| offset + i as u8 * increment)
}
fn run_split_encrypted_string_test<'a, const N: usize>(
successfully_split: bool,
plaintext_to_encrypt: &'a str,

View File

@@ -17,7 +17,7 @@ manual_test = []
anyhow = { workspace = true }
autotype = { path = "../autotype" }
base64 = { workspace = true }
bitwarden_chromium_importer = { path = "../bitwarden_chromium_importer" }
chromium_importer = { path = "../chromium_importer" }
desktop_core = { path = "../core" }
hex = { workspace = true }
napi = { workspace = true, features = ["async"] }

View File

@@ -3,15 +3,6 @@
/* auto-generated by NAPI-RS */
/** Mechanisms that load data into the importer */
export interface NativeImporterMetadata {
/** Identifies the importer */
id: string
/** Describes the strategies used to obtain imported data */
loaders: Array<string>
/** Identifies the instructions for the importer */
instructions: string
}
export declare namespace passwords {
/** The error message returned when a password is not found during retrieval or deletion. */
export const PASSWORD_NOT_FOUND: string
@@ -249,6 +240,11 @@ export declare namespace chromium_importer {
login?: Login
failure?: LoginImportFailure
}
export interface NativeImporterMetadata {
id: string
loaders: Array<string>
instructions: string
}
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
export function getMetadata(): Record<string, NativeImporterMetadata>
export function getInstalledBrowsers(): Array<string>

View File

@@ -1064,11 +1064,13 @@ pub mod logging {
#[napi]
pub mod chromium_importer {
use bitwarden_chromium_importer::chromium::DefaultInstalledBrowserRetriever;
use bitwarden_chromium_importer::chromium::InstalledBrowserRetriever;
use bitwarden_chromium_importer::chromium::LoginImportResult as _LoginImportResult;
use bitwarden_chromium_importer::chromium::ProfileInfo as _ProfileInfo;
use bitwarden_chromium_importer::metadata::NativeImporterMetadata;
use chromium_importer::{
chromium::{
DefaultInstalledBrowserRetriever, InstalledBrowserRetriever,
LoginImportResult as _LoginImportResult, ProfileInfo as _ProfileInfo,
},
metadata::NativeImporterMetadata as _NativeImporterMetadata,
};
use std::collections::HashMap;
#[napi(object)]
@@ -1098,6 +1100,13 @@ pub mod chromium_importer {
pub failure: Option<LoginImportFailure>,
}
#[napi(object)]
pub struct NativeImporterMetadata {
pub id: String,
pub loaders: Vec<&'static str>,
pub instructions: &'static str,
}
impl From<_LoginImportResult> for LoginImportResult {
fn from(l: _LoginImportResult) -> Self {
match l {
@@ -1131,23 +1140,34 @@ pub mod chromium_importer {
}
}
impl From<_NativeImporterMetadata> for NativeImporterMetadata {
fn from(m: _NativeImporterMetadata) -> Self {
NativeImporterMetadata {
id: m.id,
loaders: m.loaders,
instructions: m.instructions,
}
}
}
#[napi]
/// Returns OS aware metadata describing supported Chromium based importers as a JSON string.
pub fn get_metadata() -> HashMap<String, NativeImporterMetadata> {
bitwarden_chromium_importer::metadata::get_supported_importers::<
DefaultInstalledBrowserRetriever,
>()
chromium_importer::metadata::get_supported_importers::<DefaultInstalledBrowserRetriever>()
.into_iter()
.map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata)))
.collect()
}
#[napi]
pub fn get_installed_browsers() -> napi::Result<Vec<String>> {
bitwarden_chromium_importer::chromium::DefaultInstalledBrowserRetriever::get_installed_browsers()
chromium_importer::chromium::DefaultInstalledBrowserRetriever::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)
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()))
}
@@ -1157,7 +1177,7 @@ pub mod chromium_importer {
browser: String,
profile_id: String,
) -> napi::Result<Vec<LoginImportResult>> {
bitwarden_chromium_importer::chromium::import_logins(&browser, &profile_id)
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()))

View File

@@ -1,5 +1,5 @@
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import type { NativeImporterMetadata } from "@bitwarden/desktop-napi";
import type { chromium_importer } from "@bitwarden/desktop-napi";
import {
ImportType,
DefaultImportMetadataService,
@@ -25,7 +25,9 @@ export class DesktopImportMetadataService
await super.init();
}
private async parseNativeMetaData(raw: Record<string, NativeImporterMetadata>): Promise<void> {
private async parseNativeMetaData(
raw: Record<string, chromium_importer.NativeImporterMetadata>,
): Promise<void> {
const entries = Object.entries(raw).map(([id, meta]) => {
const loaders = meta.loaders.map(this.mapLoader);
const instructions = this.mapInstructions(meta.instructions);

View File

@@ -1,9 +1,9 @@
import { ipcRenderer } from "electron";
import type { NativeImporterMetadata } from "@bitwarden/desktop-napi";
import type { chromium_importer } from "@bitwarden/desktop-napi";
const chromiumImporter = {
getMetadata: (): Promise<Record<string, NativeImporterMetadata>> =>
getMetadata: (): Promise<Record<string, chromium_importer.NativeImporterMetadata>> =>
ipcRenderer.invoke("chromium_importer.getMetadata"),
getInstalledBrowsers: (): Promise<string[]> =>
ipcRenderer.invoke("chromium_importer.getInstalledBrowsers"),