diff --git a/apps/desktop/desktop_native/objc/build.rs b/apps/desktop/desktop_native/objc/build.rs index ecbdf595f3..e65d7ae6c0 100644 --- a/apps/desktop/desktop_native/objc/build.rs +++ b/apps/desktop/desktop_native/objc/build.rs @@ -1,54 +1,8 @@ #[cfg(target_os = "macos")] fn main() { - use std::process::Command; - use glob::glob; - let out_dir = std::env::var("OUT_DIR").expect("env var OUT_DIR is invalid or not set"); - - // Compile Swift files FIRST (generates Bitwarden-Swift.h for browser_access.m) - let swift_files: Vec = glob("src/native/**/*.swift") - .expect("Failed to read Swift glob pattern") - .filter_map(Result::ok) - .filter_map(|p| { - println!("cargo::rerun-if-changed={}", p.display()); - p.to_str().map(|s| s.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) + // Compile Objective-C files let mut builder = cc::Build::new(); // Compile all .m files in the src/native directory @@ -59,9 +13,12 @@ fn main() { } 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) .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"))] diff --git a/apps/desktop/desktop_native/objc/src/native/chromium_importer/BrowserAccessManager.h b/apps/desktop/desktop_native/objc/src/native/chromium_importer/BrowserAccessManager.h new file mode 100644 index 0000000000..d7a37f928d --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/chromium_importer/BrowserAccessManager.h @@ -0,0 +1,25 @@ +#import + +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; + +/// 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 diff --git a/apps/desktop/desktop_native/objc/src/native/chromium_importer/BrowserAccessManager.m b/apps/desktop/desktop_native/objc/src/native/chromium_importer/BrowserAccessManager.m new file mode 100644 index 0000000000..52d77556f4 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/chromium_importer/BrowserAccessManager.m @@ -0,0 +1,196 @@ +#import "BrowserAccessManager.h" +#import + +@implementation BrowserAccessManager { + NSString *_bookmarkKey; + NSDictionary *_browserPaths; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _bookmarkKey = @"com.bitwarden.chromiumImporter.bookmarks"; + + _browserPaths = @{ + @"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" + }; + } + return self; +} + +- (NSString *)requestAccessToBrowserDir:(NSString *)browserName { + // NSLog(@"[OBJC] requestAccessToBrowserDir called for: %@", browserName); + + NSString *relativePath = _browserPaths[browserName]; + if (!relativePath) { + // NSLog(@"[OBJC] Unknown browser: %@", browserName); + return nil; + } + + NSURL *homeDir = [[NSFileManager defaultManager] homeDirectoryForCurrentUser]; + NSURL *browserPath = [homeDir URLByAppendingPathComponent:relativePath]; + + // NSLog(@"[OBJC] Browser path: %@", browserPath.path); + + // 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; + + // NSLog(@"[OBJC] About to call runModal"); + panelResult = [openPanel runModal]; + selectedURL = openPanel.URL; + // NSLog(@"[OBJC] runModal returned: %ld", (long)panelResult); + }; + + if ([NSThread isMainThread]) { + // NSLog(@"[OBJC] Already on main thread"); + showPanel(); + } else { + // NSLog(@"[OBJC] Dispatching to main queue..."); + dispatch_sync(dispatch_get_main_queue(), showPanel); + } + + if (panelResult != NSModalResponseOK || !selectedURL) { + // NSLog(@"[OBJC] User cancelled access request or panel failed"); + return nil; + } + + // NSLog(@"[OBJC] User selected URL: %@", selectedURL.path); + + NSURL *localStatePath = [selectedURL URLByAppendingPathComponent:@"Local State"]; + if (![[NSFileManager defaultManager] fileExistsAtPath:localStatePath.path]) { + // NSLog(@"[OBJC] Selected folder doesn't appear to be a valid %@ directory", browserName); + + 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) { + // NSLog(@"[OBJC] Failed to create bookmark: %@", error); + return nil; + } + + [self saveBookmark:bookmarkData forBrowser:browserName]; + // NSLog(@"[OBJC] Successfully created and saved bookmark"); + 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) { + // NSLog(@"Failed to resolve bookmark: %@", error); + return nil; + } + + if (isStale) { + // NSLog(@"Security bookmark for %@ is stale, attempting to re-create it", browserName); + NSData *newBookmarkData = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope + includingResourceValuesForKeys:nil + relativeToURL:nil + error:&error]; + + if (!newBookmarkData) { + // NSLog(@"Failed to create bookmark: %@", error); + return nil; + } + + [self saveBookmark:newBookmarkData forBrowser:browserName]; + } + + if (![url startAccessingSecurityScopedResource]) { + // NSLog(@"Failed to start accessing security-scoped resource"); + 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) { + // NSLog(@"Failed to resolve bookmark for stop: %@", error); + 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 diff --git a/apps/desktop/desktop_native/objc/src/native/chromium_importer/browser_access.m b/apps/desktop/desktop_native/objc/src/native/chromium_importer/browser_access.m index fa204fca4e..d1b10a003f 100644 --- a/apps/desktop/desktop_native/objc/src/native/chromium_importer/browser_access.m +++ b/apps/desktop/desktop_native/objc/src/native/chromium_importer/browser_access.m @@ -2,7 +2,7 @@ #import "browser_access.h" #import "../utils.h" -#import "Bitwarden-Swift.h" +#import "BrowserAccessManager.h" static BrowserAccessManager* sharedManager = nil; @@ -17,8 +17,7 @@ static BrowserAccessManager* getManager() { 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]; + NSString* result = [getManager() requestAccessToBrowserDir:name]; if (result == nil) { return NULL; diff --git a/apps/desktop/desktop_native/objc/src/native/chromium_importer/browser_access.swift b/apps/desktop/desktop_native/objc/src/native/chromium_importer/browser_access.swift deleted file mode 100644 index 5bcdd4835f..0000000000 --- a/apps/desktop/desktop_native/objc/src/native/chromium_importer/browser_access.swift +++ /dev/null @@ -1,196 +0,0 @@ -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? { - // NSLog("[SWIFT] requestAccessToBroswerDir called for: \(browserName)") - - guard let relativePath = browserPaths[browserName] else { - // NSLog("[SWIFT] Unknown browser: \(browserName)") - return nil - } - - let homeDir = FileManager.default.homeDirectoryForCurrentUser - let browserPath = homeDir.appendingPathComponent(relativePath) - - // NSLog("[SWIFT] Browser path: \(browserPath.path)") - - // NSOpenPanel must be run on the main thread - var selectedURL: URL? - var panelResult: NSApplication.ModalResponse = .cancel - - 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 - - // 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 - - // 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("[SWIFT] 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) - // NSLog("[SWIFT] Successfully created and saved bookmark") - return bookmarkData.base64EncodedString() - } catch { - // NSLog("[SWIFT] 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 - @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) - } - -}