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:
@@ -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) {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
1
apps/desktop/desktop_native/napi/index.d.ts
vendored
1
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 doesn’t have access to this URL
|
||||
The URL isn’t a security scoped URL
|
||||
T The directory doesn’t 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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user