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

adapt chromium_importer to match conventions established by autofill:

• eliminate extern c and unsafe rust from chromium_importer logic (still called in prior napi/lib.rs)
• use CommandInput / CommandResult JSON pattern to call objc methods
• remove usage of libc to cleanup memory and remove this dependency
This commit is contained in:
John Harrington
2025-12-04 11:02:19 -07:00
parent 7beed6eb1e
commit 16a0aad849
19 changed files with 270 additions and 129 deletions

View File

@@ -610,9 +610,9 @@ dependencies = [
"async-trait",
"base64",
"cbc",
"desktop_objc",
"dirs",
"hex",
"libc",
"oo7",
"pbkdf2",
"rand 0.9.1",

View File

@@ -18,10 +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 }
libc = { workspace = true }
sha1 = "=0.10.6"
tokio = { workspace = true, features = ["rt"] }
[target.'cfg(target_os = "windows")'.dependencies]
aes-gcm = { workspace = true }

View File

@@ -84,9 +84,9 @@ pub fn get_available_profiles(browser_name: &str) -> Result<Vec<ProfileInfo>> {
/// Request access to browser directory (MAS builds only)
/// This shows the permission dialog and creates a security-scoped bookmark
#[cfg(target_os = "macos")]
pub fn request_browser_access(browser_name: &str, mas_build: bool) -> Result<()> {
pub async fn request_browser_access(browser_name: &str, mas_build: bool) -> Result<()> {
if mas_build {
platform::sandbox::ScopedBrowserAccess::request_only(browser_name)?;
platform::sandbox::ScopedBrowserAccess::request_only(browser_name).await?;
}
Ok(())
}
@@ -101,7 +101,7 @@ pub async fn import_logins(
let _access = if _mas_build {
Some(platform::sandbox::ScopedBrowserAccess::resume(
browser_name,
)?)
).await?)
} else {
None
};

View File

@@ -12,18 +12,41 @@ use crate::{
//
pub mod sandbox {
use std::{ffi::CString, os::raw::c_char};
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
extern "C" {
fn requestBrowserAccess(
browser_name: *const c_char,
relative_path: *const c_char,
) -> *mut c_char;
fn hasStoredBrowserAccess(browser_name: *const c_char) -> bool;
fn startBrowserAccess(browser_name: *const c_char) -> *mut c_char;
fn stopBrowserAccess(browser_name: *const c_char);
#[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 {
@@ -32,52 +55,105 @@ pub mod sandbox {
impl ScopedBrowserAccess {
/// Request access to browser directory and create a security bookmark if access is approved
pub fn request_only(browser_name: &str) -> Result<()> {
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))?;
let c_name = CString::new(browser_name)?;
let c_path = CString::new(config.data_dir)?;
let input = CommandInput {
namespace: "chromium_importer".to_string(),
command: "request_access".to_string(),
params: serde_json::json!({
"browserName": browser_name,
"relativePath": config.data_dir,
}),
};
let bookmark_ptr = unsafe { requestBrowserAccess(c_name.as_ptr(), c_path.as_ptr()) };
if bookmark_ptr.is_null() {
return Err(anyhow!(
"User denied access or selected an invalid browser directory"
));
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)),
}
unsafe { libc::free(bookmark_ptr as *mut libc::c_void) };
Ok(())
}
/// Resume browser directory access using previously created security bookmark
pub fn resume(browser_name: &str) -> Result<Self> {
let c_name = CString::new(browser_name)?;
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,
}),
};
if !unsafe { hasStoredBrowserAccess(c_name.as_ptr()) } {
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"));
}
let path_ptr = unsafe { startBrowserAccess(c_name.as_ptr()) };
if path_ptr.is_null() {
return Err(anyhow!("Existing security scoped access has become stale"));
}
unsafe { libc::free(path_ptr as *mut libc::c_void) };
// 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,
}),
};
Ok(Self {
browser_name: browser_name.to_string(),
})
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 Ok(c_name) = CString::new(self.browser_name.as_str()) else {
return;
};
unsafe { stopBrowserAccess(c_name.as_ptr()) };
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;
}
});
}
}
}

View File

@@ -256,7 +256,7 @@ export declare namespace chromium_importer {
export function getMetadata(masBuild: boolean): Record<string, NativeImporterMetadata>
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
export function importLogins(browser: string, profileId: string, masBuild: boolean): Promise<Array<LoginImportResult>>
export function requestBrowserAccess(browser: string, masBuild: boolean): void
export function requestBrowserAccess(browser: string, masBuild: boolean): Promise<void>
}
export declare namespace autotype {
export function getForegroundWindowTitle(): string

View File

@@ -1195,17 +1195,14 @@ pub mod chromium_importer {
}
#[napi]
pub fn request_browser_access(_browser: String, _mas_build: bool) -> napi::Result<()> {
pub async fn request_browser_access(_browser: String, _mas_build: bool) -> napi::Result<()> {
#[cfg(target_os = "macos")]
{
chromium_importer::chromium::request_browser_access(&_browser, _mas_build)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
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"))]
{
// No-op outside of Mac OS
Ok(())
}
Ok(())
}
}

View File

@@ -1,22 +0,0 @@
#ifndef BROWSER_ACCESS_H
#define BROWSER_ACCESS_H
#include <stdbool.h>
// Request user permission to access browser directory
// Returns base64-encoded bookmark data, or NULL if declined
// Caller must free returned string
char* requestBrowserAccess(const char* browserName, const char* relativePath);
// Check if we have stored bookmark (doesn't verify validity)
bool hasStoredBrowserAccess(const char* browserName);
// Start accessing browser using stored bookmark
// Returns resolved path, or NULL if bookmark invalid
// Caller must free returned string and call stopBrowserAccess when done
char* startBrowserAccess(const char* browserName);
// Stop accessing browser (MUST be called after startBrowserAccess)
void stopBrowserAccess(const char* browserName);
#endif

View File

@@ -1,56 +0,0 @@
#import <Foundation/Foundation.h>
#import "browser_access.h"
#import "../utils.h"
#import "browser_access_manager.h"
static BrowserAccessManager* sharedManager = nil;
static BrowserAccessManager* getManager() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[BrowserAccessManager alloc] init];
});
return sharedManager;
}
char* requestBrowserAccess(const char* browserName, const char* relativePath) {
@autoreleasepool {
NSString* name = [NSString stringWithUTF8String:browserName];
NSString* path = [NSString stringWithUTF8String:relativePath];
NSString* result = [getManager() requestAccessToBrowserDir:name relativePath:path];
if (result == nil) {
return NULL;
}
return strdup([result UTF8String]);
}
}
bool hasStoredBrowserAccess(const char* browserName) {
@autoreleasepool {
NSString* name = [NSString stringWithUTF8String:browserName];
return [getManager() hasStoredAccess:name];
}
}
char* startBrowserAccess(const char* browserName) {
@autoreleasepool {
NSString* name = [NSString stringWithUTF8String:browserName];
NSString* result = [getManager() startAccessingBrowser:name];
if (result == nil) {
return NULL;
}
return strdup([result UTF8String]);
}
}
void stopBrowserAccess(const char* browserName) {
@autoreleasepool {
NSString* name = [NSString stringWithUTF8String:browserName];
[getManager() stopAccessingBrowser:name];
}
}

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]));