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();