From 17ad4bec09d9aa79a13b7a5f55fdf48535107a09 Mon Sep 17 00:00:00 2001 From: Sebastian Livoni <29739749+sebastianlivoni@users.noreply.github.com> Date: Thu, 19 Jun 2025 23:42:35 +0200 Subject: [PATCH 1/6] Safari Web Extension --- .gitmodules | 3 + CMakeLists.txt | 12 +- INSTALL.md | 1 + release-tool | 15 +- share/macosx/Info.plist.cmake | 2 +- share/macosx/keepassxc.entitlements | 9 +- src/CMakeLists.txt | 27 ++- src/browser/BrowserHost.cpp | 23 +++ src/browser/BrowserSettingsWidget.cpp | 12 ++ src/browser/BrowserShared.cpp | 8 +- src/browser/BrowserSharedMac.h | 6 + src/browser/BrowserSharedMac.mm | 25 +++ src/browser/CMakeLists.txt | 23 ++- src/proxy/CMakeLists.txt | 12 ++ src/safariwebextension/CMakeLists.txt | 72 ++++++++ src/safariwebextension/Info.plist | 30 ++++ .../SafariWebExtensionCheckbox.h | 17 ++ .../SafariWebExtensionCheckbox.mm | 39 ++++ .../SafariWebExtensionHandler.swift | 168 ++++++++++++++++++ .../SafariWebExtensionHelper.h | 22 +++ .../SafariWebExtensionHelper.mm | 101 +++++++++++ .../safariwebextension.entitlements | 12 ++ 22 files changed, 628 insertions(+), 11 deletions(-) create mode 100644 .gitmodules create mode 100644 src/browser/BrowserSharedMac.h create mode 100644 src/browser/BrowserSharedMac.mm create mode 100644 src/safariwebextension/CMakeLists.txt create mode 100644 src/safariwebextension/Info.plist create mode 100644 src/safariwebextension/SafariWebExtensionCheckbox.h create mode 100644 src/safariwebextension/SafariWebExtensionCheckbox.mm create mode 100644 src/safariwebextension/SafariWebExtensionHandler.swift create mode 100644 src/safariwebextension/SafariWebExtensionHelper.h create mode 100644 src/safariwebextension/SafariWebExtensionHelper.mm create mode 100644 src/safariwebextension/safariwebextension.entitlements diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..a44fabc61d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/safariwebextension/keepassxc-browser"] + path = src/safariwebextension/keepassxc-browser + url = git@github.com:sebastianlivoni/keepassxc-browser.git diff --git a/CMakeLists.txt b/CMakeLists.txt index e7183f1691..d318086c86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,9 @@ option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for control if(UNIX AND NOT APPLE) option(WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API." OFF) endif() +if (APPLE) + option(WITH_XC_SAFARI_WEB_EXTENSION "Include Safari Web Extension for browser intergration with keepassxc-browser") +endif() option(WITH_XC_DOCS "Enable building of documentation" ON) set(WITH_XC_X11 ON CACHE BOOL "Enable building with X11 deps") @@ -106,6 +109,9 @@ if(WITH_XC_ALL) if(UNIX AND NOT APPLE) set(WITH_XC_FDOSECRETS ON) endif() + if(APPLE) + set(WITH_XC_SAFARI_WEB_EXTENSION ON) + endif() endif() # Prefer WITH_XC_NETWORKING setting over WITH_XC_UPDATECHECK @@ -119,6 +125,10 @@ if(UNIX AND NOT APPLE AND NOT WITH_XC_X11) set(WITH_XC_AUTOTYPE OFF) endif() +if(APPLE AND WITH_XC_SAFARI_WEB_EXTENSION AND NOT WITH_XC_BROWSER) + message(FATAL_ERROR "Safari Web Extension requires keepassxc-browser browser integration (set it with WITH_XC_BROWSER)") +endif() + set(KEEPASSXC_VERSION_MAJOR "2") set(KEEPASSXC_VERSION_MINOR "8") set(KEEPASSXC_VERSION_PATCH "0") @@ -211,7 +221,7 @@ if(BOTAN_VERSION VERSION_GREATER_EQUAL "3.0.0") set(WITH_XC_BOTAN3 TRUE) elseif(BOTAN_VERSION VERSION_LESS "2.11.0") # Check for minimum Botan version - message(FATAL_ERROR "Botan 2.11.0 or higher is required") + message(FATAL_ERROR "Botan 2.11.0 or higher is required") endif() include_directories(SYSTEM ${BOTAN_INCLUDE_DIR}) diff --git a/INSTALL.md b/INSTALL.md index e83f064c04..de77baf405 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -97,6 +97,7 @@ KeePassXC comes with a variety of build options that can turn on/off features. M -DWITH_XC_YUBIKEY=[ON|OFF] Enable/Disable YubiKey HMAC-SHA1 authentication support (default: OFF) -DWITH_XC_BROWSER=[ON|OFF] Enable/Disable KeePassXC-Browser extension support (default: OFF) -DWITH_XC_BROWSER_PASSKEYS=[ON|OFF] Enable/Disable Passkeys support for browser integration (default: OFF) +-DWITH_XC_SAFARI_WEB_EXTENSION=[ON|OFF] Enable/Disable Safari Web Extension for browser integration (default: OFF) -DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (e.g., favicon downloading) (default: OFF) -DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF) -DWITH_XC_FDOSECRETS=[ON|OFF] (Linux Only) Enable/Disable Freedesktop.org Secrets Service support (default:OFF) diff --git a/release-tool b/release-tool index 43f0580006..ce399b103a 100755 --- a/release-tool +++ b/release-tool @@ -1211,9 +1211,22 @@ appsign() { cd "${orig_dir}" exitError "Signing failed!" fi + + # Sign safariwebextension extension + local safari_web_extension_dir="${app_dir_tmp}/Contents/PlugIns/KeePassXCSafariWebExtension.appex/Contents/MacOS/KeePassXCSafariWebExtension" + if [ -f "${safari_web_extension_dir}" ]; then + if ! xcrun codesign --sign "${key}" --verbose --force --options runtime --entitlements \ + "${real_src_dir}/build/share/macosx/safariwebextension.entitlements" "${safari_web_extension_dir}"; then + cd "${orig_dir}" + exitError "Signing failed!" + fi + else + echo "Skipping signing Safari Web Extension as it does not exists." + fi + # Sign main executable with additional entitlements if ! xcrun codesign --sign "${key}" --verbose --force --options runtime --entitlements \ - "${real_src_dir}/share/macosx/keepassxc.entitlements" "${app_dir_tmp}/Contents/MacOS/KeePassXC"; then + "${real_src_dir}/build/share/macosx/keepassxc.entitlements" "${app_dir_tmp}/Contents/MacOS/KeePassXC"; then cd "${orig_dir}" exitError "Signing failed!" fi diff --git a/share/macosx/Info.plist.cmake b/share/macosx/Info.plist.cmake index 086d208dfd..076b2e6c78 100644 --- a/share/macosx/Info.plist.cmake +++ b/share/macosx/Info.plist.cmake @@ -15,7 +15,7 @@ CFBundleIconFile keepassxc.icns CFBundleIdentifier - org.keepassxc.keepassxc + ${APPLE_APP_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/share/macosx/keepassxc.entitlements b/share/macosx/keepassxc.entitlements index 7126b7ac5b..feadb87c39 100644 --- a/share/macosx/keepassxc.entitlements +++ b/share/macosx/keepassxc.entitlements @@ -3,10 +3,15 @@ com.apple.application-identifier - G2S7P7J672.org.keepassxc.keepassxc + ${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER} + com.apple.security.application-groups + + ${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER} + keychain-access-groups - G2S7P7J672.org.keepassxc.keepassxc + ${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER} + @EXTRA_ENTITLEMENTS@ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9ddc46cf9f..6e47d7ebe7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing") add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)") add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser") add_feature_info(Passkeys WITH_XC_BROWSER_PASSKEYS "Passkeys support for browser integration") +add_feature_info(SafariWebExtension WITH_XC_SAFARI_WEB_EXTENSION "Safari Web Extension for browser integration") add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent") add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare") add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response") @@ -262,7 +263,7 @@ if(UNIX AND NOT APPLE) find_library(KEYUTILS_LIBRARIES NAMES keyutils) if(NOT KEYUTILS_LIBRARIES) - message(FATAL_ERROR "Could not find libkeyutils") + message(FATAL_ERROR "Could not find libkeyutils") endif() endif() @@ -307,6 +308,10 @@ if(WITH_XC_BROWSER_PASSKEYS) gui/passkeys/PasskeyImportDialog.cpp) endif() +if(WITH_XC_SAFARI_WEB_EXTENSION) + add_subdirectory(safariwebextension) +endif() + add_subdirectory(autotype) add_subdirectory(cli) add_subdirectory(qrcode) @@ -370,7 +375,7 @@ configure_file(git-info.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/git-info.h) # Core Library Definition add_library(keepassxc_core STATIC ${core_SOURCES}) set_target_properties(keepassxc_core PROPERTIES COMPILE_DEFINITIONS KEEPASSX_BUILDING_CORE) -target_link_libraries(keepassxc_core +target_link_libraries(keepassxc_core ${qrcode_LIB} Qt5::Core Qt5::Concurrent @@ -397,7 +402,10 @@ target_link_libraries(keepassxc_gui ${sshagent_LIB}) if(APPLE) - target_link_libraries(keepassxc_gui "-framework Foundation -framework AppKit -framework Carbon -framework Security -framework LocalAuthentication -framework ScreenCaptureKit") + target_compile_definitions(keepassxc_core PRIVATE + APP_GROUP_IDENTIFIER="${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER}") + + target_link_libraries(keepassxc_gui "-framework Foundation -framework AppKit -framework Carbon -framework Security -framework LocalAuthentication -framework ScreenCaptureKit -framework SafariServices") if(Qt5MacExtras_FOUND) target_link_libraries(keepassxc_gui Qt5::MacExtras) endif() @@ -439,12 +447,23 @@ set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON) # macOS App Bundle if(APPLE AND WITH_APP_BUNDLE) + set(APPLE_TEAM_ID "G2S7P7J672" CACHE STRING "Apple Developer Team ID") + set(APPLE_APP_IDENTIFIER "org.keepassxc.keepassxc" CACHE STRING "App bundle identifier") + install(FILES ${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile DESTINATION ${BUNDLE_INSTALL_DIR}) configure_file(${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake ${CMAKE_CURRENT_BINARY_DIR}/Info.plist) + + if(CMAKE_BUILD_TYPE_LOWER STREQUAL "debug") + set(EXTRA_ENTITLEMENTS "com.apple.security.cs.disable-library-validation\n\t\n\tcom.apple.security.get-task-allow\n\t") + else() + set(EXTRA_ENTITLEMENTS "") + endif() + + configure_file(${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements ${CMAKE_BINARY_DIR}/share/macosx/keepassxc.entitlements) set_target_properties(${PROGNAME} PROPERTIES MACOSX_BUNDLE ON MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist - CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements") + CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_BINARY_DIR}/share/macosx/keepassxc.entitlements") if(QT_MAC_USE_COCOA AND EXISTS "${QT_LIBRARY_DIR}/Resources/qt_menu.nib") install(DIRECTORY "${QT_LIBRARY_DIR}/Resources/qt_menu.nib" diff --git a/src/browser/BrowserHost.cpp b/src/browser/BrowserHost.cpp index bc6129bf11..4b100b69c1 100644 --- a/src/browser/BrowserHost.cpp +++ b/src/browser/BrowserHost.cpp @@ -22,6 +22,10 @@ #include #include +#ifdef WITH_XC_SAFARI_WEB_EXTENSION +#include "safariwebextension/SafariWebExtensionHelper.h" +#endif + #ifdef Q_OS_WIN #include #undef NOMINMAX @@ -94,9 +98,28 @@ void BrowserHost::readProxyMessage() void BrowserHost::broadcastClientMessage(const QJsonObject& json) { QString reply(QJsonDocument(json).toJson(QJsonDocument::Compact)); + +#ifdef WITH_XC_SAFARI_WEB_EXTENSION + bool containsSafariWebExtensionSocket = false; +#endif + + // Send message to all non-Safari sockets for (const auto socket : m_socketList) { +#ifdef WITH_XC_SAFARI_WEB_EXTENSION + if (safariWebExtensionHelper()->isSafariWebExtension(socket)) { + containsSafariWebExtensionSocket = true; + continue; // Skip Safari web extension sockets because we are using SFSafariApplication instead + } +#endif + sendClientData(socket, reply); } + +#ifdef WITH_XC_SAFARI_WEB_EXTENSION + if (containsSafariWebExtensionSocket) { + safariWebExtensionHelper()->broadcastClientMessage(reply); + } +#endif } void BrowserHost::sendClientMessage(QLocalSocket* socket, const QJsonObject& json) diff --git a/src/browser/BrowserSettingsWidget.cpp b/src/browser/BrowserSettingsWidget.cpp index 5a4ccce8dd..1659c7fe10 100644 --- a/src/browser/BrowserSettingsWidget.cpp +++ b/src/browser/BrowserSettingsWidget.cpp @@ -22,6 +22,10 @@ #include "config-keepassx.h" #include "gui/styles/StateColorPalette.h" +#ifdef WITH_XC_SAFARI_WEB_EXTENSION +#include "safariwebextension/SafariWebExtensionCheckbox.h" +#endif + #include BrowserSettingsWidget::BrowserSettingsWidget(QWidget* parent) @@ -44,6 +48,14 @@ BrowserSettingsWidget::BrowserSettingsWidget(QWidget* parent) connect(m_ui->enableBrowserSupport, SIGNAL(toggled(bool)), m_ui->tabWidget, SLOT(setEnabled(bool))); connect(m_ui->enableBrowserSupport, SIGNAL(toggled(bool)), SLOT(validateProxyLocation())); +#ifdef WITH_XC_SAFARI_WEB_EXTENSION + SafariWebExtensionCheckbox *safariCheckbox = new SafariWebExtensionCheckbox(); + safariCheckbox->setText("Safari"); + safariCheckbox->setChecked(false); + + m_ui->gridLayout->addWidget(safariCheckbox, 1, 3); +#endif + // Custom Browser option #ifdef Q_OS_WIN // TODO: Custom browser is disabled on Windows diff --git a/src/browser/BrowserShared.cpp b/src/browser/BrowserShared.cpp index 6fd2cf7ee7..e07c8290e5 100644 --- a/src/browser/BrowserShared.cpp +++ b/src/browser/BrowserShared.cpp @@ -25,6 +25,10 @@ #include #endif +#if defined(Q_OS_MACOS) +#include "BrowserSharedMac.h" +#endif + namespace BrowserShared { QString localServerPath() @@ -53,7 +57,9 @@ namespace BrowserShared #elif defined(Q_OS_WIN) // Windows uses named pipes return serverName + "_" + qgetenv("USERNAME"); -#else // Q_OS_MACOS and others +#elif defined(Q_OS_MACOS) + return macOSLocalServerPath(); +#else // others return QStandardPaths::writableLocation(QStandardPaths::TempLocation) + serverName; #endif } diff --git a/src/browser/BrowserSharedMac.h b/src/browser/BrowserSharedMac.h new file mode 100644 index 0000000000..24f7e60cff --- /dev/null +++ b/src/browser/BrowserSharedMac.h @@ -0,0 +1,6 @@ +#include + +namespace BrowserShared +{ + QString macOSLocalServerPath(); +} \ No newline at end of file diff --git a/src/browser/BrowserSharedMac.mm b/src/browser/BrowserSharedMac.mm new file mode 100644 index 0000000000..5e59ed7ab3 --- /dev/null +++ b/src/browser/BrowserSharedMac.mm @@ -0,0 +1,25 @@ +#include +#include +#include + +namespace BrowserShared +{ + QString macOSLocalServerPath() + { + NSString *appGroupIdentifier = QString::fromUtf8(APP_GROUP_IDENTIFIER).toNSString(); + + // Get the container URL for the app group identifier + NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupIdentifier]; + + NSString *containerPath = [containerURL path]; + + QString homePath = QString::fromNSString(containerPath); + + QDir().mkpath(homePath); + + // The path will become too long therefore we must cut off serverName + QString socketPath = homePath + "/KeePassXC.BrowserServer"; + + return socketPath; + } +} \ No newline at end of file diff --git a/src/browser/CMakeLists.txt b/src/browser/CMakeLists.txt index 7942be430e..cfae2463ac 100644 --- a/src/browser/CMakeLists.txt +++ b/src/browser/CMakeLists.txt @@ -14,7 +14,7 @@ # along with this program. If not, see . if(WITH_XC_BROWSER) - include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) + include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/src/safariwebextension) set(browser_SOURCES BrowserAccessControlDialog.cpp @@ -31,6 +31,17 @@ if(WITH_XC_BROWSER) CustomTableWidget.cpp NativeMessageInstaller.cpp) + if(APPLE) + list(APPEND browser_SOURCES + BrowserSharedMac.mm) + + if(WITH_XC_SAFARI_WEB_EXTENSION) + list(APPEND browser_SOURCES + ../safariwebextension/SafariWebExtensionHelper.mm + ../safariwebextension/SafariWebExtensionCheckbox.mm) + endif() + endif() + if(WITH_XC_BROWSER_PASSKEYS) list(APPEND browser_SOURCES BrowserCbor.cpp @@ -41,5 +52,15 @@ if(WITH_XC_BROWSER) endif() add_library(browser STATIC ${browser_SOURCES}) + + if(APPLE) + target_compile_definitions(browser PRIVATE + APP_GROUP_IDENTIFIER="${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER}" APPLE_APP_IDENTIFIER="${APPLE_APP_IDENTIFIER}") + endif() + + if(WITH_XC_SAFARI_WEB_EXTENSION) + target_compile_definitions(browser PRIVATE WITH_XC_SAFARI_WEB_EXTENSION) + endif() + target_link_libraries(browser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network ${BOTAN_LIBRARIES}) endif() diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index be756672dd..20489de241 100644 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -19,6 +19,11 @@ if(WITH_XC_BROWSER) keepassxc-proxy.cpp NativeMessagingProxy.cpp) + if(APPLE) + list(APPEND proxy_SOURCES + ../browser/BrowserSharedMac.mm) + endif() + # Alloc must be defined in a static library to prevent clashing with clang ASAN definitions add_library(proxy_alloc STATIC ../core/Alloc.cpp) target_link_libraries(proxy_alloc PRIVATE Qt5::Core ${BOTAN_LIBRARIES}) @@ -29,6 +34,11 @@ if(WITH_XC_BROWSER) BUNDLE DESTINATION . COMPONENT Runtime RUNTIME DESTINATION ${PROXY_INSTALL_DIR} COMPONENT Runtime) + if(APPLE) + target_compile_definitions(keepassxc-proxy PRIVATE + APP_GROUP_IDENTIFIER="${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER}") + endif() + if(APPLE AND WITH_APP_BUNDLE) set(PROXY_APP_DIR "${CMAKE_BINARY_DIR}/src/${PROXY_INSTALL_DIR}") add_custom_command(TARGET keepassxc-proxy @@ -39,6 +49,8 @@ if(WITH_XC_BROWSER) set_property(GLOBAL APPEND PROPERTY _MACDEPLOYQT_EXTRA_BINARIES "${PROXY_INSTALL_DIR}/keepassxc-proxy") + + target_link_libraries(keepassxc-proxy "-framework Foundation") endif() if(WIN32) diff --git a/src/safariwebextension/CMakeLists.txt b/src/safariwebextension/CMakeLists.txt new file mode 100644 index 0000000000..f5e1f958bd --- /dev/null +++ b/src/safariwebextension/CMakeLists.txt @@ -0,0 +1,72 @@ +cmake_minimum_required(VERSION 3.10) + +# Specify the Swift source +set(SWIFT_SOURCE "${CMAKE_SOURCE_DIR}/src/safariwebextension/SafariWebExtensionHandler.swift") + +# Create an object file from the Swift source +add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/SafariWebExtensionHandler.o + COMMAND swiftc -module-name KeePassXCSafariWebExtension -c ${SWIFT_SOURCE} -o ${CMAKE_BINARY_DIR}/SafariWebExtensionHandler.o + DEPENDS ${SWIFT_SOURCE} + COMMENT "Using Swift compiler for SafariWebExtensionHandler.swift" +) + +add_executable(KeePassXCSafariWebExtension + ${CMAKE_BINARY_DIR}/SafariWebExtensionHandler.o +) + +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -e _NSExtensionMain") + +configure_file(${CMAKE_SOURCE_DIR}/src/safariwebextension/safariwebextension.entitlements ${CMAKE_BINARY_DIR}/share/macosx/safariwebextension.entitlements) + +set_target_properties(KeePassXCSafariWebExtension PROPERTIES + MACOSX_BUNDLE ON + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/src/safariwebextension/Info.plist + BUNDLE_EXTENSION appex + CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_BINARY_DIR}/share/macosx/safariwebextension.entitlements" +) + +# Install the target as an app bundle +install(TARGETS KeePassXCSafariWebExtension + BUNDLE DESTINATION ${PLUGIN_INSTALL_DIR} COMPONENT Runtime) + +# Compile browser extension and copy into Resources +set(WEB_EXTENSION_SOURCE_DIR "${CMAKE_SOURCE_DIR}/src/safariwebextension/keepassxc-browser") +set(BINARY_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/KeePassXCSafariWebExtension.appex/Contents/Resources") + +execute_process( + COMMAND npm ci + WORKING_DIRECTORY ${WEB_EXTENSION_SOURCE_DIR} + RESULT_VARIABLE result + ERROR_QUIET + OUTPUT_QUIET +) + +if (NOT result EQUAL 0) + message(FATAL_ERROR "npm ci failed with error code ${result}") +endif() + +file(MAKE_DIRECTORY ${BINARY_RESOURCES_DIR}) + +execute_process( + COMMAND node build.js --skip-translations --safari-web-extension -o ${BINARY_RESOURCES_DIR} + WORKING_DIRECTORY ${WEB_EXTENSION_SOURCE_DIR} + RESULT_VARIABLE result + ERROR_QUIET + OUTPUT_QUIET +) + +if (NOT result EQUAL 0) + message(FATAL_ERROR "Node.js build script failed with error code ${result}") +endif() + +if(WITH_APP_BUNDLE) + add_custom_command(TARGET KeePassXCSafariWebExtension + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${PLUGIN_INSTALL_DIR}/KeePassXCSafariWebExtension.appex + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_BINARY_DIR}/KeePassXCSafariWebExtension.appex + ${PLUGIN_INSTALL_DIR}/KeePassXCSafariWebExtension.appex + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src + COMMENT "Copying safariwebextension into plugins") +endif() diff --git a/src/safariwebextension/Info.plist b/src/safariwebextension/Info.plist new file mode 100644 index 0000000000..4f808b784a --- /dev/null +++ b/src/safariwebextension/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDisplayName + KeePassXC Safari Web Extension + CFBundleExecutable + KeePassXCSafariWebExtension + CFBundleIdentifier + ${APPLE_APP_IDENTIFIER}.SafariWebExtension + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + KeePassXCSafariWebExtension + CFBundleVersion + 1.0 + CFBundleShortVersionString + 1.0 + APP_GROUP_IDENTIFIER + ${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER} + NSExtension + + NSExtensionPointIdentifier + com.apple.Safari.web-extension + NSExtensionPrincipalClass + KeePassXCSafariWebExtension.SafariWebExtensionHandler + + + diff --git a/src/safariwebextension/SafariWebExtensionCheckbox.h b/src/safariwebextension/SafariWebExtensionCheckbox.h new file mode 100644 index 0000000000..ddaea12ad9 --- /dev/null +++ b/src/safariwebextension/SafariWebExtensionCheckbox.h @@ -0,0 +1,17 @@ +#include +#include +#include + +class SafariWebExtensionCheckbox : public QCheckBox +{ + Q_OBJECT + +public: + explicit SafariWebExtensionCheckbox(QWidget *parent = nullptr); + +protected: + void mousePressEvent(QMouseEvent *e) override; + +private slots: + void onApplicationStateChanged(Qt::ApplicationState state); +}; \ No newline at end of file diff --git a/src/safariwebextension/SafariWebExtensionCheckbox.mm b/src/safariwebextension/SafariWebExtensionCheckbox.mm new file mode 100644 index 0000000000..d3dcf84f5f --- /dev/null +++ b/src/safariwebextension/SafariWebExtensionCheckbox.mm @@ -0,0 +1,39 @@ +#include "SafariWebExtensionCheckbox.h" + +#include + +#include + +SafariWebExtensionCheckbox::SafariWebExtensionCheckbox(QWidget *parent) + : QCheckBox(parent) +{ + connect(qApp, &QApplication::applicationStateChanged, this, &SafariWebExtensionCheckbox::onApplicationStateChanged); +} + +void SafariWebExtensionCheckbox::mousePressEvent(QMouseEvent *e) { + NSString *appIdentifier = QString::fromUtf8(APPLE_APP_IDENTIFIER).toNSString(); + NSString *extensionIdentifier = [appIdentifier stringByAppendingString:@".SafariWebExtension"]; + + if (e->button() == Qt::LeftButton) { + [SFSafariApplication showPreferencesForExtensionWithIdentifier:extensionIdentifier completionHandler:nil]; + } +} + +void SafariWebExtensionCheckbox::onApplicationStateChanged(Qt::ApplicationState state) +{ + if (state != Qt::ApplicationActive) { + return; + } + + NSString *appIdentifier = QString::fromUtf8(APPLE_APP_IDENTIFIER).toNSString(); + NSString *extensionIdentifier = [appIdentifier stringByAppendingString:@".SafariWebExtension"]; + + [SFSafariExtensionManager getStateOfSafariExtensionWithIdentifier:extensionIdentifier completionHandler:^(SFSafariExtensionState *state, NSError *error) { + if (error) { + NSLog(@"Error fetching extension state: %@", error.localizedDescription); + return; + } + + setChecked(state.isEnabled); + }]; +} diff --git a/src/safariwebextension/SafariWebExtensionHandler.swift b/src/safariwebextension/SafariWebExtensionHandler.swift new file mode 100644 index 0000000000..016c676f24 --- /dev/null +++ b/src/safariwebextension/SafariWebExtensionHandler.swift @@ -0,0 +1,168 @@ +import SafariServices +import os.log + +// SEB_TODO: Convert this to Objective C++ (maybe) +class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { + let SocketFileName = "KeePassXC.BrowserServer" + let applicationGroupIdentifier = + Bundle.main.object(forInfoDictionaryKey: "APP_GROUP_IDENTIFIER") as! String + static var socketFD: Int32 = -1 + static var socketConnected = false + var maxMessageLength: Int32 = 1024 * 1024 + + private var logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: SafariWebExtensionHandler.self) + ) + + var socketPath: String? { + FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: applicationGroupIdentifier)?.appending( + component: SocketFileName + ).path(percentEncoded: false) + } + + func closeSocket() { + if Self.socketFD != -1 { + logger.info("Closing socket") + close(Self.socketFD) + Self.socketFD = -1 + } + } + + func connectSocket() -> Bool { + // Reuse socket + guard Self.socketFD == -1 else { return true } + + guard let socketPath else { return false } + + guard FileManager.default.fileExists(atPath: socketPath) else { + logger.error("Socket file does not exist") + return false + } + + Self.socketFD = socket(AF_UNIX, SOCK_STREAM, 0) + guard Self.socketFD > 0 else { + logger.error("Failed to create socket") + return false + } + + var optval: Int = 1 // Use 1 to enable the option, 0 to disable + guard + setsockopt( + Self.socketFD, SOL_SOCKET, SO_REUSEADDR, &optval, + socklen_t(MemoryLayout.size)) != -1 + else { + logger.error("setsockopt error: \(errno)") + return false + } + + guard + setsockopt( + Self.socketFD, SOL_SOCKET, SO_SNDBUF, &maxMessageLength, + socklen_t(MemoryLayout.size(ofValue: maxMessageLength))) != -1 + else { + logger.error("setsockopt error") + return false + } + + var address = sockaddr_un() + address.sun_family = sa_family_t(AF_UNIX) + + withUnsafeMutableBytes(of: &address.sun_path) { ptr in + socketPath.utf8CString.withUnsafeBytes { bytes in + ptr.copyBytes(from: bytes) + } + } + + let result = withUnsafePointer(to: &address) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + connect(Self.socketFD, $0, socklen_t(MemoryLayout.size)) + } + } + + if result != 0 { + logger.error("Failed to connect to socket: \(errno)") + close(Self.socketFD) + Self.socketFD = -1 + return false + } + + return true + } + + func beginRequest(with context: NSExtensionContext) { + let request = context.inputItems.first as? NSExtensionItem + + let message: Any? + if #available(iOS 15.0, macOS 11.0, *) { + message = request?.userInfo?[SFExtensionMessageKey] + } else { + message = request?.userInfo?["message"] + } + + guard let message = message as? [String: Any], + let data = try? JSONSerialization.data(withJSONObject: message as Any) + else { + context.cancelRequest(withError: SafariWebExtensionHandlerError.invalidRequest) + return + } + + if !Self.socketConnected { + logger.info("Socket is not connected") + if !connectSocket() { + closeSocket() + logger.error("Socket not connected") + context.cancelRequest( + withError: SafariWebExtensionHandlerError.socketConnectionFailed) + return + } + + Self.socketConnected = true + } else { + logger.info("Socket is connected") + } + + let bytesWritten = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> Int in + write(Self.socketFD, bytes.baseAddress, data.count) + } + + if bytesWritten == -1 { + logger.error("Cannot write to socket \(errno)") + context.cancelRequest(withError: SafariWebExtensionHandlerError.socketWriteFailed) + return + } + logger.debug("Sent message of \(data.count) bytes") + + var buffer = [UInt8](repeating: 0, count: Int(maxMessageLength)) + let bytesRead = read(Self.socketFD, &buffer, buffer.count) + + guard bytesRead > 0 else { + logger.error("No bytes read") + context.cancelRequest(withError: SafariWebExtensionHandlerError.socketReadFailed) + return + } + + let res = Data(buffer[0.. +#include +#include +#include + +#include +#include +#include + +SafariWebExtensionHelper* SafariWebExtensionHelper::instance() +{ + static SafariWebExtensionHelper instance; + return &instance; +} + +bool SafariWebExtensionHelper::isSafariWebExtension(QLocalSocket* socket) +{ + int sockfd = socket->socketDescriptor(); + pid_t pid = -1; + socklen_t len = sizeof(pid); + if (getsockopt(sockfd, SOL_LOCAL, LOCAL_PEERPID, &pid, &len) == -1) { + qDebug() << "Failed to get peer PID, error:" << strerror(errno); + return false; + } + + CFNumberRef pidNumber = CFNumberCreate(NULL, kCFNumberIntType, &pid); + + const void *keys[] = { kSecGuestAttributePid }; + const void *values[] = { pidNumber }; + + CFDictionaryRef attributes = CFDictionaryCreate(NULL, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + if (attributes == NULL) { + if (pidNumber != NULL) CFRelease(pidNumber); + return false; + } + + SecCodeRef parentCode = NULL; + OSStatus status = SecCodeCopyGuestWithAttributes(NULL, attributes, 0, &parentCode); + + if (attributes != NULL) CFRelease(attributes); + if (pidNumber != NULL) CFRelease(pidNumber); + + if (status != errSecSuccess || parentCode == NULL) { + qDebug() << "Failed to get SecCode for parent process (PID" << pid << "):" << static_cast(status); + return false; + } + + CFStringRef appIdentifierCF = CFStringCreateWithCString(NULL, APPLE_APP_IDENTIFIER, kCFStringEncodingUTF8); + CFStringRef requirementStringCF = CFStringCreateWithFormat(NULL, NULL, CFSTR("anchor apple generic and identifier \"%@.SafariWebExtension\""), appIdentifierCF); + + if (appIdentifierCF != NULL) CFRelease(appIdentifierCF); + + SecRequirementRef requirement = NULL; + status = SecRequirementCreateWithString(requirementStringCF, SecCSFlags(), &requirement); + + if (requirementStringCF != NULL) CFRelease(requirementStringCF); + + if (status != errSecSuccess || requirement == NULL) { + qDebug() << "Failed to create requirement:" << static_cast(status); + if (parentCode != NULL) { + CFRelease(parentCode); + } + return false; + } + + status = SecCodeCheckValidity(parentCode, SecCSFlags(), requirement); + + if (parentCode != NULL) { + CFRelease(parentCode); + } + if (requirement != NULL) { + CFRelease(requirement); + } + + return (status == errSecSuccess); +} + +void SafariWebExtensionHelper::broadcastClientMessage(const QString& reply) +{ + NSString* jsonString = reply.toNSString(); + NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + + NSError *error = nil; + NSDictionary *message = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + + if (error) { + QString errorDescription = QString::fromNSString(error.localizedDescription); + qDebug() << "Error converting NSString to NSDictionary:" << errorDescription; + return; + } + + NSString *appIdentifier = QString::fromUtf8(APPLE_APP_IDENTIFIER).toNSString(); + NSString *extensionIdentifier = [appIdentifier stringByAppendingString:@".SafariWebExtension"]; + + [SFSafariApplication dispatchMessageWithName:@"proxy_message" + toExtensionWithIdentifier:extensionIdentifier + userInfo:message + completionHandler:nil]; +} diff --git a/src/safariwebextension/safariwebextension.entitlements b/src/safariwebextension/safariwebextension.entitlements new file mode 100644 index 0000000000..f202dbc0ed --- /dev/null +++ b/src/safariwebextension/safariwebextension.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + ${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER} + + + From 0e0c9f2c5e510dfbe8f0a586998fea81cab2c2de Mon Sep 17 00:00:00 2001 From: Sebastian Livoni <29739749+sebastianlivoni@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:47:28 +0200 Subject: [PATCH 2/6] Replace SafariWebExtensionHandler.swift with Objective-C version --- src/safariwebextension/CMakeLists.txt | 19 +- src/safariwebextension/Info.plist | 2 +- .../SafariWebExtensionHandler.h | 15 ++ .../SafariWebExtensionHandler.mm | 119 +++++++++++++ .../SafariWebExtensionHandler.swift | 168 ------------------ 5 files changed, 142 insertions(+), 181 deletions(-) create mode 100644 src/safariwebextension/SafariWebExtensionHandler.h create mode 100644 src/safariwebextension/SafariWebExtensionHandler.mm delete mode 100644 src/safariwebextension/SafariWebExtensionHandler.swift diff --git a/src/safariwebextension/CMakeLists.txt b/src/safariwebextension/CMakeLists.txt index f5e1f958bd..b0842f11bd 100644 --- a/src/safariwebextension/CMakeLists.txt +++ b/src/safariwebextension/CMakeLists.txt @@ -1,19 +1,14 @@ cmake_minimum_required(VERSION 3.10) -# Specify the Swift source -set(SWIFT_SOURCE "${CMAKE_SOURCE_DIR}/src/safariwebextension/SafariWebExtensionHandler.swift") - -# Create an object file from the Swift source -add_custom_command( - OUTPUT ${CMAKE_BINARY_DIR}/SafariWebExtensionHandler.o - COMMAND swiftc -module-name KeePassXCSafariWebExtension -c ${SWIFT_SOURCE} -o ${CMAKE_BINARY_DIR}/SafariWebExtensionHandler.o - DEPENDS ${SWIFT_SOURCE} - COMMENT "Using Swift compiler for SafariWebExtensionHandler.swift" +set(SOURCES + SafariWebExtensionHandler.mm ) -add_executable(KeePassXCSafariWebExtension - ${CMAKE_BINARY_DIR}/SafariWebExtensionHandler.o -) +# Specify the Swift source +add_executable(KeePassXCSafariWebExtension ${SOURCES}) + +target_compile_definitions(KeePassXCSafariWebExtension PRIVATE APP_GROUP_IDENTIFIER="${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER}") +target_link_libraries(KeePassXCSafariWebExtension "-framework Foundation -framework SafariServices") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -e _NSExtensionMain") diff --git a/src/safariwebextension/Info.plist b/src/safariwebextension/Info.plist index 4f808b784a..78efe4fe37 100644 --- a/src/safariwebextension/Info.plist +++ b/src/safariwebextension/Info.plist @@ -24,7 +24,7 @@ NSExtensionPointIdentifier com.apple.Safari.web-extension NSExtensionPrincipalClass - KeePassXCSafariWebExtension.SafariWebExtensionHandler + SafariWebExtensionHandler diff --git a/src/safariwebextension/SafariWebExtensionHandler.h b/src/safariwebextension/SafariWebExtensionHandler.h new file mode 100644 index 0000000000..f654c144ee --- /dev/null +++ b/src/safariwebextension/SafariWebExtensionHandler.h @@ -0,0 +1,15 @@ +#import + +extern NSString * const SafariWebExtensionHandlerErrorDomain; + +typedef NS_ENUM(NSInteger, SafariWebExtensionHandlerError) { + SafariWebExtensionHandlerErrorInvalidRequest = 1, + SafariWebExtensionHandlerErrorSocketConnectionFailed, + SafariWebExtensionHandlerErrorSocketWriteFailed, + SafariWebExtensionHandlerErrorSocketReadFailed, + SafariWebExtensionHandlerErrorInvalidJSONResponse, +}; + +@interface SafariWebExtensionHandler : NSObject + +@end diff --git a/src/safariwebextension/SafariWebExtensionHandler.mm b/src/safariwebextension/SafariWebExtensionHandler.mm new file mode 100644 index 0000000000..806ddbf78c --- /dev/null +++ b/src/safariwebextension/SafariWebExtensionHandler.mm @@ -0,0 +1,119 @@ +#include "SafariWebExtensionHandler.h" + +#include +#include + +NSString * const SafariWebExtensionHandlerErrorDomain = @"org.keepassxc.keepassxc.SafariWebExtensionHandlerError"; + +NSString *applicationGroupIdentifier = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"APP_GROUP_IDENTIFIER"];; +NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:applicationGroupIdentifier]; +NSString *socketFileName = @"KeePassXC.BrowserServer"; +NSURL *socketURL = [containerURL URLByAppendingPathComponent:socketFileName]; +int32_t socketFD = -1; +int32_t maxMessageLength = 1024 * 1024; + +@implementation SafariWebExtensionHandler + +- (void)beginRequestWithExtensionContext:(NSExtensionContext *)context { + NSExtensionItem *request = context.inputItems.firstObject; + if (!request) { + [context cancelRequestWithError:[NSError errorWithDomain:SafariWebExtensionHandlerErrorDomain code:SafariWebExtensionHandlerErrorInvalidRequest userInfo:@{NSLocalizedDescriptionKey: @"No input items."}]]; + return; + } + + id message = nil; + if (@available(iOS 15.0, macOS 11.0, *)) { + message = request.userInfo[SFExtensionMessageKey]; + } else { + message = request.userInfo[@"message"]; + } + + if (!message) { + [context cancelRequestWithError:[NSError errorWithDomain:SafariWebExtensionHandlerErrorDomain code:SafariWebExtensionHandlerErrorInvalidRequest userInfo:@{NSLocalizedDescriptionKey: @"No message found."}]]; + return; + } + + NSError *serializationError = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:message options:0 error:&serializationError]; + if (!jsonData) { + [context cancelRequestWithError:[NSError errorWithDomain:SafariWebExtensionHandlerErrorDomain code:SafariWebExtensionHandlerErrorInvalidRequest userInfo:@{NSLocalizedDescriptionKey: @"JSON serialization failed."}]]; + return; + } + + if (![self connectSocket]) { + [context cancelRequestWithError:[NSError errorWithDomain:SafariWebExtensionHandlerErrorDomain code:SafariWebExtensionHandlerErrorSocketConnectionFailed userInfo:@{NSLocalizedDescriptionKey: @"Could not connect to socket."}]]; + return; + } + + ssize_t bytesWritten = write(socketFD, jsonData.bytes, jsonData.length); + if (bytesWritten == -1) { + [self closeSocket]; + [context cancelRequestWithError:[NSError errorWithDomain:SafariWebExtensionHandlerErrorDomain code:SafariWebExtensionHandlerErrorSocketWriteFailed userInfo:@{NSLocalizedDescriptionKey: @"Write to socket failed."}]]; + return; + } + + NSMutableData *buffer = [NSMutableData dataWithLength:maxMessageLength]; + ssize_t bytesRead = read(socketFD, [buffer mutableBytes], maxMessageLength); + if (bytesRead <= 0) { + [self closeSocket]; + [context cancelRequestWithError:[NSError errorWithDomain:SafariWebExtensionHandlerErrorDomain code:SafariWebExtensionHandlerErrorSocketReadFailed userInfo:@{NSLocalizedDescriptionKey: @"Read from socket failed."}]]; + return; + } + + NSData *responseData = [NSData dataWithBytesNoCopy:[buffer mutableBytes] length:bytesRead freeWhenDone:NO]; + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil]; + if (!jsonResponse) { + [context cancelRequestWithError:[NSError errorWithDomain:SafariWebExtensionHandlerErrorDomain code:SafariWebExtensionHandlerErrorInvalidJSONResponse userInfo:@{NSLocalizedDescriptionKey: @"Response JSON parsing failed."}]]; + return; + } + + NSExtensionItem *responseItem = [[NSExtensionItem alloc] init]; + if (@available(iOS 15.0, macOS 11.0, *)) { + responseItem.userInfo = @{ SFExtensionMessageKey: jsonResponse }; + } else { + responseItem.userInfo = @{ @"message": jsonResponse }; + } + + [context completeRequestReturningItems:@[responseItem] completionHandler:nil]; +} + +- (bool)connectSocket { + if (socketFD != -1) return YES; + + socketFD = socket(AF_UNIX, SOCK_STREAM, 0); + if (socketFD == -1) return NO; + + int optval = 1; + if (setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) { + [self closeSocket]; + return NO; + } + + int max = 1024 * 1024; + if (setsockopt(socketFD, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&max), sizeof(max)) == -1) { + [self closeSocket]; + return NO; + } + + struct sockaddr_un address = { + .sun_family = AF_UNIX, + }; + strncpy(address.sun_path, [socketURL fileSystemRepresentation], sizeof(address.sun_path) - 1); + + int result = connect(socketFD, reinterpret_cast(&address), sizeof(struct sockaddr_un)); + if (result != 0) { + [self closeSocket]; + return NO; + } + + return YES; +} + +- (void)closeSocket { + if (socketFD != -1) { + close(socketFD); + socketFD = -1; + } +} + +@end diff --git a/src/safariwebextension/SafariWebExtensionHandler.swift b/src/safariwebextension/SafariWebExtensionHandler.swift deleted file mode 100644 index 016c676f24..0000000000 --- a/src/safariwebextension/SafariWebExtensionHandler.swift +++ /dev/null @@ -1,168 +0,0 @@ -import SafariServices -import os.log - -// SEB_TODO: Convert this to Objective C++ (maybe) -class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { - let SocketFileName = "KeePassXC.BrowserServer" - let applicationGroupIdentifier = - Bundle.main.object(forInfoDictionaryKey: "APP_GROUP_IDENTIFIER") as! String - static var socketFD: Int32 = -1 - static var socketConnected = false - var maxMessageLength: Int32 = 1024 * 1024 - - private var logger = Logger( - subsystem: Bundle.main.bundleIdentifier!, - category: String(describing: SafariWebExtensionHandler.self) - ) - - var socketPath: String? { - FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: applicationGroupIdentifier)?.appending( - component: SocketFileName - ).path(percentEncoded: false) - } - - func closeSocket() { - if Self.socketFD != -1 { - logger.info("Closing socket") - close(Self.socketFD) - Self.socketFD = -1 - } - } - - func connectSocket() -> Bool { - // Reuse socket - guard Self.socketFD == -1 else { return true } - - guard let socketPath else { return false } - - guard FileManager.default.fileExists(atPath: socketPath) else { - logger.error("Socket file does not exist") - return false - } - - Self.socketFD = socket(AF_UNIX, SOCK_STREAM, 0) - guard Self.socketFD > 0 else { - logger.error("Failed to create socket") - return false - } - - var optval: Int = 1 // Use 1 to enable the option, 0 to disable - guard - setsockopt( - Self.socketFD, SOL_SOCKET, SO_REUSEADDR, &optval, - socklen_t(MemoryLayout.size)) != -1 - else { - logger.error("setsockopt error: \(errno)") - return false - } - - guard - setsockopt( - Self.socketFD, SOL_SOCKET, SO_SNDBUF, &maxMessageLength, - socklen_t(MemoryLayout.size(ofValue: maxMessageLength))) != -1 - else { - logger.error("setsockopt error") - return false - } - - var address = sockaddr_un() - address.sun_family = sa_family_t(AF_UNIX) - - withUnsafeMutableBytes(of: &address.sun_path) { ptr in - socketPath.utf8CString.withUnsafeBytes { bytes in - ptr.copyBytes(from: bytes) - } - } - - let result = withUnsafePointer(to: &address) { - $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { - connect(Self.socketFD, $0, socklen_t(MemoryLayout.size)) - } - } - - if result != 0 { - logger.error("Failed to connect to socket: \(errno)") - close(Self.socketFD) - Self.socketFD = -1 - return false - } - - return true - } - - func beginRequest(with context: NSExtensionContext) { - let request = context.inputItems.first as? NSExtensionItem - - let message: Any? - if #available(iOS 15.0, macOS 11.0, *) { - message = request?.userInfo?[SFExtensionMessageKey] - } else { - message = request?.userInfo?["message"] - } - - guard let message = message as? [String: Any], - let data = try? JSONSerialization.data(withJSONObject: message as Any) - else { - context.cancelRequest(withError: SafariWebExtensionHandlerError.invalidRequest) - return - } - - if !Self.socketConnected { - logger.info("Socket is not connected") - if !connectSocket() { - closeSocket() - logger.error("Socket not connected") - context.cancelRequest( - withError: SafariWebExtensionHandlerError.socketConnectionFailed) - return - } - - Self.socketConnected = true - } else { - logger.info("Socket is connected") - } - - let bytesWritten = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> Int in - write(Self.socketFD, bytes.baseAddress, data.count) - } - - if bytesWritten == -1 { - logger.error("Cannot write to socket \(errno)") - context.cancelRequest(withError: SafariWebExtensionHandlerError.socketWriteFailed) - return - } - logger.debug("Sent message of \(data.count) bytes") - - var buffer = [UInt8](repeating: 0, count: Int(maxMessageLength)) - let bytesRead = read(Self.socketFD, &buffer, buffer.count) - - guard bytesRead > 0 else { - logger.error("No bytes read") - context.cancelRequest(withError: SafariWebExtensionHandlerError.socketReadFailed) - return - } - - let res = Data(buffer[0.. Date: Fri, 1 Aug 2025 18:24:03 +0200 Subject: [PATCH 3/6] Add submodule for keepassxc-browser --- .gitmodules | 1 + src/safariwebextension/keepassxc-browser | 1 + 2 files changed, 2 insertions(+) create mode 160000 src/safariwebextension/keepassxc-browser diff --git a/.gitmodules b/.gitmodules index a44fabc61d..231fde3536 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "src/safariwebextension/keepassxc-browser"] path = src/safariwebextension/keepassxc-browser url = git@github.com:sebastianlivoni/keepassxc-browser.git + branch = feature/safari diff --git a/src/safariwebextension/keepassxc-browser b/src/safariwebextension/keepassxc-browser new file mode 160000 index 0000000000..48905b0e50 --- /dev/null +++ b/src/safariwebextension/keepassxc-browser @@ -0,0 +1 @@ +Subproject commit 48905b0e50eb6bdbbc792d1cdc3cf1ef53252b2a From 84e2c4b32a03ceb685605f3ff2034814db3f10a0 Mon Sep 17 00:00:00 2001 From: Sebastian Livoni <29739749+sebastianlivoni@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:50:27 +0200 Subject: [PATCH 4/6] Introduce caching for web extension socket check --- src/browser/BrowserHost.cpp | 9 ++++++++- src/browser/BrowserHost.h | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/browser/BrowserHost.cpp b/src/browser/BrowserHost.cpp index 4b100b69c1..ac5a7c6a00 100644 --- a/src/browser/BrowserHost.cpp +++ b/src/browser/BrowserHost.cpp @@ -66,6 +66,10 @@ void BrowserHost::proxyConnected() auto socket = m_localServer->nextPendingConnection(); if (socket) { m_socketList.append(socket); + #ifdef WITH_XC_SAFARI_WEB_EXTENSION + bool isSafariSocket = safariWebExtensionHelper()->isSafariWebExtension(socket); + m_safariWebExtensionCache.insert(socket, isSafariSocket); + #endif connect(socket, SIGNAL(readyRead()), this, SLOT(readProxyMessage())); connect(socket, SIGNAL(disconnected()), this, SLOT(proxyDisconnected())); } @@ -106,7 +110,7 @@ void BrowserHost::broadcastClientMessage(const QJsonObject& json) // Send message to all non-Safari sockets for (const auto socket : m_socketList) { #ifdef WITH_XC_SAFARI_WEB_EXTENSION - if (safariWebExtensionHelper()->isSafariWebExtension(socket)) { + if (m_safariWebExtensionCache.contains(socket)) { containsSafariWebExtensionSocket = true; continue; // Skip Safari web extension sockets because we are using SFSafariApplication instead } @@ -141,4 +145,7 @@ void BrowserHost::proxyDisconnected() { auto socket = qobject_cast(QObject::sender()); m_socketList.removeOne(socket); + #ifdef WITH_XC_SAFARI_WEB_EXTENSION + m_safariWebExtensionCache.remove(socket); + #endif } diff --git a/src/browser/BrowserHost.h b/src/browser/BrowserHost.h index f3620c04cc..0342512574 100644 --- a/src/browser/BrowserHost.h +++ b/src/browser/BrowserHost.h @@ -21,6 +21,9 @@ #include #include #include +#ifdef WITH_XC_SAFARI_WEB_EXTENSION +#include +#endif class QLocalServer; class QLocalSocket; @@ -54,6 +57,9 @@ private slots: private: QPointer m_localServer; QList m_socketList; +#ifdef WITH_XC_SAFARI_WEB_EXTENSION + QHash m_safariWebExtensionCache; +#endif }; #endif // KEEPASSXC_NATIVEMESSAGINGHOST_H From 08503bdd5c15636eaa975bcb4bea236d5a632ca2 Mon Sep 17 00:00:00 2001 From: Sebastian Livoni <29739749+sebastianlivoni@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:55:06 +0200 Subject: [PATCH 5/6] Update submodule url to https instead of ssh --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 231fde3536..9f8cf44deb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "src/safariwebextension/keepassxc-browser"] path = src/safariwebextension/keepassxc-browser - url = git@github.com:sebastianlivoni/keepassxc-browser.git + url = https://github.com/sebastianlivoni/keepassxc-browser.git branch = feature/safari From 95f3c5487a4e14a78b0442e44d37e762e767291c Mon Sep 17 00:00:00 2001 From: Sebastian Livoni <29739749+sebastianlivoni@users.noreply.github.com> Date: Fri, 1 Aug 2025 20:45:59 +0200 Subject: [PATCH 6/6] Fix formatting --- cmake/CLangFormat.cmake | 3 ++- src/browser/BrowserHost.cpp | 8 ++++---- src/browser/BrowserSettingsWidget.cpp | 2 +- src/browser/BrowserSharedMac.h | 2 +- src/safariwebextension/CMakeLists.txt | 2 +- src/safariwebextension/SafariWebExtensionCheckbox.h | 6 +++--- src/safariwebextension/SafariWebExtensionHelper.h | 3 ++- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/cmake/CLangFormat.cmake b/cmake/CLangFormat.cmake index 9ddc4edb29..e34ddb662d 100644 --- a/cmake/CLangFormat.cmake +++ b/cmake/CLangFormat.cmake @@ -29,7 +29,8 @@ set(EXCLUDED_FILES src/gui/tag/TagsEdit.\\* tests/modeltest.\\* # objective-c files - src/core/ScreenLockListenerMac.\\*) + src/core/ScreenLockListenerMac.\\* + src/safariwebextension/SafariWebExtensionHandler.\\*) set(FIND_EXCLUDE_DIR_EXPR "") foreach(EXCLUDE ${EXCLUDED_DIRS}) diff --git a/src/browser/BrowserHost.cpp b/src/browser/BrowserHost.cpp index ac5a7c6a00..ce078b1431 100644 --- a/src/browser/BrowserHost.cpp +++ b/src/browser/BrowserHost.cpp @@ -66,10 +66,10 @@ void BrowserHost::proxyConnected() auto socket = m_localServer->nextPendingConnection(); if (socket) { m_socketList.append(socket); - #ifdef WITH_XC_SAFARI_WEB_EXTENSION +#ifdef WITH_XC_SAFARI_WEB_EXTENSION bool isSafariSocket = safariWebExtensionHelper()->isSafariWebExtension(socket); m_safariWebExtensionCache.insert(socket, isSafariSocket); - #endif +#endif connect(socket, SIGNAL(readyRead()), this, SLOT(readProxyMessage())); connect(socket, SIGNAL(disconnected()), this, SLOT(proxyDisconnected())); } @@ -145,7 +145,7 @@ void BrowserHost::proxyDisconnected() { auto socket = qobject_cast(QObject::sender()); m_socketList.removeOne(socket); - #ifdef WITH_XC_SAFARI_WEB_EXTENSION +#ifdef WITH_XC_SAFARI_WEB_EXTENSION m_safariWebExtensionCache.remove(socket); - #endif +#endif } diff --git a/src/browser/BrowserSettingsWidget.cpp b/src/browser/BrowserSettingsWidget.cpp index 1659c7fe10..9a9a5b2b58 100644 --- a/src/browser/BrowserSettingsWidget.cpp +++ b/src/browser/BrowserSettingsWidget.cpp @@ -49,7 +49,7 @@ BrowserSettingsWidget::BrowserSettingsWidget(QWidget* parent) connect(m_ui->enableBrowserSupport, SIGNAL(toggled(bool)), SLOT(validateProxyLocation())); #ifdef WITH_XC_SAFARI_WEB_EXTENSION - SafariWebExtensionCheckbox *safariCheckbox = new SafariWebExtensionCheckbox(); + SafariWebExtensionCheckbox* safariCheckbox = new SafariWebExtensionCheckbox(); safariCheckbox->setText("Safari"); safariCheckbox->setChecked(false); diff --git a/src/browser/BrowserSharedMac.h b/src/browser/BrowserSharedMac.h index 24f7e60cff..ca16beaee7 100644 --- a/src/browser/BrowserSharedMac.h +++ b/src/browser/BrowserSharedMac.h @@ -3,4 +3,4 @@ namespace BrowserShared { QString macOSLocalServerPath(); -} \ No newline at end of file +} diff --git a/src/safariwebextension/CMakeLists.txt b/src/safariwebextension/CMakeLists.txt index b0842f11bd..dae79c0805 100644 --- a/src/safariwebextension/CMakeLists.txt +++ b/src/safariwebextension/CMakeLists.txt @@ -38,7 +38,7 @@ execute_process( ) if (NOT result EQUAL 0) - message(FATAL_ERROR "npm ci failed with error code ${result}") + message(FATAL_ERROR "npm ci failed with error code ${result}. Is the SafariWebExtension submodule cloned?") endif() file(MAKE_DIRECTORY ${BINARY_RESOURCES_DIR}) diff --git a/src/safariwebextension/SafariWebExtensionCheckbox.h b/src/safariwebextension/SafariWebExtensionCheckbox.h index ddaea12ad9..769f50b1d2 100644 --- a/src/safariwebextension/SafariWebExtensionCheckbox.h +++ b/src/safariwebextension/SafariWebExtensionCheckbox.h @@ -7,11 +7,11 @@ class SafariWebExtensionCheckbox : public QCheckBox Q_OBJECT public: - explicit SafariWebExtensionCheckbox(QWidget *parent = nullptr); + explicit SafariWebExtensionCheckbox(QWidget* parent = nullptr); protected: - void mousePressEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent* e) override; private slots: void onApplicationStateChanged(Qt::ApplicationState state); -}; \ No newline at end of file +}; diff --git a/src/safariwebextension/SafariWebExtensionHelper.h b/src/safariwebextension/SafariWebExtensionHelper.h index f7e3ddb5f9..2cbc7be425 100644 --- a/src/safariwebextension/SafariWebExtensionHelper.h +++ b/src/safariwebextension/SafariWebExtensionHelper.h @@ -2,7 +2,8 @@ class QJsonObject; class QLocalSocket; class QString; -class SafariWebExtensionHelper { +class SafariWebExtensionHelper +{ public: static SafariWebExtensionHelper* instance();