mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-12-04 15:39:34 +01:00
Compare commits
16 Commits
5332075193
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66ff42c78b | ||
|
|
fc5f504509 | ||
|
|
6c963e0000 | ||
|
|
72308a1706 | ||
|
|
a5c9ffbef7 | ||
|
|
2900f919c8 | ||
|
|
6c59b5db98 | ||
|
|
1a4e9ca4e2 | ||
|
|
a2e7132ead | ||
|
|
4e59c1c579 | ||
|
|
c09ba0113b | ||
|
|
f39e0937b9 | ||
|
|
f484d7f5ed | ||
|
|
10bd651355 | ||
|
|
b3dbc49161 | ||
|
|
eefee1f092 |
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -2,10 +2,10 @@ name: "CodeQL"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ 'develop', 'release/2.7.x' ]
|
branches:
|
||||||
|
- 'develop'
|
||||||
|
- 'release/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ 'develop' ]
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '5 16 * * 3'
|
- cron: '5 16 * * 3'
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ CMakePresets.json
|
|||||||
CMakeUserPresets.json
|
CMakeUserPresets.json
|
||||||
.vs/
|
.vs/
|
||||||
out/
|
out/
|
||||||
|
\.clangd
|
||||||
|
|
||||||
# vcpkg
|
# vcpkg
|
||||||
vcpkg_installed*/
|
vcpkg_installed*/
|
||||||
|
|||||||
@@ -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,17 +229,23 @@ if("${CMAKE_SIZEOF_VOID_P}" EQUAL "4")
|
|||||||
set(IS_32BIT TRUE)
|
set(IS_32BIT TRUE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(CLANG_COMPILER_ID_REGEX "^(Apple)?[Cc]lang$")
|
if("${CMAKE_CXX_COMPILER}" MATCHES "clang-cl(.exe)?$")
|
||||||
if("${CMAKE_C_COMPILER}" MATCHES "clang$"
|
# clang-cl uses MSVC compiler flags
|
||||||
OR "${CMAKE_EXTRA_GENERATOR_C_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__"
|
set(MSVC 1)
|
||||||
OR "${CMAKE_C_COMPILER_ID}" MATCHES ${CLANG_COMPILER_ID_REGEX})
|
set(CMAKE_COMPILER_IS_CLANG_MSVC 1)
|
||||||
set(CMAKE_COMPILER_IS_CLANG 1)
|
else()
|
||||||
endif()
|
set(CLANG_COMPILER_ID_REGEX "^(Apple)?[Cc]lang$")
|
||||||
|
if("${CMAKE_C_COMPILER}" MATCHES "clang$"
|
||||||
|
OR "${CMAKE_EXTRA_GENERATOR_C_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__"
|
||||||
|
OR "${CMAKE_C_COMPILER_ID}" MATCHES ${CLANG_COMPILER_ID_REGEX})
|
||||||
|
set(CMAKE_COMPILER_IS_CLANG 1)
|
||||||
|
endif()
|
||||||
|
|
||||||
if("${CMAKE_CXX_COMPILER}" MATCHES "clang(\\+\\+)?$"
|
if("${CMAKE_CXX_COMPILER}" MATCHES "clang(\\+\\+)?$"
|
||||||
OR "${CMAKE_EXTRA_GENERATOR_CXX_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__"
|
OR "${CMAKE_EXTRA_GENERATOR_CXX_SYSTEM_DEFINED_MACROS}" 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)
|
||||||
@@ -395,11 +408,15 @@ 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)
|
||||||
if(IS_DEBUG_BUILD)
|
# Clang-cl does not support /MP, /Zf, or /fsanitize=address
|
||||||
add_compile_options(/Zf)
|
if (NOT CMAKE_COMPILER_IS_CLANG_MSVC)
|
||||||
if(MSVC_TOOLSET_VERSION GREATER 141)
|
add_compile_options(/MP)
|
||||||
add_compile_definitions(/fsanitize=address)
|
if(IS_DEBUG_BUILD)
|
||||||
|
add_compile_options(/Zf)
|
||||||
|
if(MSVC_TOOLSET_VERSION GREATER 141)
|
||||||
|
add_compile_definitions(/fsanitize=address)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
102
cmake/MacOSCodesign.cmake.in
Normal file
102
cmake/MacOSCodesign.cmake.in
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# 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 --deep --entitlements=${ENTITLEMENTS} ${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 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@")
|
||||||
|
file(GLOB_RECURSE DMG_FILE "${CPACK_PACKAGE_DIRECTORY}/${CPACK_PACKAGE_FILE_NAME}.dmg")
|
||||||
|
|
||||||
|
if(NOT KEYCHAIN_PROFILE)
|
||||||
|
message(FATAL_ERROR "No notarization credentials keychain profile specified.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# 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.")
|
||||||
|
|
||||||
|
endif()
|
||||||
79
cmake/WindowsCodesign.cmake.in
Normal file
79
cmake/WindowsCodesign.cmake.in
Normal 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()
|
||||||
@@ -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()
|
|
||||||
242
release-tool.py
242
release-tool.py
@@ -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
|
||||||
###########################################################################################
|
###########################################################################################
|
||||||
@@ -585,6 +627,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 +643,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 +725,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 +767,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,9 +794,13 @@ 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')
|
||||||
|
|
||||||
logger.info('All done! Please don\'t forget to sign the binaries before distribution.')
|
if sign:
|
||||||
|
logger.info('All done!')
|
||||||
|
else:
|
||||||
|
logger.info('All done! Please don\'t forget to sign the binaries before distribution.')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _download_tools_if_not_available(toolname, bin_dir, url, docker_args=None):
|
def _download_tools_if_not_available(toolname, bin_dir, url, docker_args=None):
|
||||||
@@ -888,162 +950,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='.')
|
help='Read Apple credentials for notarization from a keychain (default: %(default)s).')
|
||||||
|
|
||||||
if sys.platform == 'darwin':
|
def run(self, file, keychain_profile, **_):
|
||||||
parser.add_argument('-n', '--notarize', help='Notarize signed file(s).', action='store_true')
|
if sys.platform != 'darwin':
|
||||||
parser.add_argument('-c', '--keychain-profile', default='notarization-creds',
|
raise Error('Unsupported platform.')
|
||||||
help='Read Apple credentials for notarization from a keychain (default: %(default)s).')
|
|
||||||
|
|
||||||
def run(self, file, identity, src_dir, **kwargs):
|
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)
|
||||||
@@ -1271,9 +1208,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)
|
||||||
|
|||||||
@@ -67,11 +67,6 @@ if(UNIX AND NOT APPLE AND NOT HAIKU)
|
|||||||
install(FILES linux/${APP_ID}.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
|
install(FILES linux/${APP_ID}.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
|
||||||
endif(UNIX AND NOT APPLE AND NOT HAIKU)
|
endif(UNIX AND NOT APPLE AND NOT HAIKU)
|
||||||
|
|
||||||
if(APPLE)
|
|
||||||
install(FILES macosx/Assets.car DESTINATION ${DATA_INSTALL_DIR})
|
|
||||||
install(FILES macosx/keepassxc.icns DESTINATION ${DATA_INSTALL_DIR})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
install(FILES windows/qt.conf DESTINATION ${BIN_INSTALL_DIR})
|
install(FILES windows/qt.conf DESTINATION ${BIN_INSTALL_DIR})
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>${PROGNAME}</string>
|
<string>${PROGNAME}</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>keepassxc.icns</string>
|
<string>${MACOSX_BUNDLE_ICON_NAME}.icns</string>
|
||||||
<key>CFBundleIconName</key>
|
<key>CFBundleIconName</key>
|
||||||
<string>keepassxc</string>
|
<string>${MACOSX_BUNDLE_ICON_NAME}</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>org.keepassxc.keepassxc</string>
|
<string>${MACOSX_BUNDLE_IDENTIFIER}</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
|
|||||||
@@ -1799,6 +1799,10 @@ Are you sure you want to continue with this file?.</source>
|
|||||||
<source>Press ESC again to close this database</source>
|
<source>Press ESC again to close this database</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>The database file does not exist or is not accessible.</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>DatabaseSettingWidgetMetaData</name>
|
<name>DatabaseSettingWidgetMetaData</name>
|
||||||
@@ -2608,10 +2612,6 @@ This is definitely a bug, please report it to the developers.</source>
|
|||||||
<source>Open database</source>
|
<source>Open database</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
|
||||||
<source>Failed to open %1. It either does not exist or is not accessible.</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
<message>
|
||||||
<source>CSV file</source>
|
<source>CSV file</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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,13 +415,18 @@ 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()
|
||||||
|
|
||||||
# Main Executable Definition
|
# Main Executable Definition
|
||||||
|
add_executable(${PROGNAME} main.cpp)
|
||||||
|
target_link_libraries(${PROGNAME} keepassxc_gui)
|
||||||
|
set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
set_target_properties(${PROGNAME} PROPERTIES WIN32_EXECUTABLE ON)
|
||||||
include(GenerateProductVersion)
|
include(GenerateProductVersion)
|
||||||
generate_product_version(
|
generate_product_version(
|
||||||
WIN32_ResourceFiles
|
WIN32_ResourceFiles
|
||||||
@@ -432,26 +437,43 @@ if(WIN32)
|
|||||||
VERSION_PATCH ${KEEPASSXC_VERSION_PATCH}
|
VERSION_PATCH ${KEEPASSXC_VERSION_PATCH}
|
||||||
)
|
)
|
||||||
list(APPEND WIN32_ResourceFiles "${CMAKE_SOURCE_DIR}/share/windows/icon.rc")
|
list(APPEND WIN32_ResourceFiles "${CMAKE_SOURCE_DIR}/share/windows/icon.rc")
|
||||||
endif()
|
target_sources(${PROGNAME} PUBLIC ${WIN32_ResourceFiles})
|
||||||
|
|
||||||
add_executable(${PROGNAME} WIN32 main.cpp ${WIN32_ResourceFiles})
|
elseif(APPLE AND WITH_APP_BUNDLE)
|
||||||
target_link_libraries(${PROGNAME} keepassxc_gui)
|
set(MACOSX_BUNDLE_IDENTIFIER org.keepassxc.keepassxc)
|
||||||
set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
|
set(MACOSX_BUNDLE_ICON_NAME keepassxc)
|
||||||
|
set(MACOSX_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements")
|
||||||
# macOS App Bundle
|
configure_file("${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake" ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
|
||||||
if(APPLE AND WITH_APP_BUNDLE)
|
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
|
||||||
configure_file(${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
|
"${CMAKE_SOURCE_DIR}/share/macosx/Assets.car"
|
||||||
|
"${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.icns"
|
||||||
|
)
|
||||||
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}"
|
||||||
|
)
|
||||||
|
target_sources(${PROGNAME} PUBLIC ${MACOSX_BUNDLE_RESOURCE_FILES})
|
||||||
|
|
||||||
if(QT_MAC_USE_COCOA AND EXISTS "${QT_LIBRARY_DIR}/Resources/qt_menu.nib")
|
if(QT_MAC_USE_COCOA AND EXISTS "${QT_LIBRARY_DIR}/Resources/qt_menu.nib")
|
||||||
install(DIRECTORY "${QT_LIBRARY_DIR}/Resources/qt_menu.nib"
|
install(DIRECTORY "${QT_LIBRARY_DIR}/Resources/qt_menu.nib"
|
||||||
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")
|
||||||
@@ -483,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})
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
|||||||
{Config::Security_ClearSearch, {QS("Security/ClearSearch"), Roaming, false}},
|
{Config::Security_ClearSearch, {QS("Security/ClearSearch"), Roaming, false}},
|
||||||
{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, false}},
|
{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}},
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr int clearFormsDelay = 30000;
|
constexpr int clearFormsDelay = 30000;
|
||||||
|
constexpr int fileExistsCheckInterval = 5000;
|
||||||
|
|
||||||
bool isQuickUnlockAvailable()
|
bool isQuickUnlockAvailable()
|
||||||
{
|
{
|
||||||
@@ -68,6 +69,16 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
|
|||||||
m_ui->editPassword->setShowPassword(false);
|
m_ui->editPassword->setShowPassword(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
m_fileExistsTimer.setInterval(fileExistsCheckInterval);
|
||||||
|
m_fileExistsTimer.setSingleShot(false);
|
||||||
|
connect(&m_fileExistsTimer, &QTimer::timeout, this, [this] {
|
||||||
|
if (!QFile::exists(m_filename)) {
|
||||||
|
m_ui->messageWidget->showMessage(tr("The database file does not exist or is not accessible."),
|
||||||
|
MessageWidget::Warning,
|
||||||
|
fileExistsCheckInterval + 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
QFont font;
|
QFont font;
|
||||||
font.setPointSize(font.pointSize() + 4);
|
font.setPointSize(font.pointSize() + 4);
|
||||||
font.setBold(true);
|
font.setBold(true);
|
||||||
@@ -215,6 +226,7 @@ bool DatabaseOpenWidget::event(QEvent* event)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isVisible()) {
|
if (isVisible()) {
|
||||||
|
m_fileExistsTimer.start();
|
||||||
m_hideTimer.stop();
|
m_hideTimer.stop();
|
||||||
pollHardwareKey();
|
pollHardwareKey();
|
||||||
}
|
}
|
||||||
@@ -226,6 +238,8 @@ bool DatabaseOpenWidget::event(QEvent* event)
|
|||||||
m_hideTimer.start();
|
m_hideTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_fileExistsTimer.stop();
|
||||||
|
|
||||||
#ifdef WITH_XC_YUBIKEY
|
#ifdef WITH_XC_YUBIKEY
|
||||||
if (type == QEvent::Hide) {
|
if (type == QEvent::Hide) {
|
||||||
m_deviceListener->deregisterAllHotplugCallbacks();
|
m_deviceListener->deregisterAllHotplugCallbacks();
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ private:
|
|||||||
bool m_triedToQuit = false;
|
bool m_triedToQuit = false;
|
||||||
QTimer m_hideTimer;
|
QTimer m_hideTimer;
|
||||||
QTimer m_hideNoHardwareKeysFoundTimer;
|
QTimer m_hideNoHardwareKeysFoundTimer;
|
||||||
|
QTimer m_fileExistsTimer;
|
||||||
|
|
||||||
Q_DISABLE_COPY(DatabaseOpenWidget)
|
Q_DISABLE_COPY(DatabaseOpenWidget)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -163,22 +163,29 @@ void DatabaseTabWidget::addDatabaseTab(const QString& filePath,
|
|||||||
QString canonicalFilePath = fileInfo.canonicalFilePath();
|
QString canonicalFilePath = fileInfo.canonicalFilePath();
|
||||||
|
|
||||||
if (canonicalFilePath.isEmpty()) {
|
if (canonicalFilePath.isEmpty()) {
|
||||||
emit messageGlobal(tr("Failed to open %1. It either does not exist or is not accessible.").arg(cleanFilePath),
|
// The file does not exist, revert back to the cleaned path for comparison
|
||||||
MessageWidget::Error);
|
canonicalFilePath = cleanFilePath;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to find an existing tab with the same file path
|
||||||
for (int i = 0, c = count(); i < c; ++i) {
|
for (int i = 0, c = count(); i < c; ++i) {
|
||||||
auto* dbWidget = databaseWidgetFromIndex(i);
|
auto* dbWidget = databaseWidgetFromIndex(i);
|
||||||
Q_ASSERT(dbWidget);
|
if (dbWidget) {
|
||||||
if (dbWidget
|
auto dbFilePath = dbWidget->database()->canonicalFilePath();
|
||||||
&& dbWidget->database()->canonicalFilePath().compare(canonicalFilePath, FILE_CASE_SENSITIVE) == 0) {
|
if (dbFilePath.isEmpty()) {
|
||||||
dbWidget->performUnlockDatabase(password, keyfile);
|
// The file does not exist, revert back to the cleaned path for comparison
|
||||||
if (!inBackground) {
|
dbFilePath = QDir::toNativeSeparators(dbWidget->database()->filePath());
|
||||||
// switch to existing tab if file is already open
|
}
|
||||||
setCurrentIndex(indexOf(dbWidget));
|
if (dbFilePath.compare(canonicalFilePath, FILE_CASE_SENSITIVE) == 0) {
|
||||||
|
// Attempt to unlock the database if password and/or keyfile is provided
|
||||||
|
dbWidget->performUnlockDatabase(password, keyfile);
|
||||||
|
if (!inBackground) {
|
||||||
|
// switch to existing tab if file is already open
|
||||||
|
setCurrentIndex(indexOf(dbWidget));
|
||||||
|
}
|
||||||
|
// Prevent opening a new tab for this file
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,12 +92,14 @@ QIcon Icons::trayIcon(bool unlocked)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QIcon i;
|
QIcon i;
|
||||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
#if defined(Q_OS_WIN)
|
||||||
if (osUtils->isStatusBarDark()) {
|
if (osUtils->isStatusBarDark()) {
|
||||||
i = icon(QString("keepassxc-monochrome-light%1").arg(suffix), false);
|
i = icon(QString("keepassxc-monochrome-light%1").arg(suffix), false);
|
||||||
} else {
|
} else {
|
||||||
i = icon(QString("keepassxc-monochrome-dark%1").arg(suffix), false);
|
i = icon(QString("keepassxc-monochrome-dark%1").arg(suffix), false);
|
||||||
}
|
}
|
||||||
|
#elif defined(Q_OS_MACOS)
|
||||||
|
i = icon(QString("keepassxc-monochrome-light%1").arg(suffix), false);
|
||||||
#else
|
#else
|
||||||
i = icon(QString("%1-%2%3").arg(applicationIconName(), iconAppearance, suffix), false);
|
i = icon(QString("%1-%2%3").arg(applicationIconName(), iconAppearance, suffix), false);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -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->removeAction(hidePreRelWarn);
|
m_ui->globalMessageWidget, &KMessageWidget::hideAnimationFinished, [this, hidePreRelWarn, hidePreRelWarnConn] {
|
||||||
disconnect(*hidePreRelWarnConn);
|
m_ui->globalMessageWidget->removeAction(hidePreRelWarn);
|
||||||
hidePreRelWarn->deleteLater();
|
disconnect(*hidePreRelWarnConn);
|
||||||
});
|
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);
|
||||||
});
|
});
|
||||||
@@ -716,7 +714,7 @@ void MainWindow::restoreConfigState()
|
|||||||
if (config()->get(Config::OpenPreviousDatabasesOnStartup).toBool()) {
|
if (config()->get(Config::OpenPreviousDatabasesOnStartup).toBool()) {
|
||||||
const QStringList fileNames = config()->get(Config::LastOpenedDatabases).toStringList();
|
const QStringList fileNames = config()->get(Config::LastOpenedDatabases).toStringList();
|
||||||
for (const QString& filename : fileNames) {
|
for (const QString& filename : fileNames) {
|
||||||
if (!filename.isEmpty() && QFile::exists(filename)) {
|
if (!filename.isEmpty()) {
|
||||||
openDatabase(filename);
|
openDatabase(filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"};
|
||||||
|
|||||||
@@ -2464,6 +2464,41 @@ void TestGui::testMenuActionStates()
|
|||||||
QVERIFY(isActionEnabled("actionPasswordGenerator"));
|
QVERIFY(isActionEnabled("actionPasswordGenerator"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestGui::testOpenMissingDatabaseFile()
|
||||||
|
{
|
||||||
|
// Test that when trying to open a non-existent database file,
|
||||||
|
// the unlock dialog is still shown (instead of auto-closing)
|
||||||
|
// This allows user to retry when the file becomes available (e.g., cloud storage mounting)
|
||||||
|
|
||||||
|
const QString nonExistentPath = "/tmp/does_not_exist.kdbx";
|
||||||
|
|
||||||
|
// Ensure the file doesn't exist
|
||||||
|
QFile::remove(nonExistentPath);
|
||||||
|
QVERIFY(!QFile::exists(nonExistentPath));
|
||||||
|
|
||||||
|
// Record initial tab count
|
||||||
|
int initialTabCount = m_tabWidget->count();
|
||||||
|
|
||||||
|
// Try to add database tab with non-existent file
|
||||||
|
// This should NOT fail but should create a tab and show unlock dialog
|
||||||
|
m_tabWidget->addDatabaseTab(nonExistentPath);
|
||||||
|
|
||||||
|
// Verify that a tab was created (unlock dialog shown)
|
||||||
|
QCOMPARE(m_tabWidget->count(), initialTabCount + 1);
|
||||||
|
|
||||||
|
// Get the database widget for the new tab
|
||||||
|
auto* dbWidget = m_tabWidget->currentDatabaseWidget();
|
||||||
|
QVERIFY(dbWidget);
|
||||||
|
|
||||||
|
// Verify the database is in a state where it can be unlocked
|
||||||
|
// (not closed/rejected due to missing file)
|
||||||
|
QVERIFY(dbWidget->isLocked());
|
||||||
|
|
||||||
|
// Close the tab to clean up
|
||||||
|
m_tabWidget->closeDatabaseTab(m_tabWidget->currentIndex());
|
||||||
|
QCOMPARE(m_tabWidget->count(), initialTabCount);
|
||||||
|
}
|
||||||
|
|
||||||
void TestGui::addCannedEntries()
|
void TestGui::addCannedEntries()
|
||||||
{
|
{
|
||||||
// Find buttons
|
// Find buttons
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ private slots:
|
|||||||
void testTrayRestoreHide();
|
void testTrayRestoreHide();
|
||||||
void testShortcutConfig();
|
void testShortcutConfig();
|
||||||
void testMenuActionStates();
|
void testMenuActionStates();
|
||||||
|
void testOpenMissingDatabaseFile();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void addCannedEntries();
|
void addCannedEntries();
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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=
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user