1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-12-05 09:14:28 +01:00
This commit is contained in:
John Harrington 2025-12-05 03:10:30 +00:00 committed by GitHub
commit 061f5f9cf3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 680 additions and 64 deletions

View File

@ -610,6 +610,7 @@ dependencies = [
"async-trait",
"base64",
"cbc",
"desktop_objc",
"dirs",
"hex",
"oo7",

View File

@ -28,7 +28,11 @@ let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platfo
function buildNapiModule(target, release = true) {
const targetArg = target ? `--target ${target}` : "";
const releaseArg = release ? "--release" : "";
child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") });
child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, {
stdio: 'inherit',
cwd: path.join(__dirname, "napi"),
env: process.env // Pass environment variables including SANDBOX_BUILD
});
}
function buildProxyBin(target, release = true) {

View File

@ -18,9 +18,11 @@ serde_json = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
cbc = { workspace = true, features = ["alloc"] }
desktop_objc = { path = "../objc" }
pbkdf2 = "=0.12.2"
security-framework = { workspace = true }
sha1 = "=0.10.6"
tokio = { workspace = true, features = ["rt"] }
[target.'cfg(target_os = "windows")'.dependencies]
aes-gcm = { workspace = true }

View File

@ -51,19 +51,24 @@ pub enum LoginImportResult {
}
pub trait InstalledBrowserRetriever {
fn get_installed_browsers() -> Result<Vec<String>>;
fn get_installed_browsers(mas_build: bool) -> Result<Vec<String>>;
}
pub struct DefaultInstalledBrowserRetriever {}
impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever {
fn get_installed_browsers() -> Result<Vec<String>> {
fn get_installed_browsers(mas_build: bool) -> 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_and_validate_data_dir(config);
if data_dir.is_ok() {
if mas_build {
// show all browsers for MAS builds, user will grant access when selected
browsers.push((*browser).to_string());
} else {
// When not in sandbox check file system directly
let data_dir = get_and_validate_data_dir(config)?;
if data_dir.exists() {
browsers.push((*browser).to_string());
}
}
}
@ -71,15 +76,45 @@ impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever {
}
}
pub fn get_available_profiles(browser_name: &String) -> Result<Vec<ProfileInfo>> {
pub async fn get_available_profiles(
browser_name: &str,
mas_build: bool,
) -> Result<Vec<ProfileInfo>> {
// MAS builds need to resume security-scoped access before reading browser files
#[cfg(target_os = "macos")]
let _access = if mas_build {
Some(platform::sandbox::ScopedBrowserAccess::resume(browser_name).await?)
} else {
None
};
let (_, local_state) = load_local_state_for_browser(browser_name)?;
Ok(get_profile_info(&local_state))
}
/// Request access to browser directory (MAS builds only)
/// This shows the permission dialog and creates a security-scoped bookmark
#[cfg(target_os = "macos")]
pub async fn request_browser_access(browser_name: &str, mas_build: bool) -> Result<()> {
if mas_build {
platform::sandbox::ScopedBrowserAccess::request_only(browser_name).await?;
}
Ok(())
}
pub async fn import_logins(
browser_name: &String,
profile_id: &String,
browser_name: &str,
profile_id: &str,
_mas_build: bool,
) -> Result<Vec<LoginImportResult>> {
// MAS builds will use the formerly created security bookmark
#[cfg(target_os = "macos")]
let _access = if _mas_build {
Some(platform::sandbox::ScopedBrowserAccess::resume(browser_name).await?)
} else {
None
};
let (data_dir, local_state) = load_local_state_for_browser(browser_name)?;
let mut crypto_service = platform::get_crypto_service(browser_name, &local_state)
@ -177,9 +212,9 @@ struct OsCrypt {
app_bound_encrypted_key: Option<String>,
}
fn load_local_state_for_browser(browser_name: &String) -> Result<(PathBuf, LocalState)> {
fn load_local_state_for_browser(browser_name: &str) -> Result<(PathBuf, LocalState)> {
let config = SUPPORTED_BROWSER_MAP
.get(browser_name.as_str())
.get(browser_name)
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
let data_dir = get_and_validate_data_dir(config)?;
@ -222,11 +257,7 @@ struct EncryptedLogin {
encrypted_note: Vec<u8>,
}
fn get_logins(
browser_dir: &Path,
profile_id: &String,
filename: &str,
) -> Result<Vec<EncryptedLogin>> {
fn get_logins(browser_dir: &Path, profile_id: &str, 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

View File

@ -38,7 +38,7 @@ pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
];
pub(crate) fn get_crypto_service(
browser_name: &String,
browser_name: &str,
_local_state: &LocalState,
) -> Result<Box<dyn CryptoService>> {
let config = KEYRING_CONFIG

View File

@ -7,6 +7,164 @@ use crate::{
util,
};
//
// Sandbox specific (for Mac App Store Builds)
//
pub mod sandbox {
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
enum CommandResult<T> {
#[serde(rename = "success")]
Success { value: T },
#[serde(rename = "error")]
Error { error: String },
}
#[derive(Debug, Deserialize)]
struct RequestAccessResponse {
#[allow(dead_code)]
bookmark: String,
}
#[derive(Debug, Deserialize)]
struct HasStoredAccessResponse {
#[serde(rename = "hasAccess")]
has_access: bool,
}
#[derive(Debug, Deserialize)]
struct StartAccessResponse {
#[allow(dead_code)]
path: String,
}
#[derive(Debug, Serialize)]
struct CommandInput {
namespace: String,
command: String,
params: serde_json::Value,
}
pub struct ScopedBrowserAccess {
browser_name: String,
}
impl ScopedBrowserAccess {
/// Request access to browser directory and create a security bookmark if access is approved
pub async fn request_only(browser_name: &str) -> Result<()> {
let config = crate::chromium::platform::SUPPORTED_BROWSERS
.iter()
.find(|b| b.name == browser_name)
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
// For macOS, data_dir is always a single-element array
let relative_path = config
.data_dir
.first()
.ok_or_else(|| anyhow!("No data directory configured for browser"))?;
let input = CommandInput {
namespace: "chromium_importer".to_string(),
command: "request_access".to_string(),
params: serde_json::json!({
"browserName": browser_name,
"relativePath": relative_path,
}),
};
let output = desktop_objc::run_command(serde_json::to_string(&input)?)
.await
.map_err(|e| anyhow!("Failed to call ObjC command: {}", e))?;
let result: CommandResult<RequestAccessResponse> = serde_json::from_str(&output)
.map_err(|e| anyhow!("Failed to parse ObjC response: {}", e))?;
match result {
CommandResult::Success { .. } => Ok(()),
CommandResult::Error { error } => Err(anyhow!("{}", error)),
}
}
/// Resume browser directory access using previously created security bookmark
pub async fn resume(browser_name: &str) -> Result<Self> {
// First check if we have stored access
let has_access_input = CommandInput {
namespace: "chromium_importer".to_string(),
command: "has_stored_access".to_string(),
params: serde_json::json!({
"browserName": browser_name,
}),
};
let has_access_output =
desktop_objc::run_command(serde_json::to_string(&has_access_input)?)
.await
.map_err(|e| anyhow!("Failed to call ObjC command: {}", e))?;
let has_access_result: CommandResult<HasStoredAccessResponse> =
serde_json::from_str(&has_access_output)
.map_err(|e| anyhow!("Failed to parse ObjC response: {}", e))?;
let has_access = match has_access_result {
CommandResult::Success { value } => value.has_access,
CommandResult::Error { error } => return Err(anyhow!("{}", error)),
};
if !has_access {
return Err(anyhow!("Access has not been granted for this browser"));
}
// Start accessing the browser
let start_input = CommandInput {
namespace: "chromium_importer".to_string(),
command: "start_access".to_string(),
params: serde_json::json!({
"browserName": browser_name,
}),
};
let start_output = desktop_objc::run_command(serde_json::to_string(&start_input)?)
.await
.map_err(|e| anyhow!("Failed to call ObjC command: {}", e))?;
let start_result: CommandResult<StartAccessResponse> =
serde_json::from_str(&start_output)
.map_err(|e| anyhow!("Failed to parse ObjC response: {}", e))?;
match start_result {
CommandResult::Success { .. } => Ok(Self {
browser_name: browser_name.to_string(),
}),
CommandResult::Error { error } => Err(anyhow!("{}", error)),
}
}
}
impl Drop for ScopedBrowserAccess {
fn drop(&mut self) {
let browser_name = self.browser_name.clone();
tokio::task::spawn(async move {
let input = CommandInput {
namespace: "chromium_importer".to_string(),
command: "stop_access".to_string(),
params: serde_json::json!({
"browserName": browser_name,
}),
};
if let Ok(input_json) = serde_json::to_string(&input) {
let _ = desktop_objc::run_command(input_json).await;
}
});
}
}
}
//
// Public API
//
@ -43,7 +201,7 @@ pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
];
pub(crate) fn get_crypto_service(
browser_name: &String,
browser_name: &str,
_local_state: &LocalState,
) -> Result<Box<dyn CryptoService>> {
let config = KEYCHAIN_CONFIG

View File

@ -17,11 +17,12 @@ pub struct NativeImporterMetadata {
/// Only browsers listed in PLATFORM_SUPPORTED_BROWSERS will have the "chromium" loader.
/// All importers will have the "file" loader.
pub fn get_supported_importers<T: InstalledBrowserRetriever>(
mas_build: bool,
) -> HashMap<String, NativeImporterMetadata> {
let mut map = HashMap::new();
// Check for installed browsers
let installed_browsers = T::get_installed_browsers().unwrap_or_default();
let installed_browsers = T::get_installed_browsers(mas_build).unwrap_or_default();
const IMPORTERS: &[(&str, &str)] = &[
("chromecsv", "Chrome"),
@ -67,7 +68,7 @@ mod tests {
pub struct MockInstalledBrowserRetriever {}
impl InstalledBrowserRetriever for MockInstalledBrowserRetriever {
fn get_installed_browsers() -> Result<Vec<String>, anyhow::Error> {
fn get_installed_browsers(_mas_build: bool) -> Result<Vec<String>, anyhow::Error> {
Ok(SUPPORTED_BROWSER_MAP
.keys()
.map(|browser| browser.to_string())
@ -91,7 +92,7 @@ mod tests {
#[cfg(target_os = "macos")]
#[test]
fn macos_returns_all_known_importers() {
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
let expected: HashSet<String> = HashSet::from([
"chromecsv".to_string(),
@ -114,7 +115,7 @@ mod tests {
#[cfg(target_os = "macos")]
#[test]
fn macos_specific_loaders_match_const_array() {
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
let ids = [
"chromecsv",
"chromiumcsv",
@ -133,7 +134,7 @@ mod tests {
#[cfg(target_os = "linux")]
#[test]
fn returns_all_known_importers() {
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
let expected: HashSet<String> = HashSet::from([
"chromecsv".to_string(),
@ -154,7 +155,7 @@ mod tests {
#[cfg(target_os = "linux")]
#[test]
fn linux_specific_loaders_match_const_array() {
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
let ids = ["chromecsv", "chromiumcsv", "bravecsv", "operacsv"];
for id in ids {
@ -167,7 +168,7 @@ mod tests {
#[cfg(target_os = "windows")]
#[test]
fn returns_all_known_importers() {
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
let expected: HashSet<String> = HashSet::from([
"bravecsv".to_string(),
@ -190,7 +191,7 @@ mod tests {
#[cfg(target_os = "windows")]
#[test]
fn windows_specific_loaders_match_const_array() {
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
let map = get_supported_importers::<MockInstalledBrowserRetriever>(false);
let ids = [
"bravecsv",
"chromecsv",

View File

@ -253,9 +253,10 @@ export declare namespace chromium_importer {
instructions: string
}
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
export function getMetadata(): Record<string, NativeImporterMetadata>
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
export function getMetadata(masBuild: boolean): Record<string, NativeImporterMetadata>
export function getAvailableProfiles(browser: string, masBuild: boolean): Promise<Array<ProfileInfo>>
export function importLogins(browser: string, profileId: string, masBuild: boolean): Promise<Array<LoginImportResult>>
export function requestBrowserAccess(browser: string, masBuild: boolean): Promise<void>
}
export declare namespace autotype {
export function getForegroundWindowTitle(): string

View File

@ -1158,16 +1158,22 @@ pub mod chromium_importer {
#[napi]
/// Returns OS aware metadata describing supported Chromium based importers as a JSON string.
pub fn get_metadata() -> HashMap<String, NativeImporterMetadata> {
chromium_importer::metadata::get_supported_importers::<DefaultInstalledBrowserRetriever>()
.into_iter()
.map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata)))
.collect()
pub fn get_metadata(mas_build: bool) -> HashMap<String, NativeImporterMetadata> {
chromium_importer::metadata::get_supported_importers::<DefaultInstalledBrowserRetriever>(
mas_build,
)
.into_iter()
.map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata)))
.collect()
}
#[napi]
pub fn get_available_profiles(browser: String) -> napi::Result<Vec<ProfileInfo>> {
chromium_importer::chromium::get_available_profiles(&browser)
pub async fn get_available_profiles(
browser: String,
mas_build: bool,
) -> napi::Result<Vec<ProfileInfo>> {
chromium_importer::chromium::get_available_profiles(&browser, mas_build)
.await
.map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect())
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
@ -1176,12 +1182,25 @@ pub mod chromium_importer {
pub async fn import_logins(
browser: String,
profile_id: String,
mas_build: bool,
) -> napi::Result<Vec<LoginImportResult>> {
chromium_importer::chromium::import_logins(&browser, &profile_id)
chromium_importer::chromium::import_logins(&browser, &profile_id, mas_build)
.await
.map(|logins| logins.into_iter().map(LoginImportResult::from).collect())
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
#[allow(clippy::unused_async)]
pub async fn request_browser_access(_browser: String, _mas_build: bool) -> napi::Result<()> {
#[cfg(target_os = "macos")]
return chromium_importer::chromium::request_browser_access(&_browser, _mas_build)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()));
#[cfg(not(target_os = "macos"))]
Ok(())
}
}
#[napi]

View File

@ -10,7 +10,7 @@ default = []
[target.'cfg(target_os = "macos")'.dependencies]
anyhow = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
tracing = { workspace = true }
[target.'cfg(target_os = "macos")'.build-dependencies]

View File

@ -1,9 +1,11 @@
#[cfg(target_os = "macos")]
fn main() {
use glob::glob;
// Compile Objective-C files
let mut builder = cc::Build::new();
// Auto compile all .m files in the src/native directory
// Compile all .m files in the src/native directory
for entry in glob("src/native/**/*.m").expect("Failed to read glob pattern") {
let path = entry.expect("Failed to read glob entry");
builder.file(path.clone());
@ -12,7 +14,11 @@ fn main() {
builder
.flag("-fobjc-arc") // Enable Auto Reference Counting (ARC)
.compile("autofill");
.compile("objc_code");
// Link required frameworks
println!("cargo:rustc-link-lib=framework=Foundation");
println!("cargo:rustc-link-lib=framework=AppKit");
}
#[cfg(not(target_os = "macos"))]

View File

@ -0,0 +1,3 @@
#import <Foundation/Foundation.h>
#import "utils.h"
#import "interop.h"

View File

@ -0,0 +1,25 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface BrowserAccessManager : NSObject
- (instancetype)init;
/// Request access to a specific browser's directory
/// Returns security bookmark data (used to persist permissions) as base64 string, or nil if user declined
- (nullable NSString *)requestAccessToBrowserDir:(NSString *)browserName relativePath:(NSString *)relativePath;
/// Check if we have stored bookmark for browser (doesn't verify it's still valid)
- (BOOL)hasStoredAccess:(NSString *)browserName;
/// Start accessing a browser directory using stored bookmark
/// Returns the resolved path, or nil if bookmark is invalid/revoked
- (nullable NSString *)startAccessingBrowser:(NSString *)browserName;
/// Stop accessing a browser directory (must be called after startAccessingBrowser)
- (void)stopAccessingBrowser:(NSString *)browserName;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,164 @@
#import "browser_access_manager.h"
#import <Cocoa/Cocoa.h>
@implementation BrowserAccessManager {
NSString *_bookmarkKey;
}
- (instancetype)init {
self = [super init];
if (self) {
_bookmarkKey = @"com.bitwarden.chromiumImporter.bookmarks";
}
return self;
}
- (NSString *)requestAccessToBrowserDir:(NSString *)browserName relativePath:(NSString *)relativePath {
if (!relativePath) {
return nil;
}
NSURL *homeDir = [[NSFileManager defaultManager] homeDirectoryForCurrentUser];
NSURL *browserPath = [homeDir URLByAppendingPathComponent:relativePath];
// NSOpenPanel must be run on the main thread
__block NSURL *selectedURL = nil;
__block NSModalResponse panelResult = NSModalResponseCancel;
void (^showPanel)(void) = ^{
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
openPanel.message = [NSString stringWithFormat:
@"Please select your %@ data folder\n\nExpected location:\n%@",
browserName, browserPath.path];
openPanel.prompt = @"Grant Access";
openPanel.allowsMultipleSelection = NO;
openPanel.canChooseDirectories = YES;
openPanel.canChooseFiles = NO;
openPanel.directoryURL = browserPath;
panelResult = [openPanel runModal];
selectedURL = openPanel.URL;
};
if ([NSThread isMainThread]) {
showPanel();
} else {
dispatch_sync(dispatch_get_main_queue(), showPanel);
}
if (panelResult != NSModalResponseOK || !selectedURL) {
return nil;
}
NSURL *localStatePath = [selectedURL URLByAppendingPathComponent:@"Local State"];
if (![[NSFileManager defaultManager] fileExistsAtPath:localStatePath.path]) {
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = @"Invalid Folder";
alert.informativeText = [NSString stringWithFormat:
@"The selected folder doesn't appear to be a valid %@ data directory. Please select the correct folder.",
browserName];
alert.alertStyle = NSAlertStyleWarning;
[alert runModal];
return nil;
}
// Access is temporary right now, persist it by creating a security bookmark
NSError *error = nil;
NSData *bookmarkData = [selectedURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
if (!bookmarkData) {
return nil;
}
[self saveBookmark:bookmarkData forBrowser:browserName];
return [bookmarkData base64EncodedStringWithOptions:0];
}
- (BOOL)hasStoredAccess:(NSString *)browserName {
return [self loadBookmarkForBrowser:browserName] != nil;
}
- (NSString *)startAccessingBrowser:(NSString *)browserName {
NSData *bookmarkData = [self loadBookmarkForBrowser:browserName];
if (!bookmarkData) {
return nil;
}
BOOL isStale = NO;
NSError *error = nil;
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmarkData
options:NSURLBookmarkResolutionWithSecurityScope
relativeToURL:nil
bookmarkDataIsStale:&isStale
error:&error];
if (!url) {
return nil;
}
if (isStale) {
NSData *newBookmarkData = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
if (!newBookmarkData) {
return nil;
}
[self saveBookmark:newBookmarkData forBrowser:browserName];
}
if (![url startAccessingSecurityScopedResource]) {
return nil;
}
return url.path;
}
- (void)stopAccessingBrowser:(NSString *)browserName {
NSData *bookmarkData = [self loadBookmarkForBrowser:browserName];
if (!bookmarkData) {
return;
}
BOOL isStale = NO;
NSError *error = nil;
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmarkData
options:NSURLBookmarkResolutionWithSecurityScope
relativeToURL:nil
bookmarkDataIsStale:&isStale
error:&error];
if (!url) {
return;
}
[url stopAccessingSecurityScopedResource];
}
#pragma mark - Private Methods
- (NSString *)bookmarkKeyFor:(NSString *)browserName {
return [NSString stringWithFormat:@"%@.%@", _bookmarkKey, browserName];
}
- (void)saveBookmark:(NSData *)data forBrowser:(NSString *)browserName {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *key = [self bookmarkKeyFor:browserName];
[defaults setObject:data forKey:key];
[defaults synchronize];
}
- (NSData *)loadBookmarkForBrowser:(NSString *)browserName {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *key = [self bookmarkKeyFor:browserName];
return [defaults dataForKey:key];
}
@end

View File

@ -0,0 +1,8 @@
#ifndef HAS_STORED_ACCESS_H
#define HAS_STORED_ACCESS_H
#import <Foundation/Foundation.h>
void hasStoredAccessCommand(void *context, NSDictionary *params);
#endif

View File

@ -0,0 +1,17 @@
#import <Foundation/Foundation.h>
#import "../../interop.h"
#import "../browser_access_manager.h"
#import "has_stored_access.h"
void hasStoredAccessCommand(void* context, NSDictionary *params) {
NSString *browserName = params[@"browserName"];
if (!browserName) {
return _return(context, _error(@"Missing required parameter: browserName"));
}
BrowserAccessManager *manager = [[BrowserAccessManager alloc] init];
BOOL hasAccess = [manager hasStoredAccess:browserName];
_return(context, _success(@{@"hasAccess": @(hasAccess)}));
}

View File

@ -0,0 +1,8 @@
#ifndef REQUEST_ACCESS_H
#define REQUEST_ACCESS_H
#import <Foundation/Foundation.h>
void requestAccessCommand(void *context, NSDictionary *params);
#endif

View File

@ -0,0 +1,22 @@
#import <Foundation/Foundation.h>
#import "../../interop.h"
#import "../browser_access_manager.h"
#import "request_access.h"
void requestAccessCommand(void* context, NSDictionary *params) {
NSString *browserName = params[@"browserName"];
NSString *relativePath = params[@"relativePath"];
if (!browserName || !relativePath) {
return _return(context, _error(@"Missing required parameters: browserName and relativePath"));
}
BrowserAccessManager *manager = [[BrowserAccessManager alloc] init];
NSString *bookmarkData = [manager requestAccessToBrowserDir:browserName relativePath:relativePath];
if (bookmarkData == nil) {
return _return(context, _error(@"User denied access or selected an invalid browser directory"));
}
_return(context, _success(@{@"bookmark": bookmarkData}));
}

View File

@ -0,0 +1,8 @@
#ifndef START_ACCESS_H
#define START_ACCESS_H
#import <Foundation/Foundation.h>
void startAccessCommand(void *context, NSDictionary *params);
#endif

View File

@ -0,0 +1,21 @@
#import <Foundation/Foundation.h>
#import "../../interop.h"
#import "../browser_access_manager.h"
#import "start_access.h"
void startAccessCommand(void* context, NSDictionary *params) {
NSString *browserName = params[@"browserName"];
if (!browserName) {
return _return(context, _error(@"Missing required parameter: browserName"));
}
BrowserAccessManager *manager = [[BrowserAccessManager alloc] init];
NSString *resolvedPath = [manager startAccessingBrowser:browserName];
if (resolvedPath == nil) {
return _return(context, _error(@"Failed to start accessing browser. Bookmark may be invalid or revoked"));
}
_return(context, _success(@{@"path": resolvedPath}));
}

View File

@ -0,0 +1,8 @@
#ifndef STOP_ACCESS_H
#define STOP_ACCESS_H
#import <Foundation/Foundation.h>
void stopAccessCommand(void *context, NSDictionary *params);
#endif

View File

@ -0,0 +1,17 @@
#import <Foundation/Foundation.h>
#import "../../interop.h"
#import "../browser_access_manager.h"
#import "stop_access.h"
void stopAccessCommand(void* context, NSDictionary *params) {
NSString *browserName = params[@"browserName"];
if (!browserName) {
return _return(context, _error(@"Missing required parameter: browserName"));
}
BrowserAccessManager *manager = [[BrowserAccessManager alloc] init];
[manager stopAccessingBrowser:browserName];
_return(context, _success(@{}));
}

View File

@ -0,0 +1,8 @@
#ifndef RUN_CHROMIUM_COMMAND_H
#define RUN_CHROMIUM_COMMAND_H
#import <Foundation/Foundation.h>
void runChromiumCommand(void* context, NSDictionary *input);
#endif

View File

@ -0,0 +1,25 @@
#import <Foundation/Foundation.h>
#import "commands/request_access.h"
#import "commands/has_stored_access.h"
#import "commands/start_access.h"
#import "commands/stop_access.h"
#import "../interop.h"
#import "../utils.h"
#import "run_chromium_command.h"
void runChromiumCommand(void* context, NSDictionary *input) {
NSString *command = input[@"command"];
NSDictionary *params = input[@"params"];
if ([command isEqual:@"request_access"]) {
return requestAccessCommand(context, params);
} else if ([command isEqual:@"has_stored_access"]) {
return hasStoredAccessCommand(context, params);
} else if ([command isEqual:@"start_access"]) {
return startAccessCommand(context, params);
} else if ([command isEqual:@"stop_access"]) {
return stopAccessCommand(context, params);
}
_return(context, _error([NSString stringWithFormat:@"Unknown command: %@", command]));
}

View File

@ -1,5 +1,6 @@
#import <Foundation/Foundation.h>
#import "autofill/run_autofill_command.h"
#import "chromium_importer/run_chromium_command.h"
#import "interop.h"
#import "utils.h"
@ -8,6 +9,8 @@ void pickAndRunCommand(void* context, NSDictionary *input) {
if ([namespace isEqual:@"autofill"]) {
return runAutofillCommand(context, input);
} else if ([namespace isEqual:@"chromium_importer"]) {
return runChromiumCommand(context, input);
}
_return(context, _error([NSString stringWithFormat:@"Unknown namespace: %@", namespace]));

View File

@ -2,20 +2,31 @@ import { ipcMain } from "electron";
import { chromium_importer } from "@bitwarden/desktop-napi";
import { isMacAppStore } from "../../../utils";
export class ChromiumImporterService {
constructor() {
ipcMain.handle("chromium_importer.getMetadata", async (event) => {
return await chromium_importer.getMetadata();
ipcMain.handle("chromium_importer.getMetadata", async (event, isMas: boolean) => {
return await chromium_importer.getMetadata(isMas);
});
// Used on Mac OS App Store builds to request permissions to browser entries outside the sandbox
ipcMain.handle("chromium_importer.requestBrowserAccess", async (event, browser: string) => {
if (chromium_importer.requestBrowserAccess) {
return await chromium_importer.requestBrowserAccess(browser, isMacAppStore());
}
// requestBrowserAccess not found, returning with no-op
return;
});
ipcMain.handle("chromium_importer.getAvailableProfiles", async (event, browser: string) => {
return await chromium_importer.getAvailableProfiles(browser);
return await chromium_importer.getAvailableProfiles(browser, isMacAppStore());
});
ipcMain.handle(
"chromium_importer.importLogins",
async (event, browser: string, profileId: string) => {
return await chromium_importer.importLogins(browser, profileId);
return await chromium_importer.importLogins(browser, profileId, isMacAppStore());
},
);
}

View File

@ -20,7 +20,8 @@ export class DesktopImportMetadataService
}
async init(): Promise<void> {
const metadata = await ipc.tools.chromiumImporter.getMetadata();
const isMas = ipc.platform.isMacAppStore;
const metadata = await ipc.tools.chromiumImporter.getMetadata(isMas);
await this.parseNativeMetaData(metadata);
await super.init();
}

View File

@ -1,13 +1,19 @@
<bit-dialog #dialog dialogSize="large" background="alt">
<span bitDialogTitle>{{ "importData" | i18n }}</span>
<ng-container bitDialogContent>
<tools-import
(formLoading)="this.loading = $event"
(formDisabled)="this.disabled = $event"
(onSuccessfulImport)="this.onSuccessfulImport($event)"
[onImportFromBrowser]="this.onImportFromBrowser"
[onLoadProfilesFromBrowser]="this.onLoadProfilesFromBrowser"
></tools-import>
<div class="tw-relative">
<tools-import
(formLoading)="this.loading = $event"
(formDisabled)="this.disabled = $event"
(onSuccessfulImport)="this.onSuccessfulImport($event)"
[onImportFromBrowser]="this.onImportFromBrowser"
[onLoadProfilesFromBrowser]="this.onLoadProfilesFromBrowser"
[class.tw-invisible]="loading"
></tools-import>
<div *ngIf="loading" class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center">
<i class="bwi bwi-spinner bwi-spin bwi-3x tw-text-primary-600" aria-hidden="true"></i>
</div>
</div>
</ng-container>
<ng-container bitDialogFooter>
<button

View File

@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogRef, AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
import type { chromium_importer } from "@bitwarden/desktop-napi";
import { ImportMetadataServiceAbstraction } from "@bitwarden/importer-core";
@ -39,7 +40,13 @@ export class ImportDesktopComponent {
protected disabled = false;
protected loading = false;
constructor(public dialogRef: DialogRef) {}
protected readonly onLoadProfilesFromBrowser = this._onLoadProfilesFromBrowser.bind(this);
protected readonly onImportFromBrowser = this._onImportFromBrowser.bind(this);
constructor(
public dialogRef: DialogRef,
private i18nService: I18nService,
) {}
/**
* Callback that is called after a successful import.
@ -48,11 +55,19 @@ export class ImportDesktopComponent {
this.dialogRef.close();
}
protected onLoadProfilesFromBrowser(browser: string): Promise<chromium_importer.ProfileInfo[]> {
private async _onLoadProfilesFromBrowser(
browser: string,
): Promise<chromium_importer.ProfileInfo[]> {
try {
// Request browser access (required for sandboxed builds, no-op otherwise)
await ipc.tools.chromiumImporter.requestBrowserAccess(browser);
} catch {
throw new Error(this.i18nService.t("browserAccessDenied"));
}
return ipc.tools.chromiumImporter.getAvailableProfiles(browser);
}
protected onImportFromBrowser(
private _onImportFromBrowser(
browser: string,
profile: string,
): Promise<chromium_importer.LoginImportResult[]> {

View File

@ -3,8 +3,13 @@ import { ipcRenderer } from "electron";
import type { chromium_importer } from "@bitwarden/desktop-napi";
const chromiumImporter = {
getMetadata: (): Promise<Record<string, chromium_importer.NativeImporterMetadata>> =>
ipcRenderer.invoke("chromium_importer.getMetadata"),
getMetadata: (
isMas: boolean,
): Promise<Record<string, chromium_importer.NativeImporterMetadata>> =>
ipcRenderer.invoke("chromium_importer.getMetadata", isMas),
// Request browser access for Mac OS App Store (sandboxed) builds (no-op in non-sandboxed builds)
requestBrowserAccess: (browser: string): Promise<void> =>
ipcRenderer.invoke("chromium_importer.requestBrowserAccess", browser),
getAvailableProfiles: (browser: string): Promise<chromium_importer.ProfileInfo[]> =>
ipcRenderer.invoke("chromium_importer.getAvailableProfiles", browser),
importLogins: (

View File

@ -4255,5 +4255,8 @@
},
"sessionTimeoutHeader": {
"message": "Session timeout"
},
"browserAccessDenied": {
"message": "Either an invalid browser directory was selected, or access was denied"
}
}

View File

@ -105,6 +105,10 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() csvDataLoaded = new EventEmitter<string>();
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() error = new EventEmitter<string>();
constructor(
private formBuilder: FormBuilder,
private controlContainer: ControlContainer,
@ -112,8 +116,20 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
private i18nService: I18nService,
) {
effect(async () => {
this.profileList = await this.onLoadProfilesFromBrowser(this.getBrowserName(this.format()));
// FIXME: Add error handling and display when profiles could not be loaded/retrieved
// Callback is set via @Input after constructor, so check it exists
if (this.onLoadProfilesFromBrowser) {
try {
this.profileList = await this.onLoadProfilesFromBrowser(
this.getBrowserName(this.format()),
);
} catch (error) {
this.logService.error("Error loading profiles from browser:", error);
const keyOrMessage = this.getValidationErrorI18nKey(error);
this.error.emit(
keyOrMessage === "errorOccurred" ? this.i18nService.t("errorOccurred") : keyOrMessage,
);
}
}
});
}
@ -169,9 +185,11 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
return null;
} catch (error) {
this.logService.error(`Chromium importer error: ${error}`);
const keyOrMessage = this.getValidationErrorI18nKey(error);
return {
errors: {
message: this.i18nService.t(this.getValidationErrorI18nKey(error)),
message:
keyOrMessage === "errorOccurred" ? this.i18nService.t("errorOccurred") : keyOrMessage,
},
};
}
@ -180,10 +198,7 @@ export class ImportChromeComponent implements OnInit, OnDestroy {
private getValidationErrorI18nKey(error: any): string {
const message = typeof error === "string" ? error : error?.message;
switch (message) {
default:
return "errorOccurred";
}
return message || "errorOccurred";
}
private getBrowserName(format: ImportType): string {