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} + + +