1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

chromium importer working in sandbox

This commit is contained in:
John Harrington
2025-11-19 15:22:23 -07:00
parent f40233fce4
commit aa42630410
13 changed files with 162 additions and 57 deletions

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

@@ -63,10 +63,8 @@ impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever {
for (browser, config) in SUPPORTED_BROWSER_MAP.iter() {
#[cfg(all(target_os = "macos", feature = "sandbox"))]
{
// macOS sandbox mode: check if we have stored security-scoped bookmark
if platform::ScopedBrowserAccess::has_stored_access(browser) {
browsers.push((*browser).to_string());
}
// macOS sandbox mode: show all browsers, user will grant access when selected
browsers.push((*browser).to_string());
}
#[cfg(not(all(target_os = "macos", feature = "sandbox")))]
@@ -89,9 +87,13 @@ pub fn get_available_profiles(browser_name: &String) -> Result<Vec<ProfileInfo>>
}
/// Request access to browser directory (sandbox mode only)
/// This shows the permission dialog and creates a security-scoped bookmark,
/// but does NOT start accessing the resource (that happens in resume()).
#[cfg(all(target_os = "macos", feature = "sandbox"))]
pub fn request_browser_access(browser_name: &String) -> Result<()> {
let _access = platform::ScopedBrowserAccess::request_and_start(browser_name)?;
eprintln!("[SANDBOX] request_browser_access called for: {}", browser_name);
platform::ScopedBrowserAccess::request_only(browser_name)?;
eprintln!("[SANDBOX] request_browser_access completed successfully");
Ok(())
}

View File

@@ -24,27 +24,21 @@ pub struct ScopedBrowserAccess {
}
impl ScopedBrowserAccess {
/// Request permission from user and start accessing browser directory
pub fn request_and_start(browser_name: &str) -> Result<Self> {
pub fn request_only(browser_name: &str) -> Result<()> {
let c_name = CString::new(browser_name)?;
// Request permission (shows dialog)
let bookmark_ptr = unsafe { requestBrowserAccess(c_name.as_ptr()) };
if bookmark_ptr.is_null() {
return Err(anyhow!("User declined access or browser not found"));
}
unsafe { libc::free(bookmark_ptr as *mut libc::c_void) };
// Start accessing
let path_ptr = unsafe { startBrowserAccess(c_name.as_ptr()) };
if path_ptr.is_null() {
return Err(anyhow!("Failed to start accessing browser directory"));
}
unsafe { libc::free(path_ptr as *mut libc::c_void) };
Ok(())
}
Ok(Self {
browser_name: browser_name.to_string(),
})
pub fn request_and_start(browser_name: &str) -> Result<Self> {
Self::request_only(browser_name)?;
Self::resume(browser_name)
}
/// Resume access using previously stored bookmark
@@ -66,7 +60,6 @@ impl ScopedBrowserAccess {
})
}
/// Check if we have stored access (doesn't verify validity)
pub fn has_stored_access(browser_name: &str) -> bool {
let Ok(c_name) = CString::new(browser_name) else {
return false;

View File

@@ -249,6 +249,7 @@ export declare namespace chromium_importer {
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 requestBrowserAccess(browser: string): void
}
export declare namespace autotype {
export function getForegroundWindowTitle(): string

View File

@@ -11,7 +11,12 @@ if (isRelease) {
process.env.RUST_LOG = 'debug';
}
execSync(`napi build --platform --js false`, { stdio: 'inherit', env: process.env });
const featuresArg = process.env.SANDBOX_BUILD === '1' ? '--features sandbox' : '';
if (featuresArg) {
console.log('Building with sandbox feature enabled.');
}
execSync(`napi build --platform --js false ${featuresArg}`, { stdio: 'inherit', env: process.env });
/* Mac App Store build with sandboxing - Does this belong here?

View File

@@ -1182,11 +1182,18 @@ pub mod chromium_importer {
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[cfg_attr(all(target_os = "macos", feature = "sandbox"), napi)]
#[cfg(all(target_os = "macos", feature = "sandbox"))]
#[napi]
pub fn request_browser_access(browser: String) -> napi::Result<()> {
chromium_importer::chromium::request_browser_access(&browser)
.map_err(|e| napi::Error::from_reason(e.to_string()))
#[cfg(all(target_os = "macos", feature = "sandbox"))]
{
chromium_importer::chromium::request_browser_access(&browser)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[cfg(not(all(target_os = "macos", feature = "sandbox")))]
{
// No-op when built without sandbox feature
Ok(())
}
}
}

View File

@@ -17,33 +17,67 @@ import Foundation
/// Request access to a specific browser's directory
/// Returns security bookmark data (used to persist permissions) as base64 string, or nil if user declined
@objc public func requestAccessToBroswerDir(_ browserName: String) -> String? {
NSLog("[SWIFT] requestAccessToBroswerDir called for: \(browserName)")
guard let relativePath = browserPaths[browserName] else {
NSLog("Unknown browser: \(browserName)")
NSLog("[SWIFT] Unknown browser: \(browserName)")
return nil
}
let homeDir = FileManager.default.homeDirectoryForCurrentUser
let browserPath = homeDir.appendingPathComponent(relativePath)
NSLog("[SWIFT] Browser path: \(browserPath.path)")
// Open file picker at home directory and provide instructions to grant access to browserPath
// Mac OS will automatically request permission to access this location from the sandbox when the location is selected here
let openPanel = NSOpenPanel()
openPanel.message =
"Please select your \(browserName) data folder\n\nExpected location:\n\(browserPath.path)"
openPanel.prompt = "Grant Access"
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canChooseFiles = false
openPanel.directoryURL = browserPath.deletingLastPathComponent() // home directory
// NSOpenPanel must be run on the main thread
var selectedURL: URL?
var panelResult: NSApplication.ModalResponse = .cancel
guard openPanel.runModal() == .OK, let url = openPanel.url else {
NSLog("User cancelled access request")
if Thread.isMainThread {
NSLog("[SWIFT] Already on main thread")
let openPanel = NSOpenPanel()
openPanel.message =
"Please select your \(browserName) data folder\n\nExpected location:\n\(browserPath.path)"
openPanel.prompt = "Grant Access"
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canChooseFiles = false
openPanel.directoryURL = browserPath.deletingLastPathComponent()
NSLog("[SWIFT] About to call openPanel.runModal()")
panelResult = openPanel.runModal()
selectedURL = openPanel.url
NSLog("[SWIFT] runModal returned: \(panelResult.rawValue)")
} else {
NSLog("[SWIFT] Dispatching to main queue...")
DispatchQueue.main.sync {
NSLog("[SWIFT] Inside main queue dispatch block")
let openPanel = NSOpenPanel()
openPanel.message =
"Please select your \(browserName) data folder\n\nExpected location:\n\(browserPath.path)"
openPanel.prompt = "Grant Access"
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canChooseFiles = false
openPanel.directoryURL = browserPath.deletingLastPathComponent()
NSLog("[SWIFT] About to call openPanel.runModal()")
panelResult = openPanel.runModal()
selectedURL = openPanel.url
NSLog("[SWIFT] runModal returned: \(panelResult.rawValue)")
}
}
guard panelResult == .OK, let url = selectedURL else {
NSLog("[SWIFT] User cancelled access request or panel failed")
return nil
}
NSLog("[SWIFT] User selected URL: \(url.path)")
let localStatePath = url.appendingPathComponent("Local State")
guard FileManager.default.fileExists(atPath: localStatePath.path) else {
NSLog("Selected folder doesn't appear to be a valid \(browserName) directory")
NSLog("[SWIFT] Selected folder doesn't appear to be a valid \(browserName) directory")
let alert = NSAlert()
alert.messageText = "Invalid Folder"
@@ -64,9 +98,10 @@ import Foundation
)
saveBookmark(bookmarkData, forBrowser: browserName)
NSLog("[SWIFT] Successfully created and saved bookmark")
return bookmarkData.base64EncodedString()
} catch {
NSLog("Failed to create bookmark: \(error)")
NSLog("[SWIFT] Failed to create bookmark: \(error)")
return nil
}
}
@@ -78,14 +113,6 @@ import Foundation
/// Start accessing a browser directory using stored bookmark
/// Returns the resolved path, or nil if bookmark is invalid/revoked
/*
This could return nil if:
The user doesnt have access to this URL
The URL isnt a security scoped URL
T The directory doesnt need it (~/Downloads)
https://benscheirman.com/2019/10/troubleshooting-appkit-file-permissions.html
*/
@objc public func startAccessingBrowser(_ browserName: String) -> String? {
guard let bookmarkData = loadBookmark(forBrowser: browserName) else {
return nil

View File

@@ -35,6 +35,7 @@
"build:renderer:watch": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name renderer --watch",
"electron": "node ./scripts/start.js",
"electron:ignore": "node ./scripts/start.js --ignore-certificate-errors",
"electron:sandbox": "SANDBOX_BUILD=1 node ./scripts/start.js",
"flatpak:dev": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --force-clean --install --user ../../.flatpak/ ./resources/com.bitwarden.desktop.devel.yaml && flatpak run com.bitwarden.desktop",
"clean:dist": "rimraf ./dist",
"pack:dir": "npm run clean:dist && electron-builder --dir -p never",

View File

@@ -25,7 +25,7 @@ concurrently(
},
{
name: "Elec",
command: `npx wait-on ./build/main.js && npx electron --no-sandbox --inspect=5858 ${args.join(
command: `npx wait-on ./build/main.js && npx electron ${process.env.SANDBOX_BUILD ? "" : "--no-sandbox "}--inspect=5858 ${args.join(
" ",
)} ./build --watch`,
prefixColor: "green",

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import { ipcMain } from "electron";
import { chromium_importer } from "@bitwarden/desktop-napi";
@@ -8,6 +9,23 @@ export class ChromiumImporterService {
return await chromium_importer.getMetadata();
});
ipcMain.handle("chromium_importer.requestBrowserAccess", async (event, browser: string) => {
console.log("[IPC] requestBrowserAccess handler called for:", browser);
console.log("[IPC] chromium_importer keys:", Object.keys(chromium_importer));
console.log(
"[IPC] requestBrowserAccess exists?",
typeof chromium_importer.requestBrowserAccess,
);
if (chromium_importer.requestBrowserAccess) {
console.log("[IPC] Calling native requestBrowserAccess");
return await chromium_importer.requestBrowserAccess(browser);
}
// No-op if not compiled with sandbox support
console.log("[IPC] requestBrowserAccess not found, returning no-op");
return;
});
ipcMain.handle("chromium_importer.getAvailableProfiles", async (event, browser: string) => {
return await chromium_importer.getAvailableProfiles(browser);
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
@@ -39,6 +40,10 @@ export class ImportDesktopComponent {
protected disabled = false;
protected loading = false;
// Bind callbacks in constructor to maintain reference equality
protected readonly onLoadProfilesFromBrowser = this._onLoadProfilesFromBrowser.bind(this);
protected readonly onImportFromBrowser = this._onImportFromBrowser.bind(this);
constructor(public dialogRef: DialogRef) {}
/**
@@ -48,11 +53,24 @@ export class ImportDesktopComponent {
this.dialogRef.close();
}
protected onLoadProfilesFromBrowser(browser: string): Promise<chromium_importer.ProfileInfo[]> {
private async _onLoadProfilesFromBrowser(
browser: string,
): Promise<chromium_importer.ProfileInfo[]> {
console.log("[SANDBOX] onLoadProfilesFromBrowser called for:", browser);
// Request browser access (required for sandboxed builds, no-op otherwise)
try {
console.log("[SANDBOX] Calling requestBrowserAccess...");
await ipc.tools.chromiumImporter.requestBrowserAccess(browser);
console.log("[SANDBOX] requestBrowserAccess completed successfully");
} catch (error) {
console.error("[SANDBOX] requestBrowserAccess failed:", error);
throw error;
}
console.log("[SANDBOX] Calling getAvailableProfiles...");
return ipc.tools.chromiumImporter.getAvailableProfiles(browser);
}
protected onImportFromBrowser(
private _onImportFromBrowser(
browser: string,
profile: string,
): Promise<chromium_importer.LoginImportResult[]> {

View File

@@ -5,6 +5,9 @@ import type { chromium_importer } from "@bitwarden/desktop-napi";
const chromiumImporter = {
getMetadata: (): Promise<Record<string, chromium_importer.NativeImporterMetadata>> =>
ipcRenderer.invoke("chromium_importer.getMetadata"),
// Request browser access for 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

@@ -42,14 +42,40 @@ function positionWindow(window: BrowserWindow, position?: Position) {
export function applyMainWindowStyles(window: BrowserWindow, existingWindowState: WindowState) {
window.setMinimumSize(680, 500);
// need to guard against null/undefined values
// need to guard against null/undefined values and ensure values are valid
if (existingWindowState) {
if (
typeof existingWindowState.width === "number" &&
typeof existingWindowState.height === "number" &&
Number.isFinite(existingWindowState.width) &&
Number.isFinite(existingWindowState.height) &&
existingWindowState.width > 0 &&
existingWindowState.height > 0
) {
try {
// Ensure values are integers as Electron expects integer pixel values
window.setSize(
Math.round(existingWindowState.width),
Math.round(existingWindowState.height),
);
} catch {
// Silently fail - window will use default size
}
}
if (existingWindowState?.width && existingWindowState?.height) {
window.setSize(existingWindowState.width, existingWindowState.height);
}
if (existingWindowState?.x && existingWindowState?.y) {
window.setPosition(existingWindowState.x, existingWindowState.y);
if (
typeof existingWindowState.x === "number" &&
typeof existingWindowState.y === "number" &&
Number.isFinite(existingWindowState.x) &&
Number.isFinite(existingWindowState.y)
) {
try {
// Ensure values are integers as Electron expects integer pixel values
window.setPosition(Math.round(existingWindowState.x), Math.round(existingWindowState.y));
} catch {
// Silently fail - window will use default position
}
}
}
window.setWindowButtonVisibility?.(true);