mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
initial sandbox implementation for mac app store builds
This commit is contained in:
1
apps/desktop/desktop_native/Cargo.lock
generated
1
apps/desktop/desktop_native/Cargo.lock
generated
@@ -610,6 +610,7 @@ dependencies = [
|
|||||||
"cbc",
|
"cbc",
|
||||||
"dirs",
|
"dirs",
|
||||||
"hex",
|
"hex",
|
||||||
|
"libc",
|
||||||
"oo7",
|
"oo7",
|
||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
"rand 0.9.1",
|
"rand 0.9.1",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ tracing = { workspace = true }
|
|||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
security-framework = { workspace = true }
|
security-framework = { workspace = true }
|
||||||
|
libc = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
windows = { workspace = true, features = [
|
windows = { workspace = true, features = [
|
||||||
@@ -39,3 +40,6 @@ oo7 = { workspace = true }
|
|||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
sandbox = []
|
||||||
|
|||||||
@@ -59,12 +59,25 @@ impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever {
|
|||||||
fn get_installed_browsers() -> Result<Vec<String>> {
|
fn get_installed_browsers() -> Result<Vec<String>> {
|
||||||
let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len());
|
let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len());
|
||||||
|
|
||||||
|
#[allow(unused_variables)] // config only used in non-sandbox mode
|
||||||
for (browser, config) in SUPPORTED_BROWSER_MAP.iter() {
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(all(target_os = "macos", feature = "sandbox")))]
|
||||||
|
{
|
||||||
|
// All other platforms OR macOS without sandbox: check file system directly
|
||||||
let data_dir = get_browser_data_dir(config)?;
|
let data_dir = get_browser_data_dir(config)?;
|
||||||
if data_dir.exists() {
|
if data_dir.exists() {
|
||||||
browsers.push((*browser).to_string());
|
browsers.push((*browser).to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(browsers)
|
Ok(browsers)
|
||||||
}
|
}
|
||||||
@@ -75,10 +88,21 @@ pub fn get_available_profiles(browser_name: &String) -> Result<Vec<ProfileInfo>>
|
|||||||
Ok(get_profile_info(&local_state))
|
Ok(get_profile_info(&local_state))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Request access to browser directory (sandbox mode only)
|
||||||
|
#[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)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn import_logins(
|
pub async fn import_logins(
|
||||||
browser_name: &String,
|
browser_name: &String,
|
||||||
profile_id: &String,
|
profile_id: &String,
|
||||||
) -> Result<Vec<LoginImportResult>> {
|
) -> Result<Vec<LoginImportResult>> {
|
||||||
|
// In sandbox mode, resume access to browser directory
|
||||||
|
#[cfg(all(target_os = "macos", feature = "sandbox"))]
|
||||||
|
let _access = platform::ScopedBrowserAccess::resume(browser_name)?;
|
||||||
|
|
||||||
let (data_dir, local_state) = load_local_state_for_browser(browser_name)?;
|
let (data_dir, local_state) = load_local_state_for_browser(browser_name)?;
|
||||||
|
|
||||||
let mut crypto_service = platform::get_crypto_service(browser_name, &local_state)
|
let mut crypto_service = platform::get_crypto_service(browser_name, &local_state)
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use security_framework::passwords::get_generic_password;
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::os::raw::c_char;
|
||||||
|
|
||||||
|
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Sandbox
|
||||||
|
//
|
||||||
|
|
||||||
|
// FFI declarations
|
||||||
|
extern "C" {
|
||||||
|
fn requestBrowserAccess(browser_name: *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);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScopedBrowserAccess {
|
||||||
|
browser_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScopedBrowserAccess {
|
||||||
|
/// Request permission from user and start accessing browser directory
|
||||||
|
pub fn request_and_start(browser_name: &str) -> Result<Self> {
|
||||||
|
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(Self {
|
||||||
|
browser_name: browser_name.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resume access using previously stored bookmark
|
||||||
|
pub fn resume(browser_name: &str) -> Result<Self> {
|
||||||
|
let c_name = CString::new(browser_name)?;
|
||||||
|
|
||||||
|
if !unsafe { hasStoredBrowserAccess(c_name.as_ptr()) } {
|
||||||
|
return Err(anyhow!("No stored access for this browser"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_ptr = unsafe { startBrowserAccess(c_name.as_ptr()) };
|
||||||
|
if path_ptr.is_null() {
|
||||||
|
return Err(anyhow!("Failed to resume access (bookmark may be stale)"));
|
||||||
|
}
|
||||||
|
unsafe { libc::free(path_ptr as *mut libc::c_void) };
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
browser_name: browser_name.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
};
|
||||||
|
unsafe { hasStoredBrowserAccess(c_name.as_ptr()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Existing Public API
|
||||||
|
// TODO the rest of this file exactly matches macos.rs, move sandbox code there and cfg gate it behind sandbox feature
|
||||||
|
//
|
||||||
|
|
||||||
|
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Chrome",
|
||||||
|
data_dir: "Library/Application Support/Google/Chrome",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Chromium",
|
||||||
|
data_dir: "Library/Application Support/Chromium",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Microsoft Edge",
|
||||||
|
data_dir: "Library/Application Support/Microsoft Edge",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Brave",
|
||||||
|
data_dir: "Library/Application Support/BraveSoftware/Brave-Browser",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Arc",
|
||||||
|
data_dir: "Library/Application Support/Arc/User Data",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Opera",
|
||||||
|
data_dir: "Library/Application Support/com.operasoftware.Opera",
|
||||||
|
},
|
||||||
|
BrowserConfig {
|
||||||
|
name: "Vivaldi",
|
||||||
|
data_dir: "Library/Application Support/Vivaldi",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) fn get_crypto_service(
|
||||||
|
browser_name: &String,
|
||||||
|
_local_state: &LocalState,
|
||||||
|
) -> Result<Box<dyn CryptoService>> {
|
||||||
|
let config = KEYCHAIN_CONFIG
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.browser == browser_name)
|
||||||
|
.ok_or_else(|| anyhow!("Unsupported browser: {}", browser_name))?;
|
||||||
|
|
||||||
|
Ok(Box::new(MacCryptoService::new(config)))
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Private
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct KeychainConfig {
|
||||||
|
browser: &'static str,
|
||||||
|
service: &'static str,
|
||||||
|
account: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYCHAIN_CONFIG: [KeychainConfig; SUPPORTED_BROWSERS.len()] = [
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Chrome",
|
||||||
|
service: "Chrome Safe Storage",
|
||||||
|
account: "Chrome",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Chromium",
|
||||||
|
service: "Chromium Safe Storage",
|
||||||
|
account: "Chromium",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Microsoft Edge",
|
||||||
|
service: "Microsoft Edge Safe Storage",
|
||||||
|
account: "Microsoft Edge",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Brave",
|
||||||
|
service: "Brave Safe Storage",
|
||||||
|
account: "Brave",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Arc",
|
||||||
|
service: "Arc Safe Storage",
|
||||||
|
account: "Arc",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Opera",
|
||||||
|
service: "Opera Safe Storage",
|
||||||
|
account: "Opera",
|
||||||
|
},
|
||||||
|
KeychainConfig {
|
||||||
|
browser: "Vivaldi",
|
||||||
|
service: "Vivaldi Safe Storage",
|
||||||
|
account: "Vivaldi",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const IV: [u8; 16] = [0x20; 16]; // 16 bytes of 0x20 (space character)
|
||||||
|
|
||||||
|
//
|
||||||
|
// CryptoService
|
||||||
|
//
|
||||||
|
|
||||||
|
struct MacCryptoService {
|
||||||
|
config: &'static KeychainConfig,
|
||||||
|
master_key: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MacCryptoService {
|
||||||
|
fn new(config: &'static KeychainConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
master_key: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CryptoService for MacCryptoService {
|
||||||
|
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String> {
|
||||||
|
if encrypted.is_empty() {
|
||||||
|
return Ok(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// On macOS only v10 is supported
|
||||||
|
let (_, no_prefix) = util::split_encrypted_string_and_validate(encrypted, &["v10"])?;
|
||||||
|
|
||||||
|
// This might bring up the admin password prompt
|
||||||
|
if self.master_key.is_none() {
|
||||||
|
self.master_key = Some(get_master_key(self.config.service, self.config.account)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = self
|
||||||
|
.master_key
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
|
||||||
|
let plaintext = util::decrypt_aes_128_cbc(key, &IV, no_prefix)
|
||||||
|
.map_err(|e| anyhow!("Failed to decrypt: {}", e))?;
|
||||||
|
let plaintext =
|
||||||
|
String::from_utf8(plaintext).map_err(|e| anyhow!("Invalid UTF-8: {}", e))?;
|
||||||
|
|
||||||
|
Ok(plaintext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_master_key(service: &str, account: &str) -> Result<Vec<u8>> {
|
||||||
|
let master_password = get_master_password(service, account)?;
|
||||||
|
let key = util::derive_saltysalt(&master_password, 1003)?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_master_password(service: &str, account: &str) -> Result<Vec<u8>> {
|
||||||
|
let password = get_generic_password(service, account)
|
||||||
|
.map_err(|e| anyhow!("Failed to get password from keychain: {}", e))?;
|
||||||
|
|
||||||
|
Ok(password)
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
// Platform-specific code
|
// Platform-specific code
|
||||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||||
#[cfg_attr(target_os = "windows", path = "windows/mod.rs")]
|
#[cfg_attr(target_os = "windows", path = "windows/mod.rs")]
|
||||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
#[cfg_attr(all(target_os = "macos", not(feature = "sandbox")), path = "macos.rs")]
|
||||||
|
|
||||||
|
// TODO rework this to place sandbox code in macos and cfg gate it there
|
||||||
|
#[cfg_attr(all(target_os = "macos", feature = "sandbox"), path = "macos_sandbox.rs")]
|
||||||
|
|
||||||
mod native;
|
mod native;
|
||||||
|
|
||||||
// Windows exposes public const
|
// Windows exposes public const
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ crate-type = ["cdylib"]
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
manual_test = []
|
manual_test = []
|
||||||
|
sandbox = ["chromium_importer/sandbox"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|||||||
@@ -12,3 +12,12 @@ if (isRelease) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
execSync(`napi build --platform --js false`, { stdio: 'inherit', env: process.env });
|
execSync(`napi build --platform --js false`, { stdio: 'inherit', env: process.env });
|
||||||
|
|
||||||
|
|
||||||
|
/* Mac App Store build with sandboxing - Does this belong here?
|
||||||
|
|
||||||
|
const target = process.env.npm_config_target || '';
|
||||||
|
const featuresArg = target.includes('mas') ? '--features sandbox' : '';
|
||||||
|
execSync(`napi build --platform --js false ${featuresArg}`, { stdio: 'inherit', env: process.env });
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -1181,6 +1181,13 @@ pub mod chromium_importer {
|
|||||||
.map(|logins| logins.into_iter().map(LoginImportResult::from).collect())
|
.map(|logins| logins.into_iter().map(LoginImportResult::from).collect())
|
||||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
.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"))]
|
||||||
|
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()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ default = []
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true, features = ["sync"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
|||||||
@@ -1,9 +1,56 @@
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn main() {
|
fn main() {
|
||||||
use glob::glob;
|
use glob::glob;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||||
|
|
||||||
|
// Compile Swift files FIRST (generates Bitwarden-Swift.h for browser_access.m)
|
||||||
|
let swift_files: Vec<String> = glob("src/native/**/*.swift")
|
||||||
|
.expect("Failed to read Swift glob pattern")
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.map(|p| {
|
||||||
|
println!("cargo::rerun-if-changed={}", p.display());
|
||||||
|
p.to_str().unwrap().to_string()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !swift_files.is_empty() {
|
||||||
|
// Compile Swift into a static library
|
||||||
|
let status = Command::new("swiftc")
|
||||||
|
.args(&[
|
||||||
|
"-emit-library",
|
||||||
|
"-static",
|
||||||
|
"-module-name",
|
||||||
|
"Bitwarden",
|
||||||
|
"-import-objc-header",
|
||||||
|
"src/native/bridging-header.h",
|
||||||
|
"-emit-objc-header-path",
|
||||||
|
&format!("{}/Bitwarden-Swift.h", out_dir),
|
||||||
|
"-o",
|
||||||
|
&format!("{}/libbitwarden_swift.a", out_dir),
|
||||||
|
])
|
||||||
|
.args(&swift_files)
|
||||||
|
.status()
|
||||||
|
.expect("Failed to compile Swift code");
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
panic!("Swift compilation failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell cargo to link the Swift library
|
||||||
|
println!("cargo:rustc-link-search=native={}", out_dir);
|
||||||
|
println!("cargo:rustc-link-lib=static=bitwarden_swift");
|
||||||
|
|
||||||
|
// Link required Swift/Foundation frameworks
|
||||||
|
println!("cargo:rustc-link-lib=framework=Foundation");
|
||||||
|
println!("cargo:rustc-link-lib=framework=AppKit");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile Objective-C files (Bitwarden-Swift.h exists now)
|
||||||
let mut builder = cc::Build::new();
|
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") {
|
for entry in glob("src/native/**/*.m").expect("Failed to read glob pattern") {
|
||||||
let path = entry.expect("Failed to read glob entry");
|
let path = entry.expect("Failed to read glob entry");
|
||||||
builder.file(path.clone());
|
builder.file(path.clone());
|
||||||
@@ -11,8 +58,9 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
builder
|
builder
|
||||||
|
.include(&out_dir) // Add OUT_DIR to include path so Bitwarden-Swift.h can be found
|
||||||
.flag("-fobjc-arc") // Enable Auto Reference Counting (ARC)
|
.flag("-fobjc-arc") // Enable Auto Reference Counting (ARC)
|
||||||
.compile("autofill");
|
.compile("objc_code");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "utils.h"
|
||||||
|
#import "interop.h"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#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);
|
||||||
|
|
||||||
|
// 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
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "browser_access.h"
|
||||||
|
#import "../utils.h"
|
||||||
|
|
||||||
|
// Import the Swift-generated header
|
||||||
|
// The name matches the module-name in build.rs: "Bitwarden"
|
||||||
|
#import "Bitwarden-Swift.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) {
|
||||||
|
@autoreleasepool {
|
||||||
|
NSString* name = [NSString stringWithUTF8String:browserName];
|
||||||
|
// Note: Matches the Swift method name with typo "Broswer"
|
||||||
|
NSString* result = [getManager() requestAccessToBroswerDir:name];
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import Cocoa
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc public class BrowserAccessManager: NSObject {
|
||||||
|
private let bookmarkKey = "com.bitwarden.chromiumImporter.bookmarks"
|
||||||
|
|
||||||
|
private let browserPaths: [String: String] = [
|
||||||
|
"Chrome": "Library/Application Support/Google/Chrome",
|
||||||
|
"Chromium": "Library/Application Support/Chromium",
|
||||||
|
"Microsoft Edge": "Library/Application Support/Microsoft Edge",
|
||||||
|
"Brave": "Library/Application Support/BraveSoftware/Brave-Browser",
|
||||||
|
"Arc": "Library/Application Support/Arc/User Data",
|
||||||
|
"Opera": "Library/Application Support/com.operasoftware.Opera",
|
||||||
|
"Vivaldi": "Library/Application Support/Vivaldi",
|
||||||
|
]
|
||||||
|
|
||||||
|
/// 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? {
|
||||||
|
guard let relativePath = browserPaths[browserName] else {
|
||||||
|
NSLog("Unknown browser: \(browserName)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let homeDir = FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
let browserPath = homeDir.appendingPathComponent(relativePath)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
guard openPanel.runModal() == .OK, let url = openPanel.url else {
|
||||||
|
NSLog("User cancelled access request")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Invalid Folder"
|
||||||
|
alert.informativeText =
|
||||||
|
"The selected folder doesn't appear to be a valid \(browserName) data directory. Please select the correct folder."
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.runModal()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// access is temporary right now, persist it by creating a security bookmark
|
||||||
|
do {
|
||||||
|
let bookmarkData = try url.bookmarkData(
|
||||||
|
options: .withSecurityScope,
|
||||||
|
includingResourceValuesForKeys: nil,
|
||||||
|
relativeTo: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
saveBookmark(bookmarkData, forBrowser: browserName)
|
||||||
|
return bookmarkData.base64EncodedString()
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to create bookmark: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if we have stored bookmark for browser (doesn't verify it's still valid)
|
||||||
|
@objc public func hasStoredAccess(_ browserName: String) -> Bool {
|
||||||
|
return loadBookmark(forBrowser: browserName) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
var isStale = false
|
||||||
|
let url = try URL(
|
||||||
|
resolvingBookmarkData: bookmarkData,
|
||||||
|
options: .withSecurityScope,
|
||||||
|
relativeTo: nil,
|
||||||
|
bookmarkDataIsStale: &isStale
|
||||||
|
)
|
||||||
|
|
||||||
|
if isStale {
|
||||||
|
NSLog("Security bookmark for \(browserName) is stale, attempting to re-create it")
|
||||||
|
do {
|
||||||
|
let newBookmarkData = try url.bookmarkData(
|
||||||
|
options: .withSecurityScope,
|
||||||
|
includingResourceValuesForKeys: nil,
|
||||||
|
relativeTo: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
saveBookmark(newBookmarkData, forBrowser: browserName)
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to create bookmark: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard url.startAccessingSecurityScopedResource() else {
|
||||||
|
NSLog("Failed to start accessing security-scoped resource")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.path
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to resolve bookmark: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop accessing a browser directory (must be called after startAccessingBrowser)
|
||||||
|
@objc public func stopAccessingBrowser(_ browserName: String) {
|
||||||
|
guard let bookmarkData = loadBookmark(forBrowser: browserName) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
var isStale = false
|
||||||
|
let url = try URL(
|
||||||
|
resolvingBookmarkData: bookmarkData,
|
||||||
|
options: .withSecurityScope,
|
||||||
|
relativeTo: nil,
|
||||||
|
bookmarkDataIsStale: &isStale
|
||||||
|
)
|
||||||
|
|
||||||
|
url.stopAccessingSecurityScopedResource()
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to resolve bookmark for stop: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bookmarkKeyFor(_ browserName: String) -> String {
|
||||||
|
return "\(bookmarkKey).\(browserName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveBookmark(_ data: Data, forBrowser browserName: String) {
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
let key = bookmarkKeyFor(browserName)
|
||||||
|
defaults.set(data, forKey: key)
|
||||||
|
defaults.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadBookmark(forBrowser browserName: String) -> Data? {
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
let key = bookmarkKeyFor(browserName)
|
||||||
|
return defaults.data(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user