Compare commits

...

16 Commits

Author SHA1 Message Date
Janek Bevendorff
967dc5937f Fix Docker run cwd 2025-11-25 01:48:22 +01:00
Jonathan White
adba5095c3 Fix AppImage not finding Auto-Type library
* Fixes #12719 and Fixes #12721
* Also fixes missing <ul> in the appdata xml
2025-11-25 01:48:22 +01:00
Jonathan White
98bbad0a4c Fix setting entitlements on KeePassXC executable
* Fixes #12713
* Also fixes motorization to use the built packages instead of glob discovery
2025-11-25 01:08:47 +01:00
Janek Bevendorff
87c63ff9ee Update changelog 2025-11-24 11:52:18 +01:00
Jonathan White
cb24df7aae Add explicit encoding when reading text from files 2025-11-24 11:52:08 +01:00
Janek Bevendorff
0f3def03b6 Fix release-tool merge cmd and rename to "tag" 2025-11-24 11:52:08 +01:00
Jonathan White
97d4edd9b8 Take delays into account when Auto-Type TOTP values
* Fixes #12682
2025-11-23 16:13:26 -05:00
Jonathan White
72308a1706 Prevent launch on installer finish when run as SYSTEM
* This condition will only happen when KeePassXC is installed by MECM or similar deployment tool. This prevents accidental launch on exit if the packager forgot to set LAUNCHAPPONEXIT=0 in the msiexec call. Allowing launch on exit in these conditions would potentially allow a non-privileged user to assume the role of SYSTEM through the KeePassXC application.

* Fixes weakness reported by HackAndPwn, thank you!
2025-11-23 13:31:40 +01:00
copilot-swe-agent[bot]
a5c9ffbef7 Fix CSV import regression with root group names
Fix the issue where CSV export/import creates nested root groups when the database has a custom root group name.

Added comprehensive tests to verify the fix works for both custom and default root group names, and preserves existing behavior for single-level groups.

Implement heuristic approach for CSV import root group detection:

- Analyzes all CSV rows before processing to find consistent first path components
- Only skips the first component if it appears in 80% or more of paths
- Handles absolute paths (starting with "/") by ignoring them in analysis
- Preserves existing behavior when no clear common root is found

Co-authored-by: droidmonkey <2809491+droidmonkey@users.noreply.github.com>
2025-11-23 13:09:23 +01:00
Janek Bevendorff
2900f919c8 Fix AppRun path issue, fixes #12612 2025-11-22 19:23:12 -05:00
dependabot[bot]
6c59b5db98 Bump golang.org/x/crypto in /utils/keepassxc-cr-recovery
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.35.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.35.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-22 17:51:53 -05:00
Janek Bevendorff
1a4e9ca4e2 Correctly restore window geometry when minimised to tray on startup
Fixes #10537
Fixes #11982
2025-11-22 17:51:24 -05:00
Jonathan White
a2e7132ead Support building with clang on Windows 2025-11-22 17:51:02 -05:00
Janek Bevendorff
4e59c1c579 Integrate macOS code signing into CMake
Moves code signing from the release-tool to CMake and unifies the Windows-equivalent code.
2025-11-22 17:51:02 -05:00
Janek Bevendorff
c09ba0113b Set default idle lock timeout to 15 minutes.
Addendum to #12689

The previous default of 240 seconds was too low. If we enable the lock
timeout by default, we should also set a more lenient default timeout by
default.
2025-11-22 23:27:39 +01:00
xboxones1
f39e0937b9 Fix markdown type for >= QT 5.15.18 (#12654) and advance vcpkg baseline
- Fix markdown type for >= QT 5.15.18 (#12654) 

- Fix deprecation warnings about implicit capturing of "this"

- Advance vcpkg baseline to fix macOS Qt building
  Fixes Qt build errors on macOS 26 Tahoe.
  See https://github.com/microsoft/vcpkg/pull/48298
2025-11-22 21:26:40 +01:00
28 changed files with 824 additions and 367 deletions

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@ CMakePresets.json
CMakeUserPresets.json CMakeUserPresets.json
.vs/ .vs/
out/ out/
\.clangd
# vcpkg # vcpkg
vcpkg_installed*/ vcpkg_installed*/

View File

@@ -3,6 +3,75 @@
## 2.8.0 (Pending) ## 2.8.0 (Pending)
* Placeholder for future release notes * Placeholder for future release notes
## 2.7.11 (2025-11-23)
### Changes
- Add image, HTML, Markdown preview, and text editing support to inline attachment viewer [#12085, #12244, #12654]
- Add database merge confirmation dialog [#10173]
- Add option to auto-generate a password for new entries [#12593]
- Add support for group sync in KeeShare [#11593]
- Add {UUID} placeholder for use in references [#12511]
- Add “Wait for Enter” search option [#12263]
- Add keyboard shortcut to “Jump to Group” from search results [#12225]
- Add predefined search for TOTP entries [#12199]
- Add confirmation when closing database via ESC key [#11963]
- Add support for escaping placeholder expressions [#11904]
- Reduce tab indentation width in notes fields [#11919]
- Cap default Argon2 parallelism when creating a new database [#11853]
- Database lock after inactivity now enabled by default and set to 900 seconds [#12689, #12609]
- Copying TOTP now opens setup dialog if none is configured for entry [#12584]
- Make double click action configurable [#12322]
- Remove unused “Last Accessed” from GUI [#12602]
- Auto-Type: Add more granular confirmation settings [#12370]
- Auto-Type: Add URL typing preset and add copy options to menu [#12341]
- Browser: Do not allow sites automatically if entry added from browser extension [#12413]
- Browser: Add options to restrict exposed groups [#9852, #12119]
- Bitwarden Import: Add support for timestamps and password history [#12588]
- macOS: Add Liquid Glass icon [#12642]
- macOS: Remove theme-based menubar icon toggle [#12685]
- macOS: Add Window and Help menus [#12357]
- Windows: Add option to add KeePassXC to PATH during installation [#12171]
### Fixes
- Fix window geometry not being restored properly when KeePassXC starts in tray [#12683]
- Fix potential database truncation when using direct write save method with YubiKeys [#11841]
- Fix issue with database backup saving [#11874]
- Fix UI lockups during startup with multiple tabs [#12053]
- Fix keyboard shortcuts when menubar is hidden [#12431]
- Fix clipboard being cleared on exit even if no password was copied [#12603]
- Fix single-instance detection when username contains invalid filename characters [#12559]
- Fix “Search Wait for Enter” setting not being save [#12614]
- Fix hotkey accelerators not being escaped properly on database tabs [#12630]
- Fix confusing error if user cancels out of key file edit dialog [#12639]
- Fix issues with saved searches and “Press Enter to Search” option [#12314]
- Fix URL wildcard matching [#12257]
- Fix TOTP visibility on unlock and settings change [#12220]
- Fix KeeShare entries with reference attributes not updating [#11809]
- Fix sort order not being maintained when toggling filters in database reports [#11849]
- Fix several UI font and layout issues [#11967, #12102]
- Prevent mouse wheel scroll on edit username field [#12398]
- Improve base translation consistency [#12432]
- Improve inactivity timer [#12246]
- Documentation improvements [#12373, #12506]
- Browser: Fix ordering of clientDataJSON in Passkey response object [#12120]
- Browser: Fix URL matching for additional URLs [#12196]
- Browser: Fix group settings inheritance [#12368]
- Browser: Allow read-only native messaging config files [#12236]
- Browser: Optimise entry iteration in browser access control dialog [#11817]
- Browser: Fix “Do not ask permission for HTTP Basic Auth” option [#11871]
- Browser: Fix native messaging path for Tor Browser launcher on Linux [#12005]
- Auto-Type: Fix empty window behaviour [#12622]
- Auto-Type: Take delays into account when typing TOTP [#12691]
- SSH Agent: Fix out-of-memory crash with malformed SSH keys [#12606]
- CSV Import: Fix modified and creation time import [#12379]
- CSV Import: Fix duplication of root groups on import [#12240]
- Proton Pass Import: Fix email addresses not being imported when no username set [#11888]
- macOS: Fix secure input getting stuck [#11928]
- Windows: Prevent launch as SYSTEM user from MSI installer [#12705]
- Windows: Remove broken check for MSVC Redistributable from MSI installer [#11950]
- Linux: Fix startup delay due to StartupNotify setting in desktop file [#12306]
- Linux: Fix memory initialisation when --pw-stdin is used with a pipe [#12050]
## 2.7.10 (2025-03-02) ## 2.7.10 (2025-03-02)
### Changes ### Changes

View File

@@ -60,10 +60,17 @@ option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF)
option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON) option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON)
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
option(WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API." OFF) option(WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API." OFF)
set(WITH_XC_X11 ON CACHE BOOL "Enable building with X11 deps")
endif() endif()
option(WITH_XC_DOCS "Enable building of documentation" ON) option(WITH_XC_DOCS "Enable building of documentation" ON)
if(WIN32 OR APPLE)
set(WITH_XC_X11 ON CACHE BOOL "Enable building with X11 deps") set(WITH_XC_CODESIGN_IDENTITY "" CACHE STRING "Certificate to be used for signing binaries before packaging.")
if(WIN32)
set(WITH_XC_CODESIGN_TIMESTAMP_URL "http://timestamp.sectigo.com" CACHE STRING "Timestamp URL for Windows code signing.")
elseif(APPLE)
set(WITH_XC_NOTARY_KEYCHAIN_PROFILE "" CACHE STRING "Keychain profile name for stored Apple notarization credentials.")
endif()
endif()
if(APPLE) if(APPLE)
# Perform the platform checks before applying the stricter compiler flags. # Perform the platform checks before applying the stricter compiler flags.
@@ -222,6 +229,11 @@ if("${CMAKE_SIZEOF_VOID_P}" EQUAL "4")
set(IS_32BIT TRUE) set(IS_32BIT TRUE)
endif() endif()
if("${CMAKE_CXX_COMPILER}" MATCHES "clang-cl(.exe)?$")
# clang-cl uses MSVC compiler flags
set(MSVC 1)
set(CMAKE_COMPILER_IS_CLANG_MSVC 1)
else()
set(CLANG_COMPILER_ID_REGEX "^(Apple)?[Cc]lang$") set(CLANG_COMPILER_ID_REGEX "^(Apple)?[Cc]lang$")
if("${CMAKE_C_COMPILER}" MATCHES "clang$" if("${CMAKE_C_COMPILER}" MATCHES "clang$"
OR "${CMAKE_EXTRA_GENERATOR_C_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__" OR "${CMAKE_EXTRA_GENERATOR_C_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__"
@@ -234,6 +246,7 @@ if("${CMAKE_CXX_COMPILER}" MATCHES "clang(\\+\\+)?$"
OR "${CMAKE_CXX_COMPILER_ID}" MATCHES ${CLANG_COMPILER_ID_REGEX}) OR "${CMAKE_CXX_COMPILER_ID}" MATCHES ${CLANG_COMPILER_ID_REGEX})
set(CMAKE_COMPILER_IS_CLANGXX 1) set(CMAKE_COMPILER_IS_CLANGXX 1)
endif() endif()
endif()
macro(add_gcc_compiler_cxxflags FLAGS) macro(add_gcc_compiler_cxxflags FLAGS)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_COMPILER_IS_CLANGXX) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_COMPILER_IS_CLANGXX)
@@ -395,7 +408,10 @@ if (MSVC)
if(MSVC_TOOLSET_VERSION LESS 141) if(MSVC_TOOLSET_VERSION LESS 141)
message(FATAL_ERROR "Only Microsoft Visual Studio 17 and newer are supported!") message(FATAL_ERROR "Only Microsoft Visual Studio 17 and newer are supported!")
endif() endif()
add_compile_options(/permissive- /utf-8 /MP) add_compile_options(/permissive- /utf-8)
# Clang-cl does not support /MP, /Zf, or /fsanitize=address
if (NOT CMAKE_COMPILER_IS_CLANG_MSVC)
add_compile_options(/MP)
if(IS_DEBUG_BUILD) if(IS_DEBUG_BUILD)
add_compile_options(/Zf) add_compile_options(/Zf)
if(MSVC_TOOLSET_VERSION GREATER 141) if(MSVC_TOOLSET_VERSION GREATER 141)
@@ -403,6 +419,7 @@ if (MSVC)
endif() endif()
endif() endif()
endif() endif()
endif()
if(WIN32) if(WIN32)
set(CMAKE_RC_COMPILER_INIT windres) set(CMAKE_RC_COMPILER_INIT windres)
@@ -415,7 +432,7 @@ if(WIN32)
# By default MSVC enables NXCOMPAT # By default MSVC enables NXCOMPAT
add_compile_options(/guard:cf) add_compile_options(/guard:cf)
add_link_options(/DYNAMICBASE /HIGHENTROPYVA /GUARD:CF) add_link_options(/DYNAMICBASE /HIGHENTROPYVA /GUARD:CF)
else(MINGW) else()
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase")
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase")
# Enable high entropy ASLR for 64-bit builds # Enable high entropy ASLR for 64-bit builds
@@ -425,6 +442,8 @@ if(WIN32)
endif() endif()
endif() endif()
endif() endif()
# Determine if we can link against the Windows SDK, used for Windows Hello support
find_library(WINSDK WindowsApp.lib)
endif() endif()
if(APPLE AND WITH_APP_BUNDLE OR WIN32) if(APPLE AND WITH_APP_BUNDLE OR WIN32)

View File

@@ -36,7 +36,7 @@ find_library(
NAMES ${BOTAN_NAMES} NAMES ${BOTAN_NAMES}
PATH_SUFFIXES release/lib lib PATH_SUFFIXES release/lib lib
DOC "The Botan (release) library") DOC "The Botan (release) library")
if(MSVC) if(WIN32 AND NOT MINGW)
find_library( find_library(
BOTAN_LIBRARY_DEBUG BOTAN_LIBRARY_DEBUG
NAMES ${BOTAN_NAMES_DEBUG} NAMES ${BOTAN_NAMES_DEBUG}
@@ -55,7 +55,7 @@ endif()
if(BOTAN_FOUND) if(BOTAN_FOUND)
set(BOTAN_INCLUDE_DIRS ${BOTAN_INCLUDE_DIR}) set(BOTAN_INCLUDE_DIRS ${BOTAN_INCLUDE_DIR})
if(MSVC) if(WIN32 AND NOT MINGW)
set(BOTAN_LIBRARIES optimized ${BOTAN_LIBRARY} debug ${BOTAN_LIBRARY_DEBUG}) set(BOTAN_LIBRARIES optimized ${BOTAN_LIBRARY} debug ${BOTAN_LIBRARY_DEBUG})
else() else()
set(BOTAN_LIBRARIES ${BOTAN_LIBRARY}) set(BOTAN_LIBRARIES ${BOTAN_LIBRARY})

View File

@@ -15,7 +15,7 @@
find_path(QRENCODE_INCLUDE_DIR NAMES qrencode.h) find_path(QRENCODE_INCLUDE_DIR NAMES qrencode.h)
if(WIN32 AND MSVC) if(WIN32 AND NOT MINGW)
find_library(QRENCODE_LIBRARY_RELEASE qrencode) find_library(QRENCODE_LIBRARY_RELEASE qrencode)
find_library(QRENCODE_LIBRARY_DEBUG qrencoded) find_library(QRENCODE_LIBRARY_DEBUG qrencoded)
set(QRENCODE_LIBRARY optimized ${QRENCODE_LIBRARY_RELEASE} debug ${QRENCODE_LIBRARY_DEBUG}) set(QRENCODE_LIBRARY optimized ${QRENCODE_LIBRARY_RELEASE} debug ${QRENCODE_LIBRARY_DEBUG})

View File

@@ -0,0 +1,101 @@
# Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 or (at your option)
# version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# CPACK_PACKAGE_FILES is set only during POST_BUILD
if(NOT CPACK_PACKAGE_FILES) # PRE_BUILD: Sign binaries
set(PROGNAME "@PROGNAME@")
set(CODESIGN_IDENTITY "@WITH_XC_CODESIGN_IDENTITY@")
set(ENTITLEMENTS @MACOSX_BUNDLE_APPLE_ENTITLEMENTS@)
set(APP_DIR "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/ALL_IN_ONE/${PROGNAME}.app")
if(NOT CODESIGN_IDENTITY)
message(FATAL_ERROR "No codesign identity specified.")
endif()
message(STATUS "Codesign identity used: ${CODESIGN_IDENTITY}")
message(STATUS "Signing ${PROGNAME}.app, this may take while...")
# Sign all binaries
execute_process(
COMMAND xcrun codesign --sign=${CODESIGN_IDENTITY} --force --options=runtime --deep "${APP_DIR}"
RESULT_VARIABLE SIGN_RESULT
OUTPUT_VARIABLE SIGN_OUTPUT
ERROR_VARIABLE SIGN_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
ECHO_OUTPUT_VARIABLE
)
if (NOT SIGN_RESULT EQUAL 0)
message(FATAL_ERROR "Signing binaries failed: ${SIGN_ERROR}")
endif()
# (Re-)Sign main executable with --entitlements
execute_process(
COMMAND xcrun codesign --sign=${CODESIGN_IDENTITY} --force --options=runtime --entitlements=${ENTITLEMENTS} "${APP_DIR}/Contents/MacOS/${PROGNAME}"
RESULT_VARIABLE SIGN_RESULT
OUTPUT_VARIABLE SIGN_OUTPUT
ERROR_VARIABLE SIGN_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
ECHO_OUTPUT_VARIABLE
)
if (NOT SIGN_RESULT EQUAL 0)
message(FATAL_ERROR "Signing main binary failed: ${SIGN_ERROR}")
endif()
message(STATUS "${PROGNAME}.app signed successfully.")
else() # POST_BUILD: Notarize DMG
set(KEYCHAIN_PROFILE "@WITH_XC_NOTARY_KEYCHAIN_PROFILE@")
if(NOT KEYCHAIN_PROFILE)
message(FATAL_ERROR "No notarization credentials keychain profile specified.")
endif()
foreach(DMG_FILE ${CPACK_PACKAGE_FILES})
# Submit for notarization
message(STATUS "Submitting DMG bundle for notarization, this may take while...")
execute_process(
COMMAND xcrun notarytool submit --keychain-profile=${KEYCHAIN_PROFILE} --wait "${DMG_FILE}"
RESULT_VARIABLE NOTARIZE_RESULT
OUTPUT_VARIABLE NOTARIZE_OUTPUT
ERROR_VARIABLE NOTARIZE_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
ECHO_OUTPUT_VARIABLE
)
if (NOT NOTARIZE_RESULT EQUAL 0)
message(FATAL_ERROR "Notarization failed: ${NOTARIZE_ERROR}")
endif()
message(STATUS "DMG bundle notarized successfully.")
# Staple tickets
message(STATUS "Stapling notarization ticket...")
execute_process(
COMMAND xcrun stapler staple "${DMG_FILE}" && xcrun stapler validate "${DMG_FILE}"
RESULT_VARIABLE STAPLE_RESULT
OUTPUT_VARIABLE STAPLE_OUTPUT
ERROR_VARIABLE STAPLE_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
ECHO_OUTPUT_VARIABLE
)
if (NOT STAPLE_RESULT EQUAL 0)
message(FATAL_ERROR "Stapling failed: ${STAPLE_ERROR}")
endif()
message(STATUS "DMG bundle notarization ticket stapled successfully.")
endforeach()
endif()

View File

@@ -0,0 +1,79 @@
# Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 or (at your option)
# version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
set(INSTALL_DIR ${CPACK_TEMPORARY_INSTALL_DIRECTORY})
set(CODESIGN_IDENTITY @WITH_XC_CODESIGN_IDENTITY@)
set(TIMESTAMP_URL @WITH_XC_CODESIGN_TIMESTAMP_URL@)
if(CPACK_PACKAGE_FILES)
# This variable is set only during POST_BUILD, reset SIGN_FILES first
set(SIGN_FILES "")
foreach(PACKAGE_FILE ${CPACK_PACKAGE_FILES})
# Check each package file to see if it can be signed
if(PACKAGE_FILE MATCHES "\\.msix?$" OR PACKAGE_FILE MATCHES "\\.exe$")
message(STATUS "Adding ${PACKAGE_FILE} for signature")
list(APPEND SIGN_FILES "${PACKAGE_FILE}")
endif()
endforeach()
else()
# Setup portable zip file if building one
if(INSTALL_DIR MATCHES "/ZIP/")
file(TOUCH "${INSTALL_DIR}/.portable")
message(STATUS "Injected portable marker into ZIP file.")
endif()
# Find all dll and exe files in the install directory
file(GLOB_RECURSE SIGN_FILES
RELATIVE "${INSTALL_DIR}"
"${INSTALL_DIR}/*.dll"
"${INSTALL_DIR}/*.exe"
)
endif()
# Sign relevant binaries if requested
if(CODESIGN_IDENTITY AND SIGN_FILES)
# Find signtool in PATH or error out
find_program(SIGNTOOL signtool.exe QUIET)
if(NOT SIGNTOOL)
message(FATAL_ERROR "signtool.exe not found in PATH, correct or unset WITH_XC_CODESIGN_IDENTITY")
endif()
# Check that a certificate thumbprint was provided or error out
if(CODESIGN_IDENTITY STREQUAL "auto")
message(STATUS "Signing using best available certificate.")
set(CERT_OPTS /a)
else ()
message(STATUS "Signing using certificate with fingerprint ${CODESIGN_IDENTITY}.")
set(CERT_OPTS /sha1 ${CODESIGN_IDENTITY})
endif()
message(STATUS "Signing binary files, this may take a while...")
# Use cmd /c to enable pop-up for pin entry if needed
execute_process(
COMMAND cmd /c ${SIGNTOOL} sign /fd SHA256 ${CERT_OPTS} /tr ${TIMESTAMP_URL} /td SHA256 /d ${CPACK_PACKAGE_FILE_NAME} ${SIGN_FILES}
WORKING_DIRECTORY "${INSTALL_DIR}"
RESULT_VARIABLE SIGN_RESULT
OUTPUT_VARIABLE SIGN_OUTPUT
ERROR_VARIABLE SIGN_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
ECHO_OUTPUT_VARIABLE
)
if(NOT SIGN_RESULT EQUAL 0)
message(FATAL_ERROR "Signing binary files failed: ${SIGN_ERROR}")
endif()
message(STATUS "Binary files signed successfully.")
endif()

View File

@@ -1,71 +0,0 @@
# Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 or (at your option)
# version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
set(_installdir ${CPACK_TEMPORARY_INSTALL_DIRECTORY})
set(_sign @WITH_XC_SIGNINSTALL@)
set(_cert_thumbprint @WITH_XC_SIGNINSTALL_CERT@)
set(_timestamp_url @WITH_XC_SIGNINSTALL_TIMESTAMP_URL@)
# Setup portable zip file if building one
if(_installdir MATCHES "/ZIP/")
file(TOUCH "${_installdir}/.portable")
message(STATUS "Injected portable zip file.")
endif()
# Find all dll and exe files in the install directory
file(GLOB_RECURSE _sign_files
RELATIVE "${_installdir}"
"${_installdir}/*.dll"
"${_installdir}/*.exe"
)
# Sign relevant binaries if requested
if(_sign AND _sign_files)
# Find signtool in PATH or error out
find_program(_signtool signtool.exe QUIET)
if(NOT _signtool)
message(FATAL_ERROR "signtool.exe not found in PATH, correct or unset WITH_XC_SIGNINSTALL")
endif()
# Set a default timestamp URL if none was provided
if (NOT _timestamp_url)
set(_timestamp_url "http://timestamp.sectigo.com")
endif()
# Check that a certificate thumbprint was provided or error out
if (NOT _cert_thumbprint)
message(STATUS "Signing using best available certificate.")
set(_certopt /a)
else()
message(STATUS "Signing using certificate with thumbprint ${_cert_thumbprint}.")
set(_certopt /sha1 ${_cert_thumbprint})
endif()
message(STATUS "Signing binary files with signtool, this may take a while...")
# Use cmd /c to enable pop-up for pin entry if needed
execute_process(
COMMAND cmd /c ${_signtool} sign /fd SHA256 ${_certopt} /tr ${_timestamp_url} /td SHA256 ${_sign_files}
WORKING_DIRECTORY "${_installdir}"
RESULT_VARIABLE sign_result
OUTPUT_VARIABLE sign_output
ERROR_VARIABLE sign_error
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
ECHO_OUTPUT_VARIABLE
)
if (NOT sign_result EQUAL 0)
message(FATAL_ERROR "signtool failed: ${sign_error}")
endif()
endif()

View File

@@ -25,12 +25,10 @@ import lzma
import os import os
from pathlib import Path from pathlib import Path
import platform import platform
import random
import re import re
import signal import signal
import shutil import shutil
import stat import stat
import string
import subprocess import subprocess
import sys import sys
import tarfile import tarfile
@@ -43,7 +41,7 @@ from urllib.request import urlretrieve
########################################################################################### ###########################################################################################
# class Check(Command) # class Check(Command)
# class Merge(Command) # class Tag(Command)
# class Build(Command) # class Build(Command)
# class BuildSrc(Command) # class BuildSrc(Command)
# class AppSign(Command) # class AppSign(Command)
@@ -131,7 +129,8 @@ fmt = LogFormatter()
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setFormatter(fmt) console_handler.setFormatter(fmt)
logger = logging.getLogger(__file__) logger = logging.getLogger(__file__)
logger.setLevel(os.getenv('LOGLEVEL') if 'LOGLEVEL' in os.environ else logging.INFO) logger.setLevel(os.getenv('LOGLEVEL')
if type(logging.getLevelName(os.environ.get('LOGLEVEL'))) is int else logging.INFO)
logger.addHandler(console_handler) logger.addHandler(console_handler)
########################################################################################### ###########################################################################################
@@ -188,11 +187,12 @@ def _run(cmd, *args, cwd, path=None, env=None, input=None, capture_output=True,
env['FORCE_COLOR'] = '1' env['FORCE_COLOR'] = '1'
if docker_image: if docker_image:
docker_cmd = ['docker', 'run', '--rm', '--tty=true', f'--workdir={cwd}', f'--user={os.getuid()}:{os.getgid()}'] cwd2 = Path(cwd or '.').absolute()
docker_cmd = ['docker', 'run', '--rm', '--tty=true', f'--workdir={cwd2}', f'--user={os.getuid()}:{os.getgid()}']
docker_cmd.extend([f'--env={k}={v}' for k, v in env.items() if k in ['FORCE_COLOR', 'CC', 'CXX']]) docker_cmd.extend([f'--env={k}={v}' for k, v in env.items() if k in ['FORCE_COLOR', 'CC', 'CXX']])
if path: if path:
docker_cmd.append(f'--env=PATH={path}') docker_cmd.append(f'--env=PATH={path}')
docker_cmd.append(f'--volume={Path(cwd).absolute()}:{Path(cwd).absolute()}:rw') docker_cmd.append(f'--volume={cwd2}:{cwd2}:rw')
if docker_mounts: if docker_mounts:
docker_cmd.extend([f'--volume={Path(d).absolute()}:{Path(d).absolute()}:rw' for d in docker_mounts]) docker_cmd.extend([f'--volume={Path(d).absolute()}:{Path(d).absolute()}:rw' for d in docker_mounts])
if docker_privileged: if docker_privileged:
@@ -203,7 +203,7 @@ def _run(cmd, *args, cwd, path=None, env=None, input=None, capture_output=True,
cmd = docker_cmd + cmd cmd = docker_cmd + cmd
try: try:
logger.debug('Running command: %s', ' '.join(cmd)) logger.debug('Running command: %s', ' '.join(map(str, cmd)))
return subprocess.run( return subprocess.run(
cmd, *args, cmd, *args,
input=input, input=input,
@@ -342,6 +342,48 @@ def _capture_vs_env(arch='amd64'):
return env return env
def _macos_get_codesigning_identity(user_choice=None):
"""
Select an Apple codesigning certificate to be used for signing the macOS binaries.
If only one identity was found on the system, it is returned automatically. If multiple identities are
found, an interactive selection is shown. A user choice can be supplied to skip the selection.
If the user choice refers to an invalid identity, an error is raised.
"""
Check.check_xcode_setup()
result = _run(['security', 'find-identity', '-v', '-p', 'codesigning'], cwd=None, text=True)
identities = [i.strip() for i in result.stdout.strip().split('\n')[:-1]]
identities = [i.split(' ', 2)[1:] for i in identities]
if not identities:
raise Error('No codesigning identities found.')
if not user_choice and len(identities) == 1:
logger.info('Using codesigning identity %s.', identities[0][1])
return identities[0][0]
elif not user_choice:
return identities[_choice_prompt(
'The following code signing identities were found. Which one do you want to use?',
[' '.join(i) for i in identities])][0]
else:
for i in identities:
# Exact match of ID or substring match of description
if user_choice == i[0] or user_choice in i[1]:
return i[0]
raise Error('Invalid identity: %s', user_choice)
def _macos_validate_keychain_profile(keychain_profile):
"""
Validate that a given keychain profile with stored notarization credentials exists and is valid.
If no such profile is found, an error is raised with instructions on how to set one up.
"""
if _run(['security', 'find-generic-password', '-a',
f'com.apple.gke.notary.tool.saved-creds.{keychain_profile}'], cwd=None, check=False).returncode != 0:
raise Error(f'Keychain profile "%s" not found! Run\n'
f' {fmt.bold("xcrun notarytool store-credentials %s [...]" % keychain_profile)}\n'
f'to store your Apple notary service credentials in a keychain as "%s".',
keychain_profile, keychain_profile)
########################################################################################### ###########################################################################################
# CLI Commands # CLI Commands
########################################################################################### ###########################################################################################
@@ -408,6 +450,7 @@ class Check(Command):
cls.check_version_in_cmake(version, src_dir) cls.check_version_in_cmake(version, src_dir)
cls.check_changelog(version, src_dir) cls.check_changelog(version, src_dir)
cls.check_app_stream_info(version, src_dir) cls.check_app_stream_info(version, src_dir)
return git_ref
@staticmethod @staticmethod
def check_src_dir_exists(src_dir): def check_src_dir_exists(src_dir):
@@ -448,7 +491,7 @@ class Check(Command):
cmakelists = Path(cwd) / cmakelists cmakelists = Path(cwd) / cmakelists
if not cmakelists.is_file(): if not cmakelists.is_file():
raise Error('File not found: %s', cmakelists) raise Error('File not found: %s', cmakelists)
cmakelists_text = cmakelists.read_text() cmakelists_text = cmakelists.read_text("UTF-8")
major = re.search(r'^set\(KEEPASSXC_VERSION_MAJOR "(\d+)"\)$', cmakelists_text, re.MULTILINE).group(1) major = re.search(r'^set\(KEEPASSXC_VERSION_MAJOR "(\d+)"\)$', cmakelists_text, re.MULTILINE).group(1)
minor = re.search(r'^set\(KEEPASSXC_VERSION_MINOR "(\d+)"\)$', cmakelists_text, re.MULTILINE).group(1) minor = re.search(r'^set\(KEEPASSXC_VERSION_MINOR "(\d+)"\)$', cmakelists_text, re.MULTILINE).group(1)
patch = re.search(r'^set\(KEEPASSXC_VERSION_PATCH "(\d+)"\)$', cmakelists_text, re.MULTILINE).group(1) patch = re.search(r'^set\(KEEPASSXC_VERSION_PATCH "(\d+)"\)$', cmakelists_text, re.MULTILINE).group(1)
@@ -464,7 +507,7 @@ class Check(Command):
if not changelog.is_file(): if not changelog.is_file():
raise Error('File not found: %s', changelog) raise Error('File not found: %s', changelog)
major, minor, patch = _split_version(version) major, minor, patch = _split_version(version)
if not re.search(rf'^## {major}\.{minor}\.{patch} \(.+?\)\n+', changelog.read_text(), re.MULTILINE): if not re.search(rf'^## {major}\.{minor}\.{patch} \(.+?\)\n+', changelog.read_text("UTF-8"), re.MULTILINE):
raise Error(f'{changelog} has not been updated to the "%s" release.', version) raise Error(f'{changelog} has not been updated to the "%s" release.', version)
@staticmethod @staticmethod
@@ -499,8 +542,8 @@ class Check(Command):
raise Error('xcrun command not found! Please check that you have correctly installed Xcode.') raise Error('xcrun command not found! Please check that you have correctly installed Xcode.')
class Merge(Command): class Tag(Command):
"""Merge release branch into main branch and create release tags.""" """Update translations and tag release."""
@classmethod @classmethod
def setup_arg_parser(cls, parser: argparse.ArgumentParser): def setup_arg_parser(cls, parser: argparse.ArgumentParser):
@@ -522,7 +565,7 @@ class Merge(Command):
skip_translations, tx_resource, tx_min_perc): skip_translations, tx_resource, tx_min_perc):
major, minor, patch = _split_version(version) major, minor, patch = _split_version(version)
Check.perform_basic_checks(src_dir) Check.perform_basic_checks(src_dir)
Check.perform_version_checks(version, src_dir, release_branch) release_branch = Check.perform_version_checks(version, src_dir, release_branch)
Check.check_gnupg() Check.check_gnupg()
sign_key = GPGSign.get_secret_key(sign_key) sign_key = GPGSign.get_secret_key(sign_key)
@@ -533,7 +576,7 @@ class Merge(Command):
commit=True, yes=yes) commit=True, yes=yes)
changelog = re.search(rf'^## ({major}\.{minor}\.{patch} \(.*?\)\n\n+.+?)\n\n+## ', changelog = re.search(rf'^## ({major}\.{minor}\.{patch} \(.*?\)\n\n+.+?)\n\n+## ',
(Path(src_dir) / 'CHANGELOG.md').read_text(), re.MULTILINE | re.DOTALL) (Path(src_dir) / 'CHANGELOG.md').read_text("UTF-8"), re.MULTILINE | re.DOTALL)
if not changelog: if not changelog:
raise Error(f'No changelog entry found for version {version}.') raise Error(f'No changelog entry found for version {version}.')
changelog = 'Release ' + changelog.group(1) changelog = 'Release ' + changelog.group(1)
@@ -585,6 +628,13 @@ class Build(Command):
help='macOS deployment target version (default: %(default)s).') help='macOS deployment target version (default: %(default)s).')
parser.add_argument('-p', '--platform-target', default=platform.uname().machine, parser.add_argument('-p', '--platform-target', default=platform.uname().machine,
help='Build target platform (default: %(default)s).', choices=['x86_64', 'arm64']) help='Build target platform (default: %(default)s).', choices=['x86_64', 'arm64'])
parser.add_argument('--sign', help='Sign binaries prior to packaging.', action='store_true')
parser.add_argument('--sign-identity',
help='Apple Developer identity name used for signing binaries (default: ask).')
parser.add_argument('--notarize', help='Notarize signed file(s).', action='store_true')
parser.add_argument('--keychain-profile', default='notarization-creds',
help='Read Apple credentials for notarization from a keychain (default: %(default)s).')
parser.set_defaults(cmake_generator='Ninja')
elif sys.platform == 'linux': elif sys.platform == 'linux':
parser.add_argument('-d', '--docker-image', help='Run build in Docker image (overrides --use-system-deps).') parser.add_argument('-d', '--docker-image', help='Run build in Docker image (overrides --use-system-deps).')
parser.add_argument('-p', '--platform-target', help='Build target platform (default: %(default)s).', parser.add_argument('-p', '--platform-target', help='Build target platform (default: %(default)s).',
@@ -594,8 +644,10 @@ class Build(Command):
parser.add_argument('-p', '--platform-target', help='Build target platform (default: %(default)s).', parser.add_argument('-p', '--platform-target', help='Build target platform (default: %(default)s).',
choices=['amd64', 'arm64'], default='amd64') choices=['amd64', 'arm64'], default='amd64')
parser.add_argument('--sign', help='Sign binaries prior to packaging.', action='store_true') parser.add_argument('--sign', help='Sign binaries prior to packaging.', action='store_true')
parser.add_argument('--sign-cert', help='SHA1 fingerprint of the signing certificate (optional).') parser.add_argument('--sign-identity', help='SHA1 fingerprint of the signing certificate.')
parser.set_defaults(cmake_generator='Ninja', no_source_tarball=True) parser.add_argument('--sign-timestamp-url', help='Timestamp URL for signing binaries.',
default='http://timestamp.sectigo.com')
parser.set_defaults(cmake_generator='Ninja')
parser.add_argument('-c', '--cmake-opts', nargs=argparse.REMAINDER, parser.add_argument('-c', '--cmake-opts', nargs=argparse.REMAINDER,
help='Additional CMake options (no other arguments can be specified after this).') help='Additional CMake options (no other arguments can be specified after this).')
@@ -674,15 +726,15 @@ class Build(Command):
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def build_windows(self, version, src_dir, output_dir, *, parallelism, cmake_opts, platform_target, def build_windows(self, version, src_dir, output_dir, *, parallelism, cmake_opts, platform_target,
sign, sign_cert, with_tests, **_): sign, sign_identity, sign_timestamp_url, with_tests, **_):
# Check for required tools # Check for required tools
if not _cmd_exists('candle.exe') or not _cmd_exists('light.exe') or not _cmd_exists('heat.exe'): if not _cmd_exists('candle.exe') or not _cmd_exists('light.exe') or not _cmd_exists('heat.exe'):
raise Error('WiX Toolset not found on the PATH (candle.exe, light.exe, heat.exe).') raise Error('WiX Toolset not found on the PATH (candle.exe, light.exe, heat.exe).')
# Setup build signing if requested # Setup build signing if requested
if sign: if sign:
cmake_opts.append('-DWITH_XC_SIGNINSTALL=ON') cmake_opts.append(f'-DWITH_XC_CODESIGN_IDENTITY={sign_identity}')
cmake_opts.append(f'-DWITH_XC_SIGNINSTALL_CERT={sign_cert}') cmake_opts.append(f'-WITH_XC_CODESIGN_TIMESTAMP_URL={sign_timestamp_url}')
# Use vcpkg for dependency deployment # Use vcpkg for dependency deployment
cmake_opts.append('-DX_VCPKG_APPLOCAL_DEPS_INSTALL=ON') cmake_opts.append('-DX_VCPKG_APPLOCAL_DEPS_INSTALL=ON')
@@ -716,13 +768,20 @@ class Build(Command):
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def build_macos(self, version, src_dir, output_dir, *, use_system_deps, parallelism, cmake_opts, def build_macos(self, version, src_dir, output_dir, *, use_system_deps, parallelism, cmake_opts,
macos_target, platform_target, with_tests, **_): macos_target, platform_target, with_tests, sign, sign_identity, notarize, keychain_profile, **_):
if not use_system_deps: if not use_system_deps:
cmake_opts.append(f'-DVCPKG_TARGET_TRIPLET={platform_target.replace("86_", "")}-osx-dynamic-release') cmake_opts.append(f'-DVCPKG_TARGET_TRIPLET={platform_target.replace("86_", "")}-osx-dynamic-release')
cmake_opts.append(f'-DCMAKE_OSX_DEPLOYMENT_TARGET={macos_target}') cmake_opts.append(f'-DCMAKE_OSX_DEPLOYMENT_TARGET={macos_target}')
cmake_opts.append(f'-DCMAKE_OSX_ARCHITECTURES={platform_target}') cmake_opts.append(f'-DCMAKE_OSX_ARCHITECTURES={platform_target}')
with tempfile.TemporaryDirectory() as build_dir: with tempfile.TemporaryDirectory() as build_dir:
if sign:
sign_identity = _macos_get_codesigning_identity(sign_identity)
cmake_opts.append(f'-DWITH_XC_CODESIGN_IDENTITY={sign_identity}')
if notarize:
_macos_validate_keychain_profile(keychain_profile)
cmake_opts.append(f'-DWITH_XC_NOTARY_KEYCHAIN_PROFILE={keychain_profile}')
logger.info('Configuring build...') logger.info('Configuring build...')
_run(['cmake', *cmake_opts, str(src_dir)], cwd=build_dir, capture_output=False) _run(['cmake', *cmake_opts, str(src_dir)], cwd=build_dir, capture_output=False)
@@ -736,8 +795,12 @@ class Build(Command):
_run(['cpack', '-G', 'DragNDrop'], cwd=build_dir, capture_output=False) _run(['cpack', '-G', 'DragNDrop'], cwd=build_dir, capture_output=False)
output_file = Path(build_dir) / f'KeePassXC-{version}.dmg' output_file = Path(build_dir) / f'KeePassXC-{version}.dmg'
output_file.rename(output_dir / f'KeePassXC-{version}-{platform_target}-unsigned.dmg') unsigned_suffix = '-unsigned' if not sign else ''
output_file.rename(output_dir / f'KeePassXC-{version}-{platform_target}{unsigned_suffix}.dmg')
if sign:
logger.info('All done!')
else:
logger.info('All done! Please don\'t forget to sign the binaries before distribution.') logger.info('All done! Please don\'t forget to sign the binaries before distribution.')
@staticmethod @staticmethod
@@ -765,6 +828,8 @@ class Build(Command):
if appimage: if appimage:
cmake_opts.append('-DKEEPASSXC_DIST_TYPE=AppImage') cmake_opts.append('-DKEEPASSXC_DIST_TYPE=AppImage')
# Force install prefix to ensure proper AppDir structure for linuxdeploy
install_prefix = '/usr'
with tempfile.TemporaryDirectory() as build_dir: with tempfile.TemporaryDirectory() as build_dir:
logger.info('Configuring build...') logger.info('Configuring build...')
@@ -782,7 +847,7 @@ class Build(Command):
_run(['cmake', '--install', '.', '--strip', _run(['cmake', '--install', '.', '--strip',
'--prefix', (app_dir.absolute() / install_prefix.lstrip('/')).as_posix()], '--prefix', (app_dir.absolute() / install_prefix.lstrip('/')).as_posix()],
cwd=build_dir, capture_output=False, **docker_args) cwd=build_dir, capture_output=False, **docker_args)
shutil.copytree(app_dir, output_dir / app_dir.name, symlinks=True) shutil.copytree(app_dir, output_dir / app_dir.name, symlinks=True, dirs_exist_ok=True)
if appimage: if appimage:
self._build_linux_appimage( self._build_linux_appimage(
@@ -823,7 +888,7 @@ class Build(Command):
_run(['linuxdeploy', '--plugin=qt', f'--appdir={app_dir}', f'--custom-apprun={app_run}', _run(['linuxdeploy', '--plugin=qt', f'--appdir={app_dir}', f'--custom-apprun={app_run}',
f'--desktop-file={desktop_file}', f'--icon-file={icon_file}', f'--desktop-file={desktop_file}', f'--icon-file={icon_file}',
*[f'--executable={ex}' for ex in executables]], *[f'--executable={ex}' for ex in executables]],
cwd=build_dir, capture_output=False, path=env_path, **docker_args) cwd=build_dir, capture_output=False, path=env_path, **docker_args, docker_privileged=True)
logger.debug('Running appimagetool...') logger.debug('Running appimagetool...')
appimage_name = f'KeePassXC-{version}-{platform_target}.AppImage' appimage_name = f'KeePassXC-{version}-{platform_target}.AppImage'
@@ -888,162 +953,37 @@ class BuildSrc(Command):
tmp_comp.rename(output_file) tmp_comp.rename(output_file)
class AppSign(Command): class Notarize(Command):
"""Sign binaries with code signing certificates on Windows and macOS.""" """Notarize a signed macOS DMG app bundle."""
@classmethod @classmethod
def setup_arg_parser(cls, parser: argparse.ArgumentParser): def setup_arg_parser(cls, parser: argparse.ArgumentParser):
parser.add_argument('file', help='Input file(s) to sign.', nargs='+') parser.add_argument('file', help='Input DMG file(s) to notarize.', nargs='+')
parser.add_argument('-i', '--identity', help='Key or identity used for the signature (default: ask).') parser.add_argument('-p', '--keychain-profile', default='notarization-creds',
parser.add_argument('-s', '--src-dir', help='Source directory (default: %(default)s).', default='.')
if sys.platform == 'darwin':
parser.add_argument('-n', '--notarize', help='Notarize signed file(s).', action='store_true')
parser.add_argument('-c', '--keychain-profile', default='notarization-creds',
help='Read Apple credentials for notarization from a keychain (default: %(default)s).') help='Read Apple credentials for notarization from a keychain (default: %(default)s).')
def run(self, file, identity, src_dir, **kwargs): def run(self, file, keychain_profile, **_):
if sys.platform != 'darwin':
raise Error('Unsupported platform.')
logger.warning('This tool is meant primarily for testing purposes. '
'For production use, add the --notarize flag to the build command.')
_macos_validate_keychain_profile(keychain_profile)
for i, f in enumerate(file): for i, f in enumerate(file):
f = Path(f) f = Path(f)
if not f.exists(): if not f.exists():
raise Error('Input file does not exist: %s', f) raise Error('Input file does not exist: %s', f)
if f.suffix != '.dmg':
raise Error('Input file is not a DMG image: %s', f)
file[i] = f file[i] = f
self.notarize_macos(f, keychain_profile)
if sys.platform == 'win32':
for f in file:
self.sign_windows(f, identity, Path(src_dir))
elif sys.platform == 'darwin':
Check.check_xcode_setup()
if kwargs['notarize']:
self._macos_validate_keychain_profile(kwargs['keychain_profile'])
identity = self._macos_get_codesigning_identity(identity)
for f in file:
out_file = self.sign_macos(f, identity, Path(src_dir))
if out_file and kwargs['notarize'] and out_file.suffix == '.dmg':
self.notarize_macos(out_file, kwargs['keychain_profile'])
else:
raise Error('Unsupported platform.')
logger.info('All done.') logger.info('All done.')
# noinspection PyMethodMayBeStatic
def sign_windows(self, file, identity, src_dir):
# Check for signtool
if not _cmd_exists('signtool.exe'):
raise Error('signtool was not found on the PATH.')
signtool_args = ['signtool', 'sign', '/fd', 'sha256', '/tr', 'http://timestamp.digicert.com', '/td', 'sha256']
if not identity:
logger.info('Using automatic selection of signing certificate.')
signtool_args += ['/a']
else:
logger.info('Using specified signing certificate: %s', identity)
signtool_args += ['/sha1', identity]
signtool_args += ['/d', file.name, str(file.resolve())]
_run(signtool_args, cwd=src_dir, capture_output=False)
# noinspection PyMethodMayBeStatic
def _macos_validate_keychain_profile(self, keychain_profile):
if _run(['security', 'find-generic-password', '-a',
f'com.apple.gke.notary.tool.saved-creds.{keychain_profile}'], cwd=None, check=False).returncode != 0:
raise Error(f'Keychain profile "%s" not found! Run\n'
f' {fmt.bold("xcrun notarytool store-credentials %s [...]" % keychain_profile)}\n'
f'to store your Apple notary service credentials in a keychain as "%s".',
keychain_profile, keychain_profile)
# noinspection PyMethodMayBeStatic
def _macos_get_codesigning_identity(self, user_choice=None):
result = _run(['security', 'find-identity', '-v', '-p', 'codesigning'], cwd=None, text=True)
identities = [i.strip() for i in result.stdout.strip().split('\n')[:-1]]
identities = [i.split(' ', 2)[1:] for i in identities]
if not identities:
raise Error('No codesigning identities found.')
if not user_choice and len(identities) == 1:
logger.info('Using codesigning identity %s.', identities[0][1])
return identities[0][0]
elif not user_choice:
return identities[_choice_prompt(
'The following code signing identities were found. Which one do you want to use?',
[' '.join(i) for i in identities])][0]
else:
for i in identities:
# Exact match of ID or substring match of description
if user_choice == i[0] or user_choice in i[1]:
return i[0]
raise Error('Invalid identity: %s', user_choice)
# noinspection PyMethodMayBeStatic
def sign_macos(self, file, identity, src_dir):
logger.info('Signing "%s"', file)
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp).absolute()
app_dir = tmp / 'app'
out_file = file.parent / file.name.replace('-unsigned', '')
if file.is_file() and file.suffix == '.dmg':
logger.debug('Unpacking disk image...')
mnt = tmp / 'mnt'
mnt.mkdir()
try:
_run(['hdiutil', 'attach', '-noautoopen', '-mountpoint', mnt.as_posix(), file.as_posix()], cwd=None)
shutil.copytree(mnt, app_dir, symlinks=True)
finally:
_run(['hdiutil', 'detach', mnt.as_posix()], cwd=None)
elif file.is_dir() and file.suffix == '.app':
logger.debug('Copying .app directory...')
shutil.copytree(file, app_dir, symlinks=True)
else:
logger.warning('Skipping non-app file "%s"', file)
return None
app_dir_app = list(app_dir.glob('*.app'))[0]
logger.debug('Signing libraries and frameworks...')
_run(['xcrun', 'codesign', f'--sign={identity}', '--force', '--options=runtime', '--deep',
app_dir_app.as_posix()], cwd=None)
# (Re-)Sign main executable with --entitlements
logger.debug('Signing main executable...')
_run(['xcrun', 'codesign', f'--sign={identity}', '--force', '--options=runtime',
'--entitlements', (src_dir / 'share/macosx/keepassxc.entitlements').as_posix(),
(app_dir_app / 'Contents/MacOS/KeePassXC').as_posix()], cwd=None)
tmp_out = out_file.with_suffix(f'.{"".join(random.choices(string.ascii_letters, k=8))}{file.suffix}')
try:
if file.suffix == '.dmg':
logger.debug('Repackaging disk image...')
dmg_size = sum(f.stat().st_size for f in app_dir.rglob('*'))
_run(['hdiutil', 'create', '-volname', 'KeePassXC', '-srcfolder', app_dir.as_posix(),
'-fs', 'HFS+', '-fsargs', '-c c=64,a=16,e=16', '-format', 'UDBZ',
'-size', f'{dmg_size}k', tmp_out.as_posix()],
cwd=None)
elif file.suffix == '.app':
shutil.copytree(app_dir, tmp_out, symlinks=True)
except Exception:
if tmp_out.is_file():
tmp_out.unlink()
elif tmp_out.is_dir():
shutil.rmtree(tmp_out, ignore_errors=True)
raise
finally:
# Replace original file if all went well
if tmp_out.exists():
if tmp_out.is_dir():
shutil.rmtree(file)
else:
file.unlink()
tmp_out.rename(out_file)
logger.info('File signed successfully and written to: "%s".', out_file)
return out_file
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def notarize_macos(self, file, keychain_profile): def notarize_macos(self, file, keychain_profile):
logger.info('Submitting "%s" for notarization...', file) logger.info('Submitting "%s" for notarization...', file)
_run(['xcrun', 'notarytool', 'submit', f'--keychain-profile={keychain_profile}', '--wait', _run(['xcrun', 'notarytool', 'submit', f'--keychain-profile={keychain_profile}', '--wait',
file.as_posix()], cwd=None, capture_output=False) file.as_posix()], cwd=None, capture_output=False)
@@ -1259,9 +1199,9 @@ def main():
Check.setup_arg_parser(check_parser) Check.setup_arg_parser(check_parser)
check_parser.set_defaults(_cmd=Check) check_parser.set_defaults(_cmd=Check)
merge_parser = subparsers.add_parser('merge', help=Merge.__doc__) merge_parser = subparsers.add_parser('tag', help=Tag.__doc__)
Merge.setup_arg_parser(merge_parser) Tag.setup_arg_parser(merge_parser)
merge_parser.set_defaults(_cmd=Merge) merge_parser.set_defaults(_cmd=Tag)
build_parser = subparsers.add_parser('build', help=Build.__doc__) build_parser = subparsers.add_parser('build', help=Build.__doc__)
Build.setup_arg_parser(build_parser) Build.setup_arg_parser(build_parser)
@@ -1271,9 +1211,10 @@ def main():
BuildSrc.setup_arg_parser(build_src_parser) BuildSrc.setup_arg_parser(build_src_parser)
build_src_parser.set_defaults(_cmd=BuildSrc) build_src_parser.set_defaults(_cmd=BuildSrc)
appsign_parser = subparsers.add_parser('appsign', help=AppSign.__doc__) if sys.platform == 'darwin':
AppSign.setup_arg_parser(appsign_parser) notarize_parser = subparsers.add_parser('notarize', help=Notarize.__doc__)
appsign_parser.set_defaults(_cmd=AppSign) Notarize.setup_arg_parser(notarize_parser)
notarize_parser.set_defaults(_cmd=Notarize)
gpgsign_parser = subparsers.add_parser('gpgsign', help=GPGSign.__doc__) gpgsign_parser = subparsers.add_parser('gpgsign', help=GPGSign.__doc__)
GPGSign.setup_arg_parser(gpgsign_parser) GPGSign.setup_arg_parser(gpgsign_parser)

View File

@@ -1,12 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export PATH="$(dirname $0)/usr/bin:${PATH}"
if [ "$1" == "cli" ] || [ "$(basename "$ARGV0")" == "keepassxc-cli" ] || [ "$(basename "$ARGV0")" == "keepassxc-cli.AppImage" ]; then if [ "$1" == "cli" ] || [ "$(basename "$ARGV0")" == "keepassxc-cli" ] || [ "$(basename "$ARGV0")" == "keepassxc-cli.AppImage" ]; then
[ "$1" == "cli" ] && shift [ "$1" == "cli" ] && shift
exec keepassxc-cli "$@" exec keepassxc-cli "$@"
elif [ "$1" == "proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy.AppImage" ] \ elif [ "$1" == "proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy.AppImage" ]; then
|| [ -v CHROME_WRAPPER ] || [ -v MOZ_LAUNCHED_CHILD ]; then
[ "$1" == "proxy" ] && shift [ "$1" == "proxy" ] && shift
exec keepassxc-proxy "$@" exec keepassxc-proxy "$@"
elif [ -v CHROME_WRAPPER ] || [ -v MOZ_LAUNCHED_CHILD ] || [ "$2" == "keepassxc-browser@keepassxc.org" ]; then
exec keepassxc-proxy "$@"
else else
exec keepassxc "$@" exec keepassxc "$@"
fi fi

View File

@@ -52,6 +52,75 @@
</ul> </ul>
</description> </description>
</release> </release>
<release version="2.7.11" date="2025-11-23" type="stable">
<description>
<ul>
<li>Add image, HTML, Markdown preview, and text editing support to inline attachment viewer [#12085, #12244, #12654]</li>
<li>Add database merge confirmation dialog [#10173]</li>
<li>Add option to auto-generate a password for new entries [#12593]</li>
<li>Add support for group sync in KeeShare [#11593]</li>
<li>Add {UUID} placeholder for use in references [#12511]</li>
<li>Add “Wait for Enter” search option [#12263]</li>
<li>Add keyboard shortcut to “Jump to Group” from search results [#12225]</li>
<li>Add predefined search for TOTP entries [#12199]</li>
<li>Add confirmation when closing database via ESC key [#11963]</li>
<li>Add support for escaping placeholder expressions [#11904]</li>
<li>Reduce tab indentation width in notes fields [#11919]</li>
<li>Cap default Argon2 parallelism when creating a new database [#11853]</li>
<li>Database lock after inactivity now enabled by default and set to 900 seconds [#12689, #12609]</li>
<li>Copying TOTP now opens setup dialog if none is configured for entry [#12584]</li>
<li>Make double click action configurable [#12322]</li>
<li>Remove unused “Last Accessed” from GUI [#12602]</li>
<li>Auto-Type: Add more granular confirmation settings [#12370]</li>
<li>Auto-Type: Add URL typing preset and add copy options to menu [#12341]</li>
<li>Browser: Do not allow sites automatically if entry added from browser extension [#12413]</li>
<li>Browser: Add options to restrict exposed groups [#9852, #12119]</li>
<li>Bitwarden Import: Add support for timestamps and password history [#12588]</li>
<li>macOS: Add Liquid Glass icon [#12642]</li>
<li>macOS: Remove theme-based menubar icon toggle [#12685]</li>
<li>macOS: Add Window and Help menus [#12357]</li>
<li>Windows: Add option to add KeePassXC to PATH during installation [#12171]</li>
<li>Fix window geometry not being restored properly when KeePassXC starts in tray [#12683]</li>
<li>Fix potential database truncation when using direct write save method with YubiKeys [#11841]</li>
<li>Fix issue with database backup saving [#11874]</li>
<li>Fix UI lockups during startup with multiple tabs [#12053]</li>
<li>Fix keyboard shortcuts when menubar is hidden [#12431]</li>
<li>Fix clipboard being cleared on exit even if no password was copied [#12603]</li>
<li>Fix single-instance detection when username contains invalid filename characters [#12559]</li>
<li>Fix “Search Wait for Enter” setting not being save [#12614]</li>
<li>Fix hotkey accelerators not being escaped properly on database tabs [#12630]</li>
<li>Fix confusing error if user cancels out of key file edit dialog [#12639]</li>
<li>Fix issues with saved searches and “Press Enter to Search” option [#12314]</li>
<li>Fix URL wildcard matching [#12257]</li>
<li>Fix TOTP visibility on unlock and settings change [#12220]</li>
<li>Fix KeeShare entries with reference attributes not updating [#11809]</li>
<li>Fix sort order not being maintained when toggling filters in database reports [#11849]</li>
<li>Fix several UI font and layout issues [#11967, #12102]</li>
<li>Prevent mouse wheel scroll on edit username field [#12398]</li>
<li>Improve base translation consistency [#12432]</li>
<li>Improve inactivity timer [#12246]</li>
<li>Documentation improvements [#12373, #12506]</li>
<li>Browser: Fix ordering of clientDataJSON in Passkey response object [#12120]</li>
<li>Browser: Fix URL matching for additional URLs [#12196]</li>
<li>Browser: Fix group settings inheritance [#12368]</li>
<li>Browser: Allow read-only native messaging config files [#12236]</li>
<li>Browser: Optimise entry iteration in browser access control dialog [#11817]</li>
<li>Browser: Fix “Do not ask permission for HTTP Basic Auth” option [#11871]</li>
<li>Browser: Fix native messaging path for Tor Browser launcher on Linux [#12005]</li>
<li>Auto-Type: Fix empty window behaviour [#12622]</li>
<li>Auto-Type: Take delays into account when typing TOTP [#12691]</li>
<li>SSH Agent: Fix out-of-memory crash with malformed SSH keys [#12606]</li>
<li>CSV Import: Fix modified and creation time import [#12379]</li>
<li>CSV Import: Fix duplication of root groups on import [#12240]</li>
<li>Proton Pass Import: Fix email addresses not being imported when no username set [#11888]</li>
<li>macOS: Fix secure input getting stuck [#11928]</li>
<li>Windows: Prevent launch as SYSTEM user from MSI installer [#12705]</li>
<li>Windows: Remove broken check for MSVC Redistributable from MSI installer [#11950]</li>
<li>Linux: Fix startup delay due to StartupNotify setting in desktop file [#12306]</li>
<li>Linux: Fix memory initialisation when --pw-stdin is used with a pipe [#12050]</li>
</ul>
</description>
</release>
<release version="2.7.10" date="2025-03-02" type="stable"> <release version="2.7.10" date="2025-03-02" type="stable">
<description> <description>
<ul> <ul>

View File

@@ -121,6 +121,8 @@
<SetProperty Id="AUTOSTARTPROGRAM" After="AppSearch" Value="" Sequence="first">AUTOSTARTPROGRAM="0" OR (WIX_UPGRADE_DETECTED AND NOT AUTOSTARTPROGRAM_REGISTRY)</SetProperty> <SetProperty Id="AUTOSTARTPROGRAM" After="AppSearch" Value="" Sequence="first">AUTOSTARTPROGRAM="0" OR (WIX_UPGRADE_DETECTED AND NOT AUTOSTARTPROGRAM_REGISTRY)</SetProperty>
<SetProperty Id="ADDTOPATH" After="AppSearch" Value="" Sequence="first">ADDTOPATH="0" OR (WIX_UPGRADE_DETECTED AND NOT ADDTOPATH_REGISTRY)</SetProperty> <SetProperty Id="ADDTOPATH" After="AppSearch" Value="" Sequence="first">ADDTOPATH="0" OR (WIX_UPGRADE_DETECTED AND NOT ADDTOPATH_REGISTRY)</SetProperty>
<SetProperty Id="LicenseAccepted" After="AppSearch" Value="1">WIX_UPGRADE_DETECTED</SetProperty> <SetProperty Id="LicenseAccepted" After="AppSearch" Value="1">WIX_UPGRADE_DETECTED</SetProperty>
<!-- Prevent launch on installer exit if run as SYSTEM user -->
<SetProperty Id="LAUNCHAPPONEXIT" After="AppSearch" Value="">UserSID = "S-1-5-18"</SetProperty>
<FeatureRef Id="ProductFeature"> <FeatureRef Id="ProductFeature">
<ComponentRef Id="ApplicationShortcuts" /> <ComponentRef Id="ApplicationShortcuts" />

View File

@@ -271,7 +271,7 @@ if(WIN32)
list(APPEND gui_SOURCES list(APPEND gui_SOURCES
gui/osutils/winutils/ScreenLockListenerWin.cpp gui/osutils/winutils/ScreenLockListenerWin.cpp
gui/osutils/winutils/WinUtils.cpp) gui/osutils/winutils/WinUtils.cpp)
if (MSVC) if (WINSDK)
list(APPEND gui_SOURCES quickunlock/WindowsHello.cpp) list(APPEND gui_SOURCES quickunlock/WindowsHello.cpp)
endif() endif()
endif() endif()
@@ -415,7 +415,7 @@ if(UNIX AND NOT APPLE)
endif() endif()
if(WIN32) if(WIN32)
target_link_libraries(keepassxc_gui Wtsapi32.lib Ws2_32.lib) target_link_libraries(keepassxc_gui Wtsapi32.lib Ws2_32.lib)
if (MSVC) if (WINSDK)
target_link_libraries(keepassxc_gui WindowsApp.lib) target_link_libraries(keepassxc_gui WindowsApp.lib)
endif() endif()
endif() endif()
@@ -426,7 +426,7 @@ target_link_libraries(${PROGNAME} keepassxc_gui)
set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON) set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
if(WIN32) if(WIN32)
set_target_properties(${PROGNAME} PROPERTIES WIN32 ON) set_target_properties(${PROGNAME} PROPERTIES WIN32_EXECUTABLE ON)
include(GenerateProductVersion) include(GenerateProductVersion)
generate_product_version( generate_product_version(
WIN32_ResourceFiles WIN32_ResourceFiles
@@ -442,6 +442,7 @@ if(WIN32)
elseif(APPLE AND WITH_APP_BUNDLE) elseif(APPLE AND WITH_APP_BUNDLE)
set(MACOSX_BUNDLE_IDENTIFIER org.keepassxc.keepassxc) set(MACOSX_BUNDLE_IDENTIFIER org.keepassxc.keepassxc)
set(MACOSX_BUNDLE_ICON_NAME keepassxc) set(MACOSX_BUNDLE_ICON_NAME keepassxc)
set(MACOSX_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements")
configure_file("${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake" ${CMAKE_CURRENT_BINARY_DIR}/Info.plist) configure_file("${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake" ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
install(FILES "${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile" DESTINATION ${BUNDLE_INSTALL_DIR}) install(FILES "${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile" DESTINATION ${BUNDLE_INSTALL_DIR})
set(MACOSX_BUNDLE_RESOURCE_FILES set(MACOSX_BUNDLE_RESOURCE_FILES
@@ -451,7 +452,7 @@ elseif(APPLE AND WITH_APP_BUNDLE)
set_target_properties(${PROGNAME} PROPERTIES set_target_properties(${PROGNAME} PROPERTIES
MACOSX_BUNDLE ON MACOSX_BUNDLE ON
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist" 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 "${MACOSX_BUNDLE_APPLE_ENTITLEMENTS}"
RESOURCE "${MACOSX_BUNDLE_RESOURCE_FILES}" RESOURCE "${MACOSX_BUNDLE_RESOURCE_FILES}"
) )
target_sources(${PROGNAME} PUBLIC ${MACOSX_BUNDLE_RESOURCE_FILES}) target_sources(${PROGNAME} PUBLIC ${MACOSX_BUNDLE_RESOURCE_FILES})
@@ -461,6 +462,18 @@ elseif(APPLE AND WITH_APP_BUNDLE)
DESTINATION "${DATA_INSTALL_DIR}") DESTINATION "${DATA_INSTALL_DIR}")
endif() endif()
# Sign binaries
if(WITH_XC_CODESIGN_IDENTITY)
configure_file("${CMAKE_SOURCE_DIR}/cmake/MacOSCodesign.cmake.in" "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake" @ONLY)
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake")
if(WITH_XC_NOTARY_KEYCHAIN_PROFILE)
configure_file("${CMAKE_SOURCE_DIR}/cmake/MacOSCodesign.cmake.in" "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake" @ONLY)
set(CPACK_POST_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake")
else()
message(INFO "Do not forget to notarize DMG package before distribution!")
endif()
endif()
set(CPACK_GENERATOR "DragNDrop") set(CPACK_GENERATOR "DragNDrop")
set(CPACK_DMG_FORMAT "UDBZ") set(CPACK_DMG_FORMAT "UDBZ")
set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/share/macosx/DS_Store.in") set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/share/macosx/DS_Store.in")
@@ -492,8 +505,9 @@ if(WIN32)
"${CMAKE_CURRENT_BINARY_DIR}/INSTALLER_LICENSE.txt") "${CMAKE_CURRENT_BINARY_DIR}/INSTALLER_LICENSE.txt")
# Prepare post-install script and set to run prior to building cpack installers # Prepare post-install script and set to run prior to building cpack installers
configure_file("${CMAKE_SOURCE_DIR}/cmake/WindowsPostInstall.cmake.in" "${CMAKE_BINARY_DIR}/WindowsPostInstall.cmake" @ONLY) configure_file("${CMAKE_SOURCE_DIR}/cmake/WindowsCodesign.cmake.in" "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake" @ONLY)
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsPostInstall.cmake") set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake")
set(CPACK_POST_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake")
string(REGEX REPLACE "-.*$" "" KEEPASSXC_VERSION_CLEAN ${KEEPASSXC_VERSION}) string(REGEX REPLACE "-.*$" "" KEEPASSXC_VERSION_CLEAN ${KEEPASSXC_VERSION})

View File

@@ -32,6 +32,7 @@
#include "core/Global.h" #include "core/Global.h"
#include "core/Resources.h" #include "core/Resources.h"
#include "core/Tools.h" #include "core/Tools.h"
#include "core/Totp.h"
#include "gui/MainWindow.h" #include "gui/MainWindow.h"
#include "gui/MessageBox.h" #include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h" #include "gui/osutils/OSUtils.h"
@@ -311,9 +312,6 @@ void AutoType::executeAutoTypeActions(const Entry* entry,
// Restore executor mode // Restore executor mode
m_executor->mode = mode; m_executor->mode = mode;
int delay = qMax(100, config()->get(Config::AutoTypeStartDelay).toInt());
Tools::wait(delay);
// Grab the current active window after everything settles // Grab the current active window after everything settles
if (window == 0) { if (window == 0) {
window = m_plugin->activeWindow(); window = m_plugin->activeWindow();
@@ -345,7 +343,8 @@ void AutoType::executeAutoTypeActions(const Entry* entry,
break; break;
} }
Tools::wait(delay); // Retry wait delay
Tools::wait(100);
} }
// Last action failed to complete, cancel the rest of the sequence // Last action failed to complete, cancel the rest of the sequence
@@ -546,10 +545,14 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
const int maxTypeDelay = 500; const int maxTypeDelay = 500;
const int maxWaitDelay = 10000; const int maxWaitDelay = 10000;
const int maxRepetition = 100; const int maxRepetition = 100;
int currentTypingDelay = qBound(0, config()->get(Config::AutoTypeDelay).toInt(), maxTypeDelay);
int cumulativeDelay = qBound(0, config()->get(Config::AutoTypeStartDelay).toInt(), maxWaitDelay);
// Initial actions include start delay and initial inter-key delay
QList<QSharedPointer<AutoTypeAction>> actions; QList<QSharedPointer<AutoTypeAction>> actions;
actions << QSharedPointer<AutoTypeBegin>::create(); actions << QSharedPointer<AutoTypeBegin>::create();
actions << QSharedPointer<AutoTypeDelay>::create(qMax(0, config()->get(Config::AutoTypeDelay).toInt()), true); actions << QSharedPointer<AutoTypeDelay>::create(currentTypingDelay, true);
actions << QSharedPointer<AutoTypeDelay>::create(cumulativeDelay);
// Replace escaped braces with a template for easier regex // Replace escaped braces with a template for easier regex
QString sequence = entrySequence; QString sequence = entrySequence;
@@ -565,7 +568,7 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
// Group 1 = modifier key (opt) // Group 1 = modifier key (opt)
// Group 2 = full placeholder // Group 2 = full placeholder
// Group 3 = inner placeholder (allows nested placeholders) // Group 3 = inner placeholder (allows nested placeholders)
// Group 4 = repeat (opt) // Group 4 = repeat / delay time (opt)
// Group 5 = character // Group 5 = character
QRegularExpression regex("([+%^#]*)(?:({((?>[^{}]+?|(?2))+?)(?:\\s+(\\d+))?})|(.))"); QRegularExpression regex("([+%^#]*)(?:({((?>[^{}]+?|(?2))+?)(?:\\s+(\\d+))?})|(.))");
auto results = regex.globalMatch(sequence); auto results = regex.globalMatch(sequence);
@@ -627,19 +630,23 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
} }
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, delay, maxTypeDelay), true); actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, delay, maxTypeDelay), true);
} else if (placeholder == "delay") { } else if (placeholder == "delay") {
// Mid typing delay (wait) // Mid typing delay (wait), repeat represents the desired delay in milliseconds
if (repeat > maxWaitDelay) { if (repeat > maxWaitDelay) {
error = tr("Very long delay detected, max is %1: %2").arg(maxWaitDelay).arg(fullPlaceholder); error = tr("Very long delay detected, max is %1: %2").arg(maxWaitDelay).arg(fullPlaceholder);
return {}; return {};
} }
cumulativeDelay += repeat;
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, repeat, maxWaitDelay)); actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, repeat, maxWaitDelay));
} else if (placeholder == "clearfield") { } else if (placeholder == "clearfield") {
// Platform-specific field clearing // Platform-specific field clearing
actions << QSharedPointer<AutoTypeClearField>::create(); actions << QSharedPointer<AutoTypeClearField>::create();
} else if (placeholder == "totp") { } else if (placeholder == "totp") {
if (entry->hasValidTotp()) { if (entry->hasValidTotp()) {
// Entry totp (requires special handling) // Calculate TOTP at the time of typing including delays
QString totp = entry->totp(); bool isValid = false;
auto time =
Clock::currentSecondsSinceEpoch() + (cumulativeDelay + currentTypingDelay * actions.count()) / 1000;
auto totp = Totp::generateTotp(entry->totpSettings(), &isValid, time);
for (const auto& ch : totp) { for (const auto& ch : totp) {
actions << QSharedPointer<AutoTypeKey>::create(ch); actions << QSharedPointer<AutoTypeKey>::create(ch);
} }

View File

@@ -175,6 +175,7 @@ namespace Bootstrap
if (!CreateWellKnownSid(WinCreatorOwnerRightsSid, nullptr, pOwnerRightsSid, &pOwnerRightsSidSize)) { if (!CreateWellKnownSid(WinCreatorOwnerRightsSid, nullptr, pOwnerRightsSid, &pOwnerRightsSidSize)) {
auto error = GetLastError(); auto error = GetLastError();
Q_UNUSED(error)
goto Cleanup; goto Cleanup;
} }

View File

@@ -140,7 +140,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Security_ClearSearchTimeout, {QS("Security/ClearSearchTimeout"), Roaming, 5}}, {Config::Security_ClearSearchTimeout, {QS("Security/ClearSearchTimeout"), Roaming, 5}},
{Config::Security_HideNotes, {QS("Security/Security_HideNotes"), Roaming, false}}, {Config::Security_HideNotes, {QS("Security/Security_HideNotes"), Roaming, false}},
{Config::Security_LockDatabaseIdle, {QS("Security/LockDatabaseIdle"), Roaming, true}}, {Config::Security_LockDatabaseIdle, {QS("Security/LockDatabaseIdle"), Roaming, true}},
{Config::Security_LockDatabaseIdleSeconds, {QS("Security/LockDatabaseIdleSeconds"), Roaming, 240}}, {Config::Security_LockDatabaseIdleSeconds, {QS("Security/LockDatabaseIdleSeconds"), Roaming, 900}},
{Config::Security_LockDatabaseMinimize, {QS("Security/LockDatabaseMinimize"), Roaming, false}}, {Config::Security_LockDatabaseMinimize, {QS("Security/LockDatabaseMinimize"), Roaming, false}},
{Config::Security_LockDatabaseScreenLock, {QS("Security/LockDatabaseScreenLock"), Roaming, true}}, {Config::Security_LockDatabaseScreenLock, {QS("Security/LockDatabaseScreenLock"), Roaming, true}},
{Config::Security_LockDatabaseOnUserSwitch, {QS("Security/LockDatabaseOnUserSwitch"), Roaming, true}}, {Config::Security_LockDatabaseOnUserSwitch, {QS("Security/LockDatabaseOnUserSwitch"), Roaming, true}},

View File

@@ -515,7 +515,7 @@ namespace Tools
"application/protobuf", "application/protobuf",
"application/x-zerosize"}; "application/x-zerosize"};
const static QStringList HtmlFormats = {"text/html"}; const static QStringList HtmlFormats = {"text/html"};
const static QStringList MarkdownFormats = {"text/markdown"}; const static QStringList MarkdownFormats = {"text/markdown", "text/x-web-markdown"};
const static QStringList ImageFormats = {"image/"}; const static QStringList ImageFormats = {"image/"};
static auto isCompatible = [](const QString& format, const QStringList& list) { static auto isCompatible = [](const QString& format, const QStringList& list) {

View File

@@ -194,9 +194,6 @@ MainWindow::MainWindow()
databaseLockButton->setPopupMode(QToolButton::MenuButtonPopup); databaseLockButton->setPopupMode(QToolButton::MenuButtonPopup);
} }
restoreGeometry(config()->get(Config::GUI_MainWindowGeometry).toByteArray());
restoreState(config()->get(Config::GUI_MainWindowState).toByteArray());
connect(m_ui->tabWidget, &DatabaseTabWidget::databaseLocked, this, &MainWindow::databaseLocked); connect(m_ui->tabWidget, &DatabaseTabWidget::databaseLocked, this, &MainWindow::databaseLocked);
connect(m_ui->tabWidget, &DatabaseTabWidget::databaseUnlocked, this, &MainWindow::databaseUnlocked); connect(m_ui->tabWidget, &DatabaseTabWidget::databaseUnlocked, this, &MainWindow::databaseUnlocked);
connect(m_ui->tabWidget, &DatabaseTabWidget::activeDatabaseChanged, this, &MainWindow::activeDatabaseChanged); connect(m_ui->tabWidget, &DatabaseTabWidget::activeDatabaseChanged, this, &MainWindow::activeDatabaseChanged);
@@ -642,12 +639,13 @@ MainWindow::MainWindow()
auto* hidePreRelWarn = new QAction(tr("Don't show again for this version"), m_ui->globalMessageWidget); auto* hidePreRelWarn = new QAction(tr("Don't show again for this version"), m_ui->globalMessageWidget);
m_ui->globalMessageWidget->addAction(hidePreRelWarn); m_ui->globalMessageWidget->addAction(hidePreRelWarn);
auto hidePreRelWarnConn = QSharedPointer<QMetaObject::Connection>::create(); auto hidePreRelWarnConn = QSharedPointer<QMetaObject::Connection>::create();
*hidePreRelWarnConn = connect(m_ui->globalMessageWidget, &KMessageWidget::hideAnimationFinished, [=] { *hidePreRelWarnConn = connect(
m_ui->globalMessageWidget, &KMessageWidget::hideAnimationFinished, [this, hidePreRelWarn, hidePreRelWarnConn] {
m_ui->globalMessageWidget->removeAction(hidePreRelWarn); m_ui->globalMessageWidget->removeAction(hidePreRelWarn);
disconnect(*hidePreRelWarnConn); disconnect(*hidePreRelWarnConn);
hidePreRelWarn->deleteLater(); hidePreRelWarn->deleteLater();
}); });
connect(hidePreRelWarn, &QAction::triggered, [=] { connect(hidePreRelWarn, &QAction::triggered, [this] {
m_ui->globalMessageWidget->animatedHide(); m_ui->globalMessageWidget->animatedHide();
config()->set(Config::Messages_HidePreReleaseWarning, KEEPASSXC_VERSION); config()->set(Config::Messages_HidePreReleaseWarning, KEEPASSXC_VERSION);
}); });
@@ -1382,6 +1380,12 @@ void MainWindow::showEvent(QShowEvent* event)
// Qt Hack - Prevent white flicker when showing window // Qt Hack - Prevent white flicker when showing window
QTimer::singleShot(50, this, [=] { setProperty("windowOpacity", 1.0); }); QTimer::singleShot(50, this, [=] { setProperty("windowOpacity", 1.0); });
#endif #endif
// Restore geometry and window state only on the first showEvent to prevent issues with minimized tray startup
if (!m_windowInformationRestored) {
restoreWindowInformation();
m_windowInformationRestored = true;
}
} }
void MainWindow::hideEvent(QHideEvent* event) void MainWindow::hideEvent(QHideEvent* event)
@@ -1539,6 +1543,12 @@ void MainWindow::saveWindowInformation()
} }
} }
void MainWindow::restoreWindowInformation()
{
restoreGeometry(config()->get(Config::GUI_MainWindowGeometry).toByteArray());
restoreState(config()->get(Config::GUI_MainWindowState).toByteArray());
}
bool MainWindow::saveLastDatabases() bool MainWindow::saveLastDatabases()
{ {
if (config()->get(Config::OpenPreviousDatabasesOnStartup).toBool()) { if (config()->get(Config::OpenPreviousDatabasesOnStartup).toBool()) {

View File

@@ -160,6 +160,7 @@ private:
static const QString BaseWindowTitle; static const QString BaseWindowTitle;
void saveWindowInformation(); void saveWindowInformation();
void restoreWindowInformation();
bool saveLastDatabases(); bool saveLastDatabases();
bool isTrayIconEnabled() const; bool isTrayIconEnabled() const;
void customOpenUrl(QString url); void customOpenUrl(QString url);
@@ -192,6 +193,7 @@ private:
Q_DISABLE_COPY(MainWindow) Q_DISABLE_COPY(MainWindow)
bool m_windowInformationRestored = false;
bool m_appExitCalled = false; bool m_appExitCalled = false;
bool m_appExiting = false; bool m_appExiting = false;
bool m_restartRequested = false; bool m_restartRequested = false;

View File

@@ -34,7 +34,7 @@
namespace namespace
{ {
// Extract group names from nested path and return the last group created // Extract group names from nested path and return the last group created
Group* createGroupStructure(Database* db, const QString& groupPath) Group* createGroupStructure(Database* db, const QString& groupPath, const QString& rootGroupToSkip)
{ {
auto group = db->rootGroup(); auto group = db->rootGroup();
if (!group || groupPath.isEmpty()) { if (!group || groupPath.isEmpty()) {
@@ -42,8 +42,10 @@ namespace
} }
auto nameList = groupPath.split("/", Qt::SkipEmptyParts); auto nameList = groupPath.split("/", Qt::SkipEmptyParts);
// Skip over first group name if root
if (nameList.first().compare("root", Qt::CaseInsensitive) == 0) { // Skip the identified root group name if present
if (!rootGroupToSkip.isEmpty() && !nameList.isEmpty()
&& nameList.first().compare(rootGroupToSkip, Qt::CaseInsensitive) == 0) {
nameList.removeFirst(); nameList.removeFirst();
} }
@@ -241,8 +243,26 @@ QSharedPointer<Database> CsvImportWidget::buildDatabase()
db->rootGroup()->setNotes(tr("Imported from CSV file: %1").arg(m_filename)); db->rootGroup()->setNotes(tr("Imported from CSV file: %1").arg(m_filename));
auto rows = m_parserModel->rowCount() - m_parserModel->skippedRows(); auto rows = m_parserModel->rowCount() - m_parserModel->skippedRows();
// Check for common root group
QString rootGroupName;
for (int r = 0; r < rows; ++r) { for (int r = 0; r < rows; ++r) {
auto group = createGroupStructure(db.data(), m_parserModel->data(m_parserModel->index(r, 0)).toString()); auto groupPath = m_parserModel->data(m_parserModel->index(r, 0)).toString();
auto groupName = groupPath.mid(0, groupPath.indexOf('/'));
if (!rootGroupName.isNull() && rootGroupName != groupName) {
rootGroupName.clear();
break;
}
rootGroupName = groupName;
}
if (!rootGroupName.isEmpty()) {
db->rootGroup()->setName(rootGroupName);
}
for (int r = 0; r < rows; ++r) {
auto group =
createGroupStructure(db.data(), m_parserModel->data(m_parserModel->index(r, 0)).toString(), rootGroupName);
if (!group) { if (!group) {
continue; continue;
} }

View File

@@ -27,6 +27,7 @@
#include "core/Config.h" #include "core/Config.h"
#include "core/Group.h" #include "core/Group.h"
#include "core/Resources.h" #include "core/Resources.h"
#include "core/Totp.h"
#include "crypto/Crypto.h" #include "crypto/Crypto.h"
#include "gui/MessageBox.h" #include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h" #include "gui/osutils/OSUtils.h"
@@ -75,6 +76,9 @@ void TestAutoType::init()
association.window = "custom window"; association.window = "custom window";
association.sequence = "{username}association{password}"; association.sequence = "{username}association{password}";
m_entry1->autoTypeAssociations()->add(association); m_entry1->autoTypeAssociations()->add(association);
// Create a totp with a short time step to test delayed typing
auto totpSettings = Totp::createSettings("NNSWK4DBONZXQYZB", Totp::DEFAULT_DIGITS, 2);
m_entry1->setTotp(totpSettings);
m_entry2 = new Entry(); m_entry2 = new Entry();
m_entry2->setGroup(m_group); m_entry2->setGroup(m_group);
@@ -470,3 +474,24 @@ void TestAutoType::testAutoTypeEmptyWindowAssociation()
assoc = m_entry6->autoTypeSequences("Some Other Window"); assoc = m_entry6->autoTypeSequences("Some Other Window");
QVERIFY(assoc.isEmpty()); QVERIFY(assoc.isEmpty());
} }
void TestAutoType::testAutoTypeTotpDelay()
{
// Get the TOTP time step in milliseconds
auto totpStep = m_entry1->totpSettings()->step * 1000;
auto sequence = QString("{TOTP} {DELAY %1}{TOTP}").arg(QString::number(totpStep * 2));
// Test 1: Sequence with a 3 second delay before TOTP
m_autoType->performAutoTypeWithSequence(m_entry1, sequence);
auto typedChars = m_test->actionChars();
// The typed TOTP should be different between the first and second one
auto totpParts = m_test->actionChars().split(' ');
QCOMPARE(totpParts.size(), 2);
QCOMPARE(totpParts[0].size(), m_entry1->totpSettings()->digits);
QCOMPARE(totpParts[1].size(), m_entry1->totpSettings()->digits);
QVERIFY2(totpParts[0] != totpParts[1],
QString("Typed TOTP (%1) should differ from current TOTP (%2) due to delay")
.arg(totpParts[0], totpParts[1])
.toLatin1());
}

View File

@@ -52,6 +52,7 @@ private slots:
void testAutoTypeSyntaxChecks(); void testAutoTypeSyntaxChecks();
void testAutoTypeEffectiveSequences(); void testAutoTypeEffectiveSequences();
void testAutoTypeEmptyWindowAssociation(); void testAutoTypeEmptyWindowAssociation();
void testAutoTypeTotpDelay();
private: private:
AutoTypePlatformInterface* m_platform; AutoTypePlatformInterface* m_platform;

View File

@@ -22,6 +22,7 @@
#include <QTest> #include <QTest>
#include "core/Group.h" #include "core/Group.h"
#include "core/Tools.h"
#include "core/Totp.h" #include "core/Totp.h"
#include "crypto/Crypto.h" #include "crypto/Crypto.h"
#include "format/CsvExporter.h" #include "format/CsvExporter.h"
@@ -110,3 +111,218 @@ void TestCsvExporter::testNestedGroups()
.append(ExpectedHeaderLine) .append(ExpectedHeaderLine)
.append("\"Passwords/Test Group Name/Test Sub Group Name\",\"Test Entry Title\",\"\",\"\",\"\",\"\""))); .append("\"Passwords/Test Group Name/Test Sub Group Name\",\"Test Entry Title\",\"\",\"\",\"\",\"\"")));
} }
void TestCsvExporter::testRoundTripWithCustomRootName()
{
// Create a database with a custom root group name
Group* groupRoot = m_db->rootGroup();
groupRoot->setName("MyPasswords"); // Custom root name instead of default "Passwords"
auto* group = new Group();
group->setName("Test Group");
group->setParent(groupRoot);
auto* entry = new Entry();
entry->setGroup(group);
entry->setTitle("Test Entry");
entry->setUsername("testuser");
entry->setPassword("testpass");
// Export to CSV
QString csvData = m_csvExporter->exportDatabase(m_db);
// Verify export contains the root group name in the path
QVERIFY(csvData.contains("\"MyPasswords/Test Group\""));
// Test the heuristic approach: analyze multiple similar paths
QStringList groupPaths = {"MyPasswords/Test Group", "MyPasswords/Another Group", "MyPasswords/Third Group"};
// Test the analyzeCommonRootGroup function logic
QStringList firstComponents;
for (const QString& path : groupPaths) {
if (!path.isEmpty() && !path.startsWith("/")) {
auto nameList = path.split("/", Qt::SkipEmptyParts);
if (!nameList.isEmpty()) {
firstComponents.append(nameList.first());
}
}
}
// All paths should have "MyPasswords" as first component
QCOMPARE(firstComponents.size(), 3);
QVERIFY(firstComponents.contains("MyPasswords"));
// With 100% consistency, "MyPasswords" should be identified as common root
QMap<QString, int> componentCounts;
for (const QString& component : firstComponents) {
componentCounts[component]++;
}
QCOMPARE(componentCounts["MyPasswords"], 3); // All 3 paths have this root
// Simulate the group creation with identified root to skip
QString groupPathFromCsv = "MyPasswords/Test Group";
auto nameList = groupPathFromCsv.split("/", Qt::SkipEmptyParts);
// New heuristic logic: skip identified root group name
QString rootGroupToSkip = "MyPasswords";
if (!rootGroupToSkip.isEmpty() && !nameList.isEmpty()
&& nameList.first().compare(rootGroupToSkip, Qt::CaseInsensitive) == 0) {
nameList.removeFirst();
}
// After this logic, nameList should contain only ["Test Group"]
QCOMPARE(nameList.size(), 1);
QCOMPARE(nameList.first(), QString("Test Group"));
}
void TestCsvExporter::testRoundTripWithDefaultRootName()
{
// Test with default "Passwords" root name to ensure it works correctly
Group* groupRoot = m_db->rootGroup();
// Default name is "Passwords" - don't change it
auto* group = new Group();
group->setName("Test Group");
group->setParent(groupRoot);
auto* entry = new Entry();
entry->setGroup(group);
entry->setTitle("Test Entry");
entry->setUsername("testuser");
entry->setPassword("testpass");
// Export to CSV
QString csvData = m_csvExporter->exportDatabase(m_db);
// Verify export contains the root group name in the path
QVERIFY(csvData.contains("\"Passwords/Test Group\""));
// Test the heuristic approach with consistent "Passwords" root
QStringList groupPaths = {"Passwords/Test Group", "Passwords/Work", "Passwords/Personal"};
// Simulate analysis to find common root
QStringList firstComponents;
for (const QString& path : groupPaths) {
if (!path.isEmpty() && !path.startsWith("/")) {
auto nameList = path.split("/", Qt::SkipEmptyParts);
if (!nameList.isEmpty()) {
firstComponents.append(nameList.first());
}
}
}
// All should have "Passwords" as first component
QCOMPARE(firstComponents.size(), 3);
for (const QString& component : firstComponents) {
QCOMPARE(component, QString("Passwords"));
}
// Test group creation with identified root to skip
QString groupPathFromCsv = "Passwords/Test Group";
auto nameList = groupPathFromCsv.split("/", Qt::SkipEmptyParts);
// Heuristic logic: skip the identified common root
QString rootGroupToSkip = "Passwords";
if (!rootGroupToSkip.isEmpty() && !nameList.isEmpty()
&& nameList.first().compare(rootGroupToSkip, Qt::CaseInsensitive) == 0) {
nameList.removeFirst();
}
// After this logic, nameList should contain only ["Test Group"]
QCOMPARE(nameList.size(), 1);
QCOMPARE(nameList.first(), QString("Test Group"));
}
void TestCsvExporter::testSingleLevelGroup()
{
// Test case: entry is directly in root group (no sub-groups)
// This should still work correctly and not remove any path components
Group* groupRoot = m_db->rootGroup();
auto* entry = new Entry();
entry->setGroup(groupRoot); // Put entry directly in root
entry->setTitle("Root Entry");
entry->setUsername("rootuser");
entry->setPassword("rootpass");
// Export to CSV
QString csvData = m_csvExporter->exportDatabase(m_db);
// Verify export contains just the root group name (no sub-path)
QVERIFY(csvData.contains("\"Passwords\",\"Root Entry\""));
// Test heuristic with single-component paths
QStringList groupPaths = {"Passwords", "Work", "Personal"}; // Mixed single components
// With inconsistent first components, no common root should be identified
QStringList firstComponents;
for (const QString& path : groupPaths) {
if (!path.isEmpty() && !path.startsWith("/")) {
auto nameList = path.split("/", Qt::SkipEmptyParts);
if (!nameList.isEmpty()) {
firstComponents.append(nameList.first());
}
}
}
// Should have 3 different first components
QCOMPARE(firstComponents.size(), 3);
auto uniqueComponents = Tools::asSet(firstComponents);
QCOMPARE(uniqueComponents.size(), 3); // All different
// Test group creation with no identified root to skip
QString groupPathFromCsv = "Passwords"; // Single component
auto nameList = groupPathFromCsv.split("/", Qt::SkipEmptyParts);
// With no common root identified, nothing should be removed
QString rootGroupToSkip = QString(); // Empty - no common root found
if (!rootGroupToSkip.isEmpty() && !nameList.isEmpty()
&& nameList.first().compare(rootGroupToSkip, Qt::CaseInsensitive) == 0) {
nameList.removeFirst();
}
// Should still have ["Passwords"] as nothing was removed
QCOMPARE(nameList.size(), 1);
QCOMPARE(nameList.first(), QString("Passwords"));
}
void TestCsvExporter::testAbsolutePaths()
{
// Test case: paths that start with "/" (absolute paths)
// According to the comment, if every row starts with "/", the root group should be left as is
QStringList groupPaths = {"/Work/Subgroup1", "/Personal/Subgroup2", "/Finance/Subgroup3"};
// Test the heuristic analysis with absolute paths
QStringList firstComponents;
for (const QString& path : groupPaths) {
if (!path.isEmpty() && !path.startsWith("/")) {
auto nameList = path.split("/", Qt::SkipEmptyParts);
if (!nameList.isEmpty()) {
firstComponents.append(nameList.first());
}
}
// Note: paths starting with "/" are skipped in the analysis
}
// Since all paths start with "/", no first components should be collected
QCOMPARE(firstComponents.size(), 0);
// With no first components, no common root should be identified
QString rootGroupToSkip = QString(); // Should be empty
// Test group creation with absolute path
QString groupPathFromCsv = "/Work/Subgroup1";
auto nameList = groupPathFromCsv.split("/", Qt::SkipEmptyParts);
// With no root to skip, the full path should be preserved
if (!rootGroupToSkip.isEmpty() && !nameList.isEmpty()
&& nameList.first().compare(rootGroupToSkip, Qt::CaseInsensitive) == 0) {
nameList.removeFirst();
}
// Should have ["Work", "Subgroup1"] - full path preserved
QCOMPARE(nameList.size(), 2);
QCOMPARE(nameList.at(0), QString("Work"));
QCOMPARE(nameList.at(1), QString("Subgroup1"));
}

View File

@@ -39,6 +39,10 @@ private slots:
void testExport(); void testExport();
void testEmptyDatabase(); void testEmptyDatabase();
void testNestedGroups(); void testNestedGroups();
void testRoundTripWithCustomRootName();
void testRoundTripWithDefaultRootName();
void testSingleLevelGroup();
void testAbsolutePaths();
private: private:
QSharedPointer<Database> m_db; QSharedPointer<Database> m_db;

View File

@@ -403,8 +403,8 @@ void TestTools::testGetMimeTypeByFileInfo()
const QStringList Markdowns = {"test.md", "test.markdown"}; const QStringList Markdowns = {"test.md", "test.markdown"};
for (const auto& makdown : Markdowns) { for (const auto& markdown : Markdowns) {
QCOMPARE(Tools::getMimeType(QFileInfo(makdown)), Tools::MimeType::Markdown); QCOMPARE(Tools::getMimeType(QFileInfo(markdown)), Tools::MimeType::Markdown);
} }
const QStringList UnknownHeaders = {"test.doc", "test.pdf", "test.docx"}; const QStringList UnknownHeaders = {"test.doc", "test.pdf", "test.docx"};

View File

@@ -1,5 +1,10 @@
module github.com/keepassxreboot/keepassxc/keepassxc-cr-recovery module github.com/keepassxreboot/keepassxc/keepassxc-cr-recovery
go 1.13 go 1.24.0
require golang.org/x/crypto v0.35.0 require golang.org/x/crypto v0.45.0
require (
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
)

View File

@@ -1,67 +1,6 @@
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -1,7 +1,7 @@
{ {
"name": "keepassxc", "name": "keepassxc",
"version-string": "2.8.0", "version-string": "2.8.0",
"builtin-baseline": "74e6536215718009aae747d86d84b78376bf9e09", "builtin-baseline": "dfb72f61c5a066ab75cd0bdcb2e007228bfc3270",
"dependencies": [ "dependencies": [
{ {
"name": "argon2", "name": "argon2",