diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..9f8cf44deb
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,4 @@
+[submodule "src/safariwebextension/keepassxc-browser"]
+ path = src/safariwebextension/keepassxc-browser
+ url = https://github.com/sebastianlivoni/keepassxc-browser.git
+ branch = feature/safari
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/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/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..ce078b1431 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
@@ -62,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()));
}
@@ -94,9 +102,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 (m_safariWebExtensionCache.contains(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)
@@ -118,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
diff --git a/src/browser/BrowserSettingsWidget.cpp b/src/browser/BrowserSettingsWidget.cpp
index 5a4ccce8dd..9a9a5b2b58 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..ca16beaee7
--- /dev/null
+++ b/src/browser/BrowserSharedMac.h
@@ -0,0 +1,6 @@
+#include
+
+namespace BrowserShared
+{
+ QString macOSLocalServerPath();
+}
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..dae79c0805
--- /dev/null
+++ b/src/safariwebextension/CMakeLists.txt
@@ -0,0 +1,67 @@
+cmake_minimum_required(VERSION 3.10)
+
+set(SOURCES
+ SafariWebExtensionHandler.mm
+)
+
+# 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")
+
+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}. Is the SafariWebExtension submodule cloned?")
+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..78efe4fe37
--- /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
+ SafariWebExtensionHandler
+
+
+
diff --git a/src/safariwebextension/SafariWebExtensionCheckbox.h b/src/safariwebextension/SafariWebExtensionCheckbox.h
new file mode 100644
index 0000000000..769f50b1d2
--- /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);
+};
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.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/SafariWebExtensionHelper.h b/src/safariwebextension/SafariWebExtensionHelper.h
new file mode 100644
index 0000000000..2cbc7be425
--- /dev/null
+++ b/src/safariwebextension/SafariWebExtensionHelper.h
@@ -0,0 +1,23 @@
+class QJsonObject;
+class QLocalSocket;
+class QString;
+
+class SafariWebExtensionHelper
+{
+
+public:
+ static SafariWebExtensionHelper* instance();
+
+ bool isSafariWebExtension(QLocalSocket* socket);
+ void broadcastClientMessage(const QString& reply);
+
+private:
+ SafariWebExtensionHelper() = default;
+ SafariWebExtensionHelper(const SafariWebExtensionHelper&) = delete;
+ SafariWebExtensionHelper& operator=(const SafariWebExtensionHelper&) = delete;
+};
+
+inline SafariWebExtensionHelper* safariWebExtensionHelper()
+{
+ return SafariWebExtensionHelper::instance();
+}
diff --git a/src/safariwebextension/SafariWebExtensionHelper.mm b/src/safariwebextension/SafariWebExtensionHelper.mm
new file mode 100644
index 0000000000..e9f1e18363
--- /dev/null
+++ b/src/safariwebextension/SafariWebExtensionHelper.mm
@@ -0,0 +1,101 @@
+#include "SafariWebExtensionHelper.h"
+
+#include
+#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/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
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}
+
+
+