mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-12-04 15:39:34 +01:00
Compare commits
5 Commits
develop
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f15ba49fc6 | ||
|
|
67b550bb6e | ||
|
|
4e2c06b943 | ||
|
|
656e0c71a3 | ||
|
|
d2ad2a95fe |
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -2,10 +2,10 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'release/**'
|
||||
branches: [ 'develop', 'release/2.7.x' ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ 'develop' ]
|
||||
schedule:
|
||||
- cron: '5 16 * * 3'
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,7 +27,6 @@ CMakePresets.json
|
||||
CMakeUserPresets.json
|
||||
.vs/
|
||||
out/
|
||||
\.clangd
|
||||
|
||||
# vcpkg
|
||||
vcpkg_installed*/
|
||||
|
||||
69
CHANGELOG.md
69
CHANGELOG.md
@@ -3,75 +3,6 @@
|
||||
## 2.8.0 (Pending)
|
||||
* 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)
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -60,17 +60,10 @@ option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF)
|
||||
option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON)
|
||||
if(UNIX AND NOT APPLE)
|
||||
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()
|
||||
option(WITH_XC_DOCS "Enable building of documentation" ON)
|
||||
if(WIN32 OR APPLE)
|
||||
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()
|
||||
|
||||
set(WITH_XC_X11 ON CACHE BOOL "Enable building with X11 deps")
|
||||
|
||||
if(APPLE)
|
||||
# Perform the platform checks before applying the stricter compiler flags.
|
||||
@@ -229,23 +222,17 @@ if("${CMAKE_SIZEOF_VOID_P}" EQUAL "4")
|
||||
set(IS_32BIT TRUE)
|
||||
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$")
|
||||
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()
|
||||
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(\\+\\+)?$"
|
||||
OR "${CMAKE_EXTRA_GENERATOR_CXX_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__"
|
||||
OR "${CMAKE_CXX_COMPILER_ID}" MATCHES ${CLANG_COMPILER_ID_REGEX})
|
||||
set(CMAKE_COMPILER_IS_CLANGXX 1)
|
||||
endif()
|
||||
if("${CMAKE_CXX_COMPILER}" MATCHES "clang(\\+\\+)?$"
|
||||
OR "${CMAKE_EXTRA_GENERATOR_CXX_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__"
|
||||
OR "${CMAKE_CXX_COMPILER_ID}" MATCHES ${CLANG_COMPILER_ID_REGEX})
|
||||
set(CMAKE_COMPILER_IS_CLANGXX 1)
|
||||
endif()
|
||||
|
||||
macro(add_gcc_compiler_cxxflags FLAGS)
|
||||
@@ -408,15 +395,11 @@ if (MSVC)
|
||||
if(MSVC_TOOLSET_VERSION LESS 141)
|
||||
message(FATAL_ERROR "Only Microsoft Visual Studio 17 and newer are supported!")
|
||||
endif()
|
||||
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)
|
||||
add_compile_options(/Zf)
|
||||
if(MSVC_TOOLSET_VERSION GREATER 141)
|
||||
add_compile_definitions(/fsanitize=address)
|
||||
endif()
|
||||
add_compile_options(/permissive- /utf-8 /MP)
|
||||
if(IS_DEBUG_BUILD)
|
||||
add_compile_options(/Zf)
|
||||
if(MSVC_TOOLSET_VERSION GREATER 141)
|
||||
add_compile_definitions(/fsanitize=address)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
@@ -432,7 +415,7 @@ if(WIN32)
|
||||
# By default MSVC enables NXCOMPAT
|
||||
add_compile_options(/guard:cf)
|
||||
add_link_options(/DYNAMICBASE /HIGHENTROPYVA /GUARD:CF)
|
||||
else()
|
||||
else(MINGW)
|
||||
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")
|
||||
# Enable high entropy ASLR for 64-bit builds
|
||||
@@ -442,8 +425,6 @@ if(WIN32)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
# Determine if we can link against the Windows SDK, used for Windows Hello support
|
||||
find_library(WINSDK WindowsApp.lib)
|
||||
endif()
|
||||
|
||||
if(APPLE AND WITH_APP_BUNDLE OR WIN32)
|
||||
|
||||
@@ -36,7 +36,7 @@ find_library(
|
||||
NAMES ${BOTAN_NAMES}
|
||||
PATH_SUFFIXES release/lib lib
|
||||
DOC "The Botan (release) library")
|
||||
if(WIN32 AND NOT MINGW)
|
||||
if(MSVC)
|
||||
find_library(
|
||||
BOTAN_LIBRARY_DEBUG
|
||||
NAMES ${BOTAN_NAMES_DEBUG}
|
||||
@@ -55,7 +55,7 @@ endif()
|
||||
|
||||
if(BOTAN_FOUND)
|
||||
set(BOTAN_INCLUDE_DIRS ${BOTAN_INCLUDE_DIR})
|
||||
if(WIN32 AND NOT MINGW)
|
||||
if(MSVC)
|
||||
set(BOTAN_LIBRARIES optimized ${BOTAN_LIBRARY} debug ${BOTAN_LIBRARY_DEBUG})
|
||||
else()
|
||||
set(BOTAN_LIBRARIES ${BOTAN_LIBRARY})
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
find_path(QRENCODE_INCLUDE_DIR NAMES qrencode.h)
|
||||
|
||||
if(WIN32 AND NOT MINGW)
|
||||
if(WIN32 AND MSVC)
|
||||
find_library(QRENCODE_LIBRARY_RELEASE qrencode)
|
||||
find_library(QRENCODE_LIBRARY_DEBUG qrencoded)
|
||||
set(QRENCODE_LIBRARY optimized ${QRENCODE_LIBRARY_RELEASE} debug ${QRENCODE_LIBRARY_DEBUG})
|
||||
|
||||
@@ -1,101 +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/>.
|
||||
|
||||
|
||||
# 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()
|
||||
@@ -1,79 +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(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()
|
||||
71
cmake/WindowsPostInstall.cmake.in
Normal file
71
cmake/WindowsPostInstall.cmake.in
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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()
|
||||
@@ -49,8 +49,6 @@ This section contains full details on advanced features available in KeePassXC.
|
||||
|{DB_DIR} |Absolute directory path of database file
|
||||
|===
|
||||
|
||||
NOTE: You can insert literal placeholder strings by escaping the beginning and ending curly braces. For example, to insert the string `{USERNAME}`, you would type `++\{USERNAME\}++`.
|
||||
|
||||
=== Entry Cross-Reference
|
||||
A reference to another entry's field is possible using the shorthand syntax:
|
||||
`{REF:<FIELD>@<SEARCH_IN>:<SEARCH_TEXT>}`
|
||||
|
||||
281
release-tool.py
281
release-tool.py
@@ -25,10 +25,12 @@ import lzma
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import signal
|
||||
import shutil
|
||||
import stat
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
@@ -41,7 +43,7 @@ from urllib.request import urlretrieve
|
||||
###########################################################################################
|
||||
|
||||
# class Check(Command)
|
||||
# class Tag(Command)
|
||||
# class Merge(Command)
|
||||
# class Build(Command)
|
||||
# class BuildSrc(Command)
|
||||
# class AppSign(Command)
|
||||
@@ -129,8 +131,7 @@ fmt = LogFormatter()
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(fmt)
|
||||
logger = logging.getLogger(__file__)
|
||||
logger.setLevel(os.getenv('LOGLEVEL')
|
||||
if type(logging.getLevelName(os.environ.get('LOGLEVEL'))) is int else logging.INFO)
|
||||
logger.setLevel(os.getenv('LOGLEVEL') if 'LOGLEVEL' in os.environ else logging.INFO)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
###########################################################################################
|
||||
@@ -187,12 +188,11 @@ def _run(cmd, *args, cwd, path=None, env=None, input=None, capture_output=True,
|
||||
env['FORCE_COLOR'] = '1'
|
||||
|
||||
if docker_image:
|
||||
cwd2 = Path(cwd or '.').absolute()
|
||||
docker_cmd = ['docker', 'run', '--rm', '--tty=true', f'--workdir={cwd2}', f'--user={os.getuid()}:{os.getgid()}']
|
||||
docker_cmd = ['docker', 'run', '--rm', '--tty=true', f'--workdir={cwd}', 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']])
|
||||
if path:
|
||||
docker_cmd.append(f'--env=PATH={path}')
|
||||
docker_cmd.append(f'--volume={cwd2}:{cwd2}:rw')
|
||||
docker_cmd.append(f'--volume={Path(cwd).absolute()}:{Path(cwd).absolute()}:rw')
|
||||
if docker_mounts:
|
||||
docker_cmd.extend([f'--volume={Path(d).absolute()}:{Path(d).absolute()}:rw' for d in docker_mounts])
|
||||
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
|
||||
|
||||
try:
|
||||
logger.debug('Running command: %s', ' '.join(map(str, cmd)))
|
||||
logger.debug('Running command: %s', ' '.join(cmd))
|
||||
return subprocess.run(
|
||||
cmd, *args,
|
||||
input=input,
|
||||
@@ -342,48 +342,6 @@ def _capture_vs_env(arch='amd64'):
|
||||
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
|
||||
###########################################################################################
|
||||
@@ -450,7 +408,6 @@ class Check(Command):
|
||||
cls.check_version_in_cmake(version, src_dir)
|
||||
cls.check_changelog(version, src_dir)
|
||||
cls.check_app_stream_info(version, src_dir)
|
||||
return git_ref
|
||||
|
||||
@staticmethod
|
||||
def check_src_dir_exists(src_dir):
|
||||
@@ -491,7 +448,7 @@ class Check(Command):
|
||||
cmakelists = Path(cwd) / cmakelists
|
||||
if not cmakelists.is_file():
|
||||
raise Error('File not found: %s', cmakelists)
|
||||
cmakelists_text = cmakelists.read_text("UTF-8")
|
||||
cmakelists_text = cmakelists.read_text()
|
||||
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)
|
||||
patch = re.search(r'^set\(KEEPASSXC_VERSION_PATCH "(\d+)"\)$', cmakelists_text, re.MULTILINE).group(1)
|
||||
@@ -507,7 +464,7 @@ class Check(Command):
|
||||
if not changelog.is_file():
|
||||
raise Error('File not found: %s', changelog)
|
||||
major, minor, patch = _split_version(version)
|
||||
if not re.search(rf'^## {major}\.{minor}\.{patch} \(.+?\)\n+', changelog.read_text("UTF-8"), re.MULTILINE):
|
||||
if not re.search(rf'^## {major}\.{minor}\.{patch} \(.+?\)\n+', changelog.read_text(), re.MULTILINE):
|
||||
raise Error(f'{changelog} has not been updated to the "%s" release.', version)
|
||||
|
||||
@staticmethod
|
||||
@@ -542,8 +499,8 @@ class Check(Command):
|
||||
raise Error('xcrun command not found! Please check that you have correctly installed Xcode.')
|
||||
|
||||
|
||||
class Tag(Command):
|
||||
"""Update translations and tag release."""
|
||||
class Merge(Command):
|
||||
"""Merge release branch into main branch and create release tags."""
|
||||
|
||||
@classmethod
|
||||
def setup_arg_parser(cls, parser: argparse.ArgumentParser):
|
||||
@@ -565,7 +522,7 @@ class Tag(Command):
|
||||
skip_translations, tx_resource, tx_min_perc):
|
||||
major, minor, patch = _split_version(version)
|
||||
Check.perform_basic_checks(src_dir)
|
||||
release_branch = Check.perform_version_checks(version, src_dir, release_branch)
|
||||
Check.perform_version_checks(version, src_dir, release_branch)
|
||||
Check.check_gnupg()
|
||||
sign_key = GPGSign.get_secret_key(sign_key)
|
||||
|
||||
@@ -576,7 +533,7 @@ class Tag(Command):
|
||||
commit=True, yes=yes)
|
||||
|
||||
changelog = re.search(rf'^## ({major}\.{minor}\.{patch} \(.*?\)\n\n+.+?)\n\n+## ',
|
||||
(Path(src_dir) / 'CHANGELOG.md').read_text("UTF-8"), re.MULTILINE | re.DOTALL)
|
||||
(Path(src_dir) / 'CHANGELOG.md').read_text(), re.MULTILINE | re.DOTALL)
|
||||
if not changelog:
|
||||
raise Error(f'No changelog entry found for version {version}.')
|
||||
changelog = 'Release ' + changelog.group(1)
|
||||
@@ -628,13 +585,6 @@ class Build(Command):
|
||||
help='macOS deployment target version (default: %(default)s).')
|
||||
parser.add_argument('-p', '--platform-target', default=platform.uname().machine,
|
||||
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':
|
||||
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).',
|
||||
@@ -644,10 +594,8 @@ class Build(Command):
|
||||
parser.add_argument('-p', '--platform-target', help='Build target platform (default: %(default)s).',
|
||||
choices=['amd64', 'arm64'], default='amd64')
|
||||
parser.add_argument('--sign', help='Sign binaries prior to packaging.', action='store_true')
|
||||
parser.add_argument('--sign-identity', help='SHA1 fingerprint of the signing certificate.')
|
||||
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('--sign-cert', help='SHA1 fingerprint of the signing certificate (optional).')
|
||||
parser.set_defaults(cmake_generator='Ninja', no_source_tarball=True)
|
||||
|
||||
parser.add_argument('-c', '--cmake-opts', nargs=argparse.REMAINDER,
|
||||
help='Additional CMake options (no other arguments can be specified after this).')
|
||||
@@ -726,15 +674,15 @@ class Build(Command):
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def build_windows(self, version, src_dir, output_dir, *, parallelism, cmake_opts, platform_target,
|
||||
sign, sign_identity, sign_timestamp_url, with_tests, **_):
|
||||
sign, sign_cert, with_tests, **_):
|
||||
# Check for required tools
|
||||
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).')
|
||||
|
||||
# Setup build signing if requested
|
||||
if sign:
|
||||
cmake_opts.append(f'-DWITH_XC_CODESIGN_IDENTITY={sign_identity}')
|
||||
cmake_opts.append(f'-WITH_XC_CODESIGN_TIMESTAMP_URL={sign_timestamp_url}')
|
||||
cmake_opts.append('-DWITH_XC_SIGNINSTALL=ON')
|
||||
cmake_opts.append(f'-DWITH_XC_SIGNINSTALL_CERT={sign_cert}')
|
||||
# Use vcpkg for dependency deployment
|
||||
cmake_opts.append('-DX_VCPKG_APPLOCAL_DEPS_INSTALL=ON')
|
||||
|
||||
@@ -768,20 +716,13 @@ class Build(Command):
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def build_macos(self, version, src_dir, output_dir, *, use_system_deps, parallelism, cmake_opts,
|
||||
macos_target, platform_target, with_tests, sign, sign_identity, notarize, keychain_profile, **_):
|
||||
macos_target, platform_target, with_tests, **_):
|
||||
if not use_system_deps:
|
||||
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_ARCHITECTURES={platform_target}')
|
||||
|
||||
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...')
|
||||
_run(['cmake', *cmake_opts, str(src_dir)], cwd=build_dir, capture_output=False)
|
||||
|
||||
@@ -795,13 +736,9 @@ class Build(Command):
|
||||
_run(['cpack', '-G', 'DragNDrop'], cwd=build_dir, capture_output=False)
|
||||
|
||||
output_file = Path(build_dir) / f'KeePassXC-{version}.dmg'
|
||||
unsigned_suffix = '-unsigned' if not sign else ''
|
||||
output_file.rename(output_dir / f'KeePassXC-{version}-{platform_target}{unsigned_suffix}.dmg')
|
||||
output_file.rename(output_dir / f'KeePassXC-{version}-{platform_target}-unsigned.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
|
||||
def _download_tools_if_not_available(toolname, bin_dir, url, docker_args=None):
|
||||
@@ -828,8 +765,6 @@ class Build(Command):
|
||||
|
||||
if 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:
|
||||
logger.info('Configuring build...')
|
||||
@@ -847,7 +782,7 @@ class Build(Command):
|
||||
_run(['cmake', '--install', '.', '--strip',
|
||||
'--prefix', (app_dir.absolute() / install_prefix.lstrip('/')).as_posix()],
|
||||
cwd=build_dir, capture_output=False, **docker_args)
|
||||
shutil.copytree(app_dir, output_dir / app_dir.name, symlinks=True, dirs_exist_ok=True)
|
||||
shutil.copytree(app_dir, output_dir / app_dir.name, symlinks=True)
|
||||
|
||||
if appimage:
|
||||
self._build_linux_appimage(
|
||||
@@ -888,7 +823,7 @@ class Build(Command):
|
||||
_run(['linuxdeploy', '--plugin=qt', f'--appdir={app_dir}', f'--custom-apprun={app_run}',
|
||||
f'--desktop-file={desktop_file}', f'--icon-file={icon_file}',
|
||||
*[f'--executable={ex}' for ex in executables]],
|
||||
cwd=build_dir, capture_output=False, path=env_path, **docker_args, docker_privileged=True)
|
||||
cwd=build_dir, capture_output=False, path=env_path, **docker_args)
|
||||
|
||||
logger.debug('Running appimagetool...')
|
||||
appimage_name = f'KeePassXC-{version}-{platform_target}.AppImage'
|
||||
@@ -953,37 +888,162 @@ class BuildSrc(Command):
|
||||
tmp_comp.rename(output_file)
|
||||
|
||||
|
||||
class Notarize(Command):
|
||||
"""Notarize a signed macOS DMG app bundle."""
|
||||
class AppSign(Command):
|
||||
"""Sign binaries with code signing certificates on Windows and macOS."""
|
||||
|
||||
@classmethod
|
||||
def setup_arg_parser(cls, parser: argparse.ArgumentParser):
|
||||
parser.add_argument('file', help='Input DMG file(s) to notarize.', nargs='+')
|
||||
parser.add_argument('-p', '--keychain-profile', default='notarization-creds',
|
||||
help='Read Apple credentials for notarization from a keychain (default: %(default)s).')
|
||||
parser.add_argument('file', help='Input file(s) to sign.', nargs='+')
|
||||
parser.add_argument('-i', '--identity', help='Key or identity used for the signature (default: ask).')
|
||||
parser.add_argument('-s', '--src-dir', help='Source directory (default: %(default)s).', default='.')
|
||||
|
||||
def run(self, file, keychain_profile, **_):
|
||||
if sys.platform != 'darwin':
|
||||
raise Error('Unsupported platform.')
|
||||
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).')
|
||||
|
||||
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)
|
||||
def run(self, file, identity, src_dir, **kwargs):
|
||||
for i, f in enumerate(file):
|
||||
f = Path(f)
|
||||
if not f.exists():
|
||||
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
|
||||
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.')
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def notarize_macos(self, file, keychain_profile):
|
||||
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
|
||||
def notarize_macos(self, file, keychain_profile):
|
||||
logger.info('Submitting "%s" for notarization...', file)
|
||||
_run(['xcrun', 'notarytool', 'submit', f'--keychain-profile={keychain_profile}', '--wait',
|
||||
file.as_posix()], cwd=None, capture_output=False)
|
||||
@@ -1199,9 +1259,9 @@ def main():
|
||||
Check.setup_arg_parser(check_parser)
|
||||
check_parser.set_defaults(_cmd=Check)
|
||||
|
||||
merge_parser = subparsers.add_parser('tag', help=Tag.__doc__)
|
||||
Tag.setup_arg_parser(merge_parser)
|
||||
merge_parser.set_defaults(_cmd=Tag)
|
||||
merge_parser = subparsers.add_parser('merge', help=Merge.__doc__)
|
||||
Merge.setup_arg_parser(merge_parser)
|
||||
merge_parser.set_defaults(_cmd=Merge)
|
||||
|
||||
build_parser = subparsers.add_parser('build', help=Build.__doc__)
|
||||
Build.setup_arg_parser(build_parser)
|
||||
@@ -1211,10 +1271,9 @@ def main():
|
||||
BuildSrc.setup_arg_parser(build_src_parser)
|
||||
build_src_parser.set_defaults(_cmd=BuildSrc)
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
notarize_parser = subparsers.add_parser('notarize', help=Notarize.__doc__)
|
||||
Notarize.setup_arg_parser(notarize_parser)
|
||||
notarize_parser.set_defaults(_cmd=Notarize)
|
||||
appsign_parser = subparsers.add_parser('appsign', help=AppSign.__doc__)
|
||||
AppSign.setup_arg_parser(appsign_parser)
|
||||
appsign_parser.set_defaults(_cmd=AppSign)
|
||||
|
||||
gpgsign_parser = subparsers.add_parser('gpgsign', help=GPGSign.__doc__)
|
||||
GPGSign.setup_arg_parser(gpgsign_parser)
|
||||
|
||||
@@ -67,6 +67,10 @@ if(UNIX AND NOT APPLE AND NOT HAIKU)
|
||||
install(FILES linux/${APP_ID}.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
|
||||
endif(UNIX AND NOT APPLE AND NOT HAIKU)
|
||||
|
||||
if(APPLE)
|
||||
install(FILES macosx/keepassxc.icns DESTINATION ${DATA_INSTALL_DIR})
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
install(FILES windows/qt.conf DESTINATION ${BIN_INSTALL_DIR})
|
||||
endif()
|
||||
@@ -81,16 +85,7 @@ add_custom_command(TARGET icons
|
||||
if(APPLE)
|
||||
add_custom_command(TARGET icons
|
||||
POST_BUILD
|
||||
COMMAND xcrun actool share/macosx/keepassxc.icon
|
||||
--compile share/macosx
|
||||
--output-partial-info-plist /dev/null
|
||||
--app-icon keepassxc
|
||||
--include-all-app-icons
|
||||
--enable-on-demand-resources NO
|
||||
--target-device mac
|
||||
--minimum-deployment-target 11.0
|
||||
--platform macosx
|
||||
--output-format human-readable-text
|
||||
COMMAND png2icns macosx/keepassxc.icns icons/application/256x256/apps/keepassxc.png
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
endif()
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,15 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PATH="$(dirname $0)/usr/bin:${PATH}"
|
||||
|
||||
if [ "$1" == "cli" ] || [ "$(basename "$ARGV0")" == "keepassxc-cli" ] || [ "$(basename "$ARGV0")" == "keepassxc-cli.AppImage" ]; then
|
||||
[ "$1" == "cli" ] && shift
|
||||
exec keepassxc-cli "$@"
|
||||
elif [ "$1" == "proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy.AppImage" ]; then
|
||||
elif [ "$1" == "proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy.AppImage" ] \
|
||||
|| [ -v CHROME_WRAPPER ] || [ -v MOZ_LAUNCHED_CHILD ]; then
|
||||
[ "$1" == "proxy" ] && shift
|
||||
exec keepassxc-proxy "$@"
|
||||
elif [ -v CHROME_WRAPPER ] || [ -v MOZ_LAUNCHED_CHILD ] || [ "$2" == "keepassxc-browser@keepassxc.org" ]; then
|
||||
exec keepassxc-proxy "$@"
|
||||
else
|
||||
exec keepassxc "$@"
|
||||
fi
|
||||
|
||||
@@ -46,78 +46,9 @@
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="2.8.0" date="2025-01-01" type="development">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Placeholder for future release notes</li>
|
||||
</ul>
|
||||
</description>
|
||||
</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>
|
||||
<li>Placeholder for future release notes</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
|
||||
Binary file not shown.
@@ -13,11 +13,9 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${PROGNAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_NAME}.icns</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_NAME}</string>
|
||||
<string>keepassxc.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${MACOSX_BUNDLE_IDENTIFIER}</string>
|
||||
<string>org.keepassxc.keepassxc</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128"><defs><style>.cls-1{fill:none;}.cls-2{fill:url(#radial-gradient-2);mix-blend-mode:lighten;opacity:.7;}.cls-3{fill:url(#radial-gradient);}.cls-4{isolation:isolate;}</style><radialGradient id="radial-gradient" cx="315.9556" cy="395.3416" fx="315.9556" fy="395.3416" r="239.1689" gradientTransform="translate(-46.999 -42.8948) scale(.3539 .2026)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2e6b26"/><stop offset="1" stop-color="#6ab536"/></radialGradient><radialGradient id="radial-gradient-2" cx="314.1662" cy="394.0804" fx="314.1662" fy="394.0804" r="46.7089" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6ab536"/><stop offset="1" stop-color="#2e6b26"/></radialGradient></defs><g class="cls-4"><g id="Layer_2"><g id="Icon_macOS"><rect class="cls-1" width="128" height="128"/><path id="Background" class="cls-3" d="M63.9999,24.1601c-21.9679,0-39.84,17.8721-39.84,39.8399s17.8721,39.8399,39.84,39.8399,39.8399-17.8722,39.8399-39.8399-17.8719-39.8399-39.8399-39.8399Z"/><path id="Lighten_Hole" class="cls-2" d="M63.9998,27.4724c-6.5434,0-11.8668,5.3234-11.8668,11.8668s5.3234,11.8668,11.8668,11.8668,11.8668-5.3235,11.8668-11.8668-5.3234-11.8668-11.8668-11.8668Z"/></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128"><defs><style>.cls-1{fill:none;}.cls-2{fill:url(#radial-gradient);opacity:.3;}.cls-2,.cls-3{mix-blend-mode:multiply;}.cls-4{fill:url(#radial-gradient-2);}.cls-5{isolation:isolate;}.cls-3{fill:#0f0f0d;opacity:.2;}</style><radialGradient id="radial-gradient" cx="322.2841" cy="405.7418" fx="322.2841" fy="405.7418" r="68.9894" gradientTransform="translate(-44.2199 -179.8556) scale(.3356 .5572)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000"/><stop offset=".7842" stop-color="#4f4f4f" stop-opacity="0"/></radialGradient><radialGradient id="radial-gradient-2" cx="313.1713" cy="380.0413" fx="313.1713" fy="380.0413" r="159.6501" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f5f5f5"/><stop offset=".5036" stop-color="#f2f2f2"/></radialGradient></defs><g class="cls-5"><g id="Layer_2"><g id="Icon_macOS"><rect class="cls-1" width="128" height="128"/><path id="Key_Double_Shadow" class="cls-2" d="M43.0622,29.9746c-.0019.0137-.0038.027-.0058.0406-.0518.0097-.0526-.0026.0058-.0406ZM73.1463,55.5462l-.551,9.8444,6.5017,7.0832-6.5017,7.0832,4.2977,4.6821-4.2977,4.6821.551,9.764h-18.2929v-43.139c-7.2731-3.7217-12.232-11.7653-12.232-21.0094,0-1.5471.2166-2.9765.435-4.5215.789-.1487,13.5481-5.4962,13.5481-5.4962-.1059.578,14.8965.578,14.7906,0,0,0,12.3269,5.3041,13.5637,5.5642.3128,1.5189.4195,2.9293.4195,4.4534,0,9.2442-5.0691,17.2878-12.232,21.0095ZM84.9375,29.9746c.0077.0365.0138.0722.0213.1086.1354.0285.14-.0025-.0213-.1086Z"/><path id="Key_Drop_Shadow" class="cls-3" d="M42.6543,90.0228c-7.0698-6.1486-11.5457-15.2121-11.5457-25.3548,0-11.8019,6.172-22.696,16.3377-28.7786-.1815,1.1802-.3631,2.2696-.3631,3.4498,0,6.9903,4.0844,13.0729,10.0749,15.8872v31.8652l7.5335,7.5351,7.5335-7.5351-.4538-6.6272,3.5398-3.5406-3.5398-3.5406,5.3551-5.3563-5.3551-5.3563.4538-7.4443c5.8997-2.8143,10.0749-8.8968,10.0749-15.8872,0-1.1802-.0908-2.2696-.3631-3.4498,3.6888,2.227,6.8516,5.1011,9.3871,8.4266-6.1315-8.4206-16.0924-13.8991-27.3404-13.8991-18.6365,0-33.7443,15.0357-33.7443,33.5832,0,10.495,4.8383,19.8644,12.4149,26.0227ZM62.241,84.5497h-2.5414v-25.1472h2.5414v25.1472ZM58.6104,31.7133c1.9968-.3631,4.0844-.5447,6.0813-.5447,2.0876-.0908,4.0844.1815,6.0813.5447.0908.4539.1815.9078.1815,1.3617,0,3.4498-2.8137,6.2641-6.2628,6.2641s-6.2628-2.8143-6.2628-6.2641c0-.4539.0908-.9078.1815-1.3617Z"/><path id="Key" class="cls-4" d="M63.9999,24.1601c-21.9582,0-39.8225,17.8721-39.8225,39.8399s17.8643,39.8399,39.8225,39.8399,39.8223-17.8723,39.8223-39.8399-17.8641-39.8399-39.8223-39.8399ZM57.9186,31.0088c1.9968-.3631,4.0844-.5447,6.0813-.5447,2.0876-.0908,4.0844.1815,6.0813.5447.0908.4539.1815.9078.1815,1.3617,0,3.4498-2.8137,6.2641-6.2628,6.2641s-6.2628-2.8143-6.2628-6.2641c0-.4539.0908-.9078.1815-1.3617ZM61.5492,58.6979v25.1472h-2.5414v-25.1472h2.5414ZM63.9999,97.5535c-18.5161,0-33.5831-14.9794-33.5831-33.5901,0-11.8019,6.172-22.696,16.3377-28.7786-.1815,1.1802-.3631,2.2696-.3631,3.4498,0,6.9903,4.0844,13.0729,10.0749,15.8872v31.8652l7.5335,7.5351,7.5335-7.5351-.4538-6.6272,3.5398-3.5406-3.5398-3.5406,5.3551-5.3563-5.3551-5.3563.4538-7.4443c5.8997-2.8143,10.0749-8.8968,10.0749-15.8872,0-1.1802-.0908-2.2696-.3631-3.4498,10.0749,6.0826,16.247,16.9766,16.3377,28.7786,0,18.5199-14.9763,33.5901-33.5831,33.5901Z"/></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128"><defs><style>.cls-1{fill:none;}.cls-2{fill:url(#radial-gradient-2);opacity:.44;}.cls-3{fill:#0f0f0d;opacity:.35;}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:url(#linear-gradient);}.cls-6{isolation:isolate;}.cls-7{fill:url(#radial-gradient);mix-blend-mode:lighten;opacity:.32;}.cls-8{opacity:.6;}.cls-9{fill:rgba(15,15,13,.35);}</style><linearGradient id="linear-gradient" x1="63.9998" y1="20.4513" x2="63.9998" y2="107.5488" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000"/><stop offset="1" stop-color="#000" stop-opacity=".2"/></linearGradient><linearGradient id="linear-gradient-2" x1="63.9998" y1="15.9186" x2="63.9998" y2="102.9186" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#414141"/><stop offset=".196" stop-color="#3e3e3e"/><stop offset="1" stop-color="#3a3a3a"/></linearGradient><radialGradient id="radial-gradient" cx="305.8904" cy="449.6754" fx="305.8904" fy="449.6754" r="148.0242" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset=".6733" stop-color="#fcfcfc" stop-opacity="0"/><stop offset="1" stop-color="#fcfcfc"/></radialGradient><radialGradient id="radial-gradient-2" cx="311.8517" cy="469.1737" fx="311.8517" fy="469.1737" r="123.1352" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset=".1088" stop-color="#0f0f0d" stop-opacity="0"/><stop offset=".8856" stop-color="#414141" stop-opacity="0"/><stop offset=".9339" stop-color="#1e1e1d" stop-opacity=".6841"/><stop offset=".9874" stop-color="#0f0f0d"/></radialGradient></defs><g class="cls-6"><g id="Layer_2"><g id="Icon_macOS"><rect class="cls-1" width="128" height="128"/><g id="Countour"><g class="cls-8"><path class="cls-5" d="M63.9999,20.8039c23.7037,0,42.9881,19.3777,42.9881,43.1962s-19.2844,43.1962-42.9881,43.1962-42.9882-19.3777-42.9882-43.1962S40.2961,20.8039,63.9999,20.8039M63.9999,20.4513c-23.8983,0-43.3408,19.5358-43.3408,43.5487s19.4425,43.5487,43.3408,43.5487,43.3406-19.536,43.3406-43.5487-19.4423-43.5487-43.3406-43.5487h0Z"/></g></g><path id="Rim" class="cls-4" d="M63.9999,107.5c-23.9861,0-43.5001-19.5141-43.5001-43.5S40.0138,20.5,63.9999,20.5s43.4999,19.5139,43.4999,43.5-19.5138,43.5-43.4999,43.5ZM63.9999,24.1601c-21.9582,0-39.8225,17.8721-39.8225,39.8399s17.8643,39.8399,39.8225,39.8399,39.8223-17.8723,39.8223-39.8399-17.8641-39.8399-39.8223-39.8399Z"/><path id="Bottom_Light" class="cls-7" d="M63.9999,20.517c-23.9767,0-43.4831,19.5063-43.4831,43.483s19.5064,43.483,43.4831,43.483,43.4829-19.5065,43.4829-43.483-19.5062-43.483-43.4829-43.483Z"/><path id="Inner_Shadow" class="cls-2" d="M63.9999,107.5c-23.9861,0-43.5001-19.5141-43.5001-43.5S40.0138,20.5,63.9999,20.5s43.4999,19.5139,43.4999,43.5-19.5138,43.5-43.4999,43.5ZM63.9999,24.1601c-21.9582,0-39.8225,17.8721-39.8225,39.8399s17.8643,39.8399,39.8225,39.8399,39.8223-17.8723,39.8223-39.8399-17.8641-39.8399-39.8223-39.8399Z"/><path id="Bottom_Drop_Shadow" class="cls-9" d="M63.9999,24.3931c-22.1274,0-40.1292,18.0018-40.1292,40.1291s18.0018,40.1291,40.1292,40.1291,40.129-18.002,40.129-40.1291-18.0017-40.1291-40.129-40.1291Z"/><path id="Top_Drop_Shadow" class="cls-3" d="M63.9999,107.5c-23.9861,0-43.5001-19.5141-43.5001-43.5S40.0138,20.5,63.9999,20.5s43.4999,19.5139,43.4999,43.5-19.5138,43.5-43.4999,43.5ZM63.9999,21.431c-23.986,0-43.5001,19.2544-43.5001,42.9213s19.5141,42.9213,43.5001,42.9213,43.4999-19.2546,43.4999-42.9213-19.5138-42.9213-43.4999-42.9213Z"/></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : "automatic"
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "automatic"
|
||||
}
|
||||
],
|
||||
"groups" : [
|
||||
{
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"glass" : true,
|
||||
"image-name" : "macos-comp-key.svg",
|
||||
"name" : "key",
|
||||
"position" : {
|
||||
"scale" : 9.5,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"glass" : true,
|
||||
"image-name" : "macos-comp-bg-green.svg",
|
||||
"name" : "bg-green",
|
||||
"position" : {
|
||||
"scale" : 9.5,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"blur-material" : 0.5,
|
||||
"hidden" : false,
|
||||
"layers" : [
|
||||
{
|
||||
"glass" : true,
|
||||
"image-name" : "macos-comp-rim.svg",
|
||||
"name" : "rim",
|
||||
"position" : {
|
||||
"scale" : 9.5,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.7
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.2
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
@@ -657,10 +657,6 @@
|
||||
<source>Convenience</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable database quick unlock (Touch ID / Windows Hello)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Lock databases when session is locked or lid is closed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -705,6 +701,14 @@
|
||||
<source>Hide notes in the entry preview panel</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable database quick unlock by default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Remember quick unlock after database is closed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>AttachmentWidget</name>
|
||||
@@ -1654,10 +1658,6 @@ Backup database located at %2</source>
|
||||
<source>Unlock Database</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Unlock</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1735,10 +1735,6 @@ To prevent this error from appearing, you must go to "Database Settings / S
|
||||
<source>Cannot use database file as key file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>authenticate to access the database</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to authenticate with Quick Unlock: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1791,6 +1787,14 @@ Are you sure you want to continue with this file?.</source>
|
||||
<source><a href="#" style="text-decoration: underline">I have a key file</a></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Reset</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Close Database</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hardware keys found, but no slots are configured.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1799,6 +1803,10 @@ Are you sure you want to continue with this file?.</source>
|
||||
<source>Press ESC again to close this database</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Quick Unlock</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DatabaseSettingWidgetMetaData</name>
|
||||
@@ -3735,14 +3743,6 @@ Supported extensions are: %1.</source>
|
||||
<source>Select import/export file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Maintain group structure with shared database</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Keep Group Structure</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EditGroupWidgetMain</name>
|
||||
@@ -9134,46 +9134,10 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||
<source>Passkeys</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>AES initialization failed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>AES encrypt failed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to store in Linux Keyring</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Polkit returned an error: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Could not locate key in keyring</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Could not read key in keyring</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>AES decrypt failed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No Polkit authentication agent was available</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Polkit authorization failed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No Quick Unlock provider is available</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to init KeePassXC crypto.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -9182,10 +9146,6 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||
<source>Failed to encrypt key data.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to get Windows Hello credential.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to decrypt key data.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -9349,7 +9309,35 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Format to use when exporting. Available choices are 'xml', 'csv' or 'html'. Defaults to 'xml'.</source>
|
||||
<source>Quick Unlock Pin Entry</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pin setup was canceled. Quick unlock has not been enabled.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to get credentials for quick unlock.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enter quick unlock pin (%1 of %2 attempts):</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pin entry was canceled.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No Polkit authentication agent was available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Polkit authorization failed.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Windows Hello setup was canceled or failed. Quick unlock has not been enabled.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -9433,6 +9421,34 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||
<source>Confirm Replace Entry References</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Format to use when exporting. Available choices are 'xml', 'csv' or 'html'. Defaults to 'xml'.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enter a %1–%2 digit pin to use for quick unlock:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to derive key using Argon2</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Too many pin attempts.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No key is stored for this database.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to obtain session key.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to retrieve Windows Hello credential.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QtIOCompressor</name>
|
||||
@@ -10598,7 +10614,11 @@ Example: JBSWY3DPEHPK3PXP</source>
|
||||
<context>
|
||||
<name>YubiKey</name>
|
||||
<message>
|
||||
<source>Could not find hardware key with serial number %1. Please connect it to continue.</source>
|
||||
<source>General: </source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Could not find interface for hardware key with serial number %1. Please connect it to continue.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
@@ -10659,6 +10679,10 @@ Example: JBSWY3DPEHPK3PXP</source>
|
||||
</context>
|
||||
<context>
|
||||
<name>YubiKeyInterfacePCSC</name>
|
||||
<message>
|
||||
<source>Could not find or access hardware key with serial number %1. Please present it to continue. </source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hardware key is locked or timed out. Unlock or re-present it to continue.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -121,8 +121,6 @@
|
||||
<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="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">
|
||||
<ComponentRef Id="ApplicationShortcuts" />
|
||||
|
||||
@@ -217,6 +217,7 @@ set(gui_SOURCES
|
||||
gui/wizard/NewDatabaseWizardPageEncryption.cpp
|
||||
gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp
|
||||
quickunlock/QuickUnlockInterface.cpp
|
||||
quickunlock/PinUnlock.cpp
|
||||
../share/icons/icons.qrc
|
||||
../share/wizard/wizard.qrc)
|
||||
|
||||
@@ -227,40 +228,41 @@ if(APPLE)
|
||||
gui/osutils/macutils/ScreenLockListenerMac.cpp
|
||||
gui/osutils/macutils/AppKitImpl.mm
|
||||
gui/osutils/macutils/AppKit.h
|
||||
quickunlock/TouchID.mm)
|
||||
|
||||
# TODO: Remove -Wno-error once deprecation warnings have been resolved.
|
||||
set_source_files_properties(quickunlock/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast")
|
||||
quickunlock/TouchID.cpp)
|
||||
endif()
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
list(APPEND gui_SOURCES
|
||||
gui/osutils/nixutils/ScreenLockListenerDBus.cpp
|
||||
gui/osutils/nixutils/NixUtils.cpp)
|
||||
if("${CMAKE_SYSTEM}" MATCHES "Linux")
|
||||
list(APPEND core_SOURCES
|
||||
quickunlock/Polkit.cpp
|
||||
quickunlock/PolkitDbusTypes.cpp)
|
||||
endif()
|
||||
if(WITH_XC_X11)
|
||||
list(APPEND gui_SOURCES
|
||||
gui/osutils/nixutils/X11Funcs.cpp)
|
||||
endif()
|
||||
|
||||
# Polkit is only available on Linux systems
|
||||
if("${CMAKE_SYSTEM}" MATCHES "Linux")
|
||||
list(APPEND gui_SOURCES
|
||||
quickunlock/Polkit.cpp
|
||||
quickunlock/PolkitDbusTypes.cpp)
|
||||
|
||||
set_source_files_properties(
|
||||
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
|
||||
PROPERTIES
|
||||
INCLUDE "quickunlock/PolkitDbusTypes.h"
|
||||
)
|
||||
qt5_add_dbus_interface(gui_SOURCES
|
||||
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
|
||||
polkit_dbus
|
||||
)
|
||||
endif()
|
||||
|
||||
# dbus support
|
||||
qt5_add_dbus_adaptor(gui_SOURCES
|
||||
gui/org.keepassxc.KeePassXC.MainWindow.xml
|
||||
gui/MainWindow.h
|
||||
MainWindow)
|
||||
|
||||
set_source_files_properties(
|
||||
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
|
||||
PROPERTIES
|
||||
INCLUDE "quickunlock/PolkitDbusTypes.h"
|
||||
)
|
||||
qt5_add_dbus_interface(core_SOURCES
|
||||
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
|
||||
polkit_dbus
|
||||
)
|
||||
|
||||
find_library(KEYUTILS_LIBRARIES NAMES keyutils)
|
||||
if(NOT KEYUTILS_LIBRARIES)
|
||||
message(FATAL_ERROR "Could not find libkeyutils")
|
||||
@@ -271,7 +273,7 @@ if(WIN32)
|
||||
list(APPEND gui_SOURCES
|
||||
gui/osutils/winutils/ScreenLockListenerWin.cpp
|
||||
gui/osutils/winutils/WinUtils.cpp)
|
||||
if (WINSDK)
|
||||
if (MSVC)
|
||||
list(APPEND gui_SOURCES quickunlock/WindowsHello.cpp)
|
||||
endif()
|
||||
endif()
|
||||
@@ -415,18 +417,13 @@ if(UNIX AND NOT APPLE)
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(keepassxc_gui Wtsapi32.lib Ws2_32.lib)
|
||||
if (WINSDK)
|
||||
if (MSVC)
|
||||
target_link_libraries(keepassxc_gui WindowsApp.lib)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Main Executable Definition
|
||||
add_executable(${PROGNAME} main.cpp)
|
||||
target_link_libraries(${PROGNAME} keepassxc_gui)
|
||||
set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(${PROGNAME} PROPERTIES WIN32_EXECUTABLE ON)
|
||||
include(GenerateProductVersion)
|
||||
generate_product_version(
|
||||
WIN32_ResourceFiles
|
||||
@@ -437,43 +434,26 @@ if(WIN32)
|
||||
VERSION_PATCH ${KEEPASSXC_VERSION_PATCH}
|
||||
)
|
||||
list(APPEND WIN32_ResourceFiles "${CMAKE_SOURCE_DIR}/share/windows/icon.rc")
|
||||
target_sources(${PROGNAME} PUBLIC ${WIN32_ResourceFiles})
|
||||
endif()
|
||||
|
||||
elseif(APPLE AND WITH_APP_BUNDLE)
|
||||
set(MACOSX_BUNDLE_IDENTIFIER org.keepassxc.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)
|
||||
install(FILES "${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile" DESTINATION ${BUNDLE_INSTALL_DIR})
|
||||
set(MACOSX_BUNDLE_RESOURCE_FILES
|
||||
"${CMAKE_SOURCE_DIR}/share/macosx/Assets.car"
|
||||
"${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.icns"
|
||||
)
|
||||
add_executable(${PROGNAME} WIN32 main.cpp ${WIN32_ResourceFiles})
|
||||
target_link_libraries(${PROGNAME} keepassxc_gui)
|
||||
set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
|
||||
|
||||
# macOS App Bundle
|
||||
if(APPLE AND WITH_APP_BUNDLE)
|
||||
install(FILES ${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile DESTINATION ${BUNDLE_INSTALL_DIR})
|
||||
configure_file(${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
|
||||
set_target_properties(${PROGNAME} PROPERTIES
|
||||
MACOSX_BUNDLE ON
|
||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist"
|
||||
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${MACOSX_BUNDLE_APPLE_ENTITLEMENTS}"
|
||||
RESOURCE "${MACOSX_BUNDLE_RESOURCE_FILES}"
|
||||
)
|
||||
target_sources(${PROGNAME} PUBLIC ${MACOSX_BUNDLE_RESOURCE_FILES})
|
||||
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist
|
||||
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements")
|
||||
|
||||
if(QT_MAC_USE_COCOA AND EXISTS "${QT_LIBRARY_DIR}/Resources/qt_menu.nib")
|
||||
install(DIRECTORY "${QT_LIBRARY_DIR}/Resources/qt_menu.nib"
|
||||
DESTINATION "${DATA_INSTALL_DIR}")
|
||||
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_DMG_FORMAT "UDBZ")
|
||||
set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/share/macosx/DS_Store.in")
|
||||
@@ -505,9 +485,8 @@ if(WIN32)
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/INSTALLER_LICENSE.txt")
|
||||
|
||||
# Prepare post-install script and set to run prior to building cpack installers
|
||||
configure_file("${CMAKE_SOURCE_DIR}/cmake/WindowsCodesign.cmake.in" "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake" @ONLY)
|
||||
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake")
|
||||
set(CPACK_POST_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake")
|
||||
configure_file("${CMAKE_SOURCE_DIR}/cmake/WindowsPostInstall.cmake.in" "${CMAKE_BINARY_DIR}/WindowsPostInstall.cmake" @ONLY)
|
||||
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsPostInstall.cmake")
|
||||
|
||||
string(REGEX REPLACE "-.*$" "" KEEPASSXC_VERSION_CLEAN ${KEEPASSXC_VERSION})
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
#include "core/Global.h"
|
||||
#include "core/Resources.h"
|
||||
#include "core/Tools.h"
|
||||
#include "core/Totp.h"
|
||||
#include "gui/MainWindow.h"
|
||||
#include "gui/MessageBox.h"
|
||||
#include "gui/osutils/OSUtils.h"
|
||||
@@ -312,6 +311,9 @@ void AutoType::executeAutoTypeActions(const Entry* entry,
|
||||
// Restore executor 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
|
||||
if (window == 0) {
|
||||
window = m_plugin->activeWindow();
|
||||
@@ -343,8 +345,7 @@ void AutoType::executeAutoTypeActions(const Entry* entry,
|
||||
break;
|
||||
}
|
||||
|
||||
// Retry wait delay
|
||||
Tools::wait(100);
|
||||
Tools::wait(delay);
|
||||
}
|
||||
|
||||
// Last action failed to complete, cancel the rest of the sequence
|
||||
@@ -545,14 +546,10 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
|
||||
const int maxTypeDelay = 500;
|
||||
const int maxWaitDelay = 10000;
|
||||
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;
|
||||
actions << QSharedPointer<AutoTypeBegin>::create();
|
||||
actions << QSharedPointer<AutoTypeDelay>::create(currentTypingDelay, true);
|
||||
actions << QSharedPointer<AutoTypeDelay>::create(cumulativeDelay);
|
||||
actions << QSharedPointer<AutoTypeDelay>::create(qMax(0, config()->get(Config::AutoTypeDelay).toInt()), true);
|
||||
|
||||
// Replace escaped braces with a template for easier regex
|
||||
QString sequence = entrySequence;
|
||||
@@ -568,7 +565,7 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
|
||||
// Group 1 = modifier key (opt)
|
||||
// Group 2 = full placeholder
|
||||
// Group 3 = inner placeholder (allows nested placeholders)
|
||||
// Group 4 = repeat / delay time (opt)
|
||||
// Group 4 = repeat (opt)
|
||||
// Group 5 = character
|
||||
QRegularExpression regex("([+%^#]*)(?:({((?>[^{}]+?|(?2))+?)(?:\\s+(\\d+))?})|(.))");
|
||||
auto results = regex.globalMatch(sequence);
|
||||
@@ -630,23 +627,19 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
|
||||
}
|
||||
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, delay, maxTypeDelay), true);
|
||||
} else if (placeholder == "delay") {
|
||||
// Mid typing delay (wait), repeat represents the desired delay in milliseconds
|
||||
// Mid typing delay (wait)
|
||||
if (repeat > maxWaitDelay) {
|
||||
error = tr("Very long delay detected, max is %1: %2").arg(maxWaitDelay).arg(fullPlaceholder);
|
||||
return {};
|
||||
}
|
||||
cumulativeDelay += repeat;
|
||||
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, repeat, maxWaitDelay));
|
||||
} else if (placeholder == "clearfield") {
|
||||
// Platform-specific field clearing
|
||||
actions << QSharedPointer<AutoTypeClearField>::create();
|
||||
} else if (placeholder == "totp") {
|
||||
if (entry->hasValidTotp()) {
|
||||
// Calculate TOTP at the time of typing including delays
|
||||
bool isValid = false;
|
||||
auto time =
|
||||
Clock::currentSecondsSinceEpoch() + (cumulativeDelay + currentTypingDelay * actions.count()) / 1000;
|
||||
auto totp = Totp::generateTotp(entry->totpSettings(), &isValid, time);
|
||||
// Entry totp (requires special handling)
|
||||
QString totp = entry->totp();
|
||||
for (const auto& ch : totp) {
|
||||
actions << QSharedPointer<AutoTypeKey>::create(ch);
|
||||
}
|
||||
|
||||
@@ -175,7 +175,6 @@ namespace Bootstrap
|
||||
|
||||
if (!CreateWellKnownSid(WinCreatorOwnerRightsSid, nullptr, pOwnerRightsSid, &pOwnerRightsSidSize)) {
|
||||
auto error = GetLastError();
|
||||
Q_UNUSED(error)
|
||||
goto Cleanup;
|
||||
}
|
||||
|
||||
|
||||
@@ -139,8 +139,8 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
||||
{Config::Security_ClearSearch, {QS("Security/ClearSearch"), Roaming, false}},
|
||||
{Config::Security_ClearSearchTimeout, {QS("Security/ClearSearchTimeout"), Roaming, 5}},
|
||||
{Config::Security_HideNotes, {QS("Security/Security_HideNotes"), Roaming, false}},
|
||||
{Config::Security_LockDatabaseIdle, {QS("Security/LockDatabaseIdle"), Roaming, true}},
|
||||
{Config::Security_LockDatabaseIdleSeconds, {QS("Security/LockDatabaseIdleSeconds"), Roaming, 900}},
|
||||
{Config::Security_LockDatabaseIdle, {QS("Security/LockDatabaseIdle"), Roaming, false}},
|
||||
{Config::Security_LockDatabaseIdleSeconds, {QS("Security/LockDatabaseIdleSeconds"), Roaming, 240}},
|
||||
{Config::Security_LockDatabaseMinimize, {QS("Security/LockDatabaseMinimize"), Roaming, false}},
|
||||
{Config::Security_LockDatabaseScreenLock, {QS("Security/LockDatabaseScreenLock"), Roaming, true}},
|
||||
{Config::Security_LockDatabaseOnUserSwitch, {QS("Security/LockDatabaseOnUserSwitch"), Roaming, true}},
|
||||
@@ -155,7 +155,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
||||
{Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}},
|
||||
{Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}},
|
||||
{Config::Security_QuickUnlock, {QS("Security/QuickUnlock"), Local, true}},
|
||||
{Config::Security_DatabasePasswordMinimumQuality, {QS("Security/DatabasePasswordMinimumQuality"), Local, 0}},
|
||||
{Config::Security_QuickUnlockRemember, {QS("Security/QuickUnlockRemember"), Local, true}},
|
||||
|
||||
// Browser
|
||||
{Config::Browser_Enabled, {QS("Browser/Enabled"), Roaming, false}},
|
||||
|
||||
@@ -136,6 +136,7 @@ public:
|
||||
Security_NoConfirmMoveEntryToRecycleBin,
|
||||
Security_EnableCopyOnDoubleClick,
|
||||
Security_QuickUnlock,
|
||||
Security_QuickUnlockRemember,
|
||||
Security_DatabasePasswordMinimumQuality,
|
||||
|
||||
Browser_Enabled,
|
||||
|
||||
@@ -1138,15 +1138,6 @@ QString Entry::resolveMultiplePlaceholdersRecursive(const QString& str, int maxD
|
||||
return str;
|
||||
}
|
||||
|
||||
// Short circuit if we have escaped the placeholder brackets
|
||||
if (str.startsWith("\\{") && str.endsWith("\\}")) {
|
||||
// Replace the escaped brackets with actuals and move on
|
||||
auto ret = str;
|
||||
ret.replace(0, 2, "{");
|
||||
ret.replace(ret.size() - 2, 2, "}");
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString result;
|
||||
auto matches = placeholderRegEx.globalMatch(str);
|
||||
int capEnd = 0;
|
||||
|
||||
@@ -449,11 +449,6 @@ namespace Tools
|
||||
return userName;
|
||||
}
|
||||
|
||||
QString escapeAccelerators(QString string)
|
||||
{
|
||||
return string.replace("&", "&&");
|
||||
}
|
||||
|
||||
QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties)
|
||||
{
|
||||
QVariantMap result;
|
||||
@@ -515,7 +510,7 @@ namespace Tools
|
||||
"application/protobuf",
|
||||
"application/x-zerosize"};
|
||||
const static QStringList HtmlFormats = {"text/html"};
|
||||
const static QStringList MarkdownFormats = {"text/markdown", "text/x-web-markdown"};
|
||||
const static QStringList MarkdownFormats = {"text/markdown"};
|
||||
const static QStringList ImageFormats = {"image/"};
|
||||
|
||||
static auto isCompatible = [](const QString& format, const QStringList& list) {
|
||||
|
||||
@@ -49,7 +49,6 @@ namespace Tools
|
||||
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment());
|
||||
QString cleanFilename(QString filename);
|
||||
QString cleanUsername();
|
||||
QString escapeAccelerators(QString string);
|
||||
|
||||
template <class T> QSet<T> asSet(const QList<T>& a)
|
||||
{
|
||||
|
||||
@@ -163,6 +163,13 @@ QVariantMap Argon2Kdf::writeParameters()
|
||||
|
||||
bool Argon2Kdf::transform(const QByteArray& raw, QByteArray& result) const
|
||||
{
|
||||
// This is a programming error and will result in broken encryption
|
||||
Q_ASSERT(*raw != *result);
|
||||
if (*raw == *result) {
|
||||
qWarning("Argon2Kdf: Input and output buffers must not be the same.");
|
||||
return false;
|
||||
}
|
||||
|
||||
result.clear();
|
||||
result.resize(32);
|
||||
// Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length
|
||||
|
||||
@@ -44,43 +44,29 @@ static const QString aboutContributors = R"(
|
||||
<li>Sergey Vilgelm</li>
|
||||
<li>Victor Engmark</li>
|
||||
<li>NarwhalOfAges</li>
|
||||
<li>No Name</li>
|
||||
<li>SG</li>
|
||||
<li>Riley Moses</li>
|
||||
<li>Esteban Martinez</li>
|
||||
<li>Marc Morocutti</li>
|
||||
<li>Zivix</li>
|
||||
<li>Benedikt Heine</li>
|
||||
<li>Hugo Locurcio</li>
|
||||
<li>William Petrides</li>
|
||||
<li>Gunar Gessner</li>
|
||||
<li>Christian Wittenhorst</li>
|
||||
<li>Matt Cardarelli</li>
|
||||
<li>Paul Ammann</li>
|
||||
<li>Steve Isom</li>
|
||||
<li>GodSpell</li>
|
||||
<li>Lionel Laské</li>
|
||||
<li>Daniel Epp</li>
|
||||
<li>Oleksii Aleksieiev</li>
|
||||
<li>Julian Stier</li>
|
||||
<li>Ruben Schade</li>
|
||||
<li>Bernhard</li>
|
||||
<li>Wojciech Kozlowski</li>
|
||||
<li>Caleb Currie</li>
|
||||
<li>Morgan Courbet</li>
|
||||
<li>Kyle Kneitinger</li>
|
||||
<li>Chris Sohns</li>
|
||||
<li>Shmavon Gazanchyan</li>
|
||||
<li>xjdwc</li>
|
||||
<li>Riley Moses</li>
|
||||
<li>Igor Zinovik</li>
|
||||
<li>Jeff</li>
|
||||
<li>Esteban Martinez</li>
|
||||
<li>Max Andersen</li>
|
||||
<li>Zivix</li>
|
||||
<li>Marc Morocutti</li>
|
||||
<li>super scampy</li>
|
||||
<li>Hugo Locurcio</li>
|
||||
<li>Benedikt Heine</li>
|
||||
<li>Mischa Peters</li>
|
||||
<li>Rainer-Maria Fritsch</li>
|
||||
<li>Micha Ober</li>
|
||||
<li>Ivan Gromov</li>
|
||||
<li>William Petrides</li>
|
||||
<li>Joshua Go</li>
|
||||
<li>Gunar Gessner</li>
|
||||
<li>pancakeplant</li>
|
||||
<li>Hans-Joachim Forker</li>
|
||||
<li>Nicolas Vandemaele</li>
|
||||
@@ -89,66 +75,30 @@ static const QString aboutContributors = R"(
|
||||
<li>Mike</li>
|
||||
<li>Thomas Renz</li>
|
||||
<li>Toby Cline</li>
|
||||
<li>Christian Wittenhorst</li>
|
||||
<li>Paul Ammann</li>
|
||||
<li>Matt Cardarelli</li>
|
||||
<li>Steve Isom</li>
|
||||
<li>Emre Dessoi</li>
|
||||
<li>Wojciech Kozlowski</li>
|
||||
<li>Michael Babnick</li>
|
||||
<li>kernellinux</li>
|
||||
<li>Patrick Evans</li>
|
||||
<li>Marco</li>
|
||||
<li>GodSpell</li>
|
||||
<li>Jeremy Rubin</li>
|
||||
<li>Korbi</li>
|
||||
<li>andreas</li>
|
||||
<li>Tyche's tidings</li>
|
||||
<li>Daniel Kuebler</li>
|
||||
<li>Brandon Corujo</li>
|
||||
<li>AheroX</li>
|
||||
<li>Alexandre G</li>
|
||||
<li>AshinaGa</li>
|
||||
<li>BYTEBOLT</li>
|
||||
<li>CEH</li>
|
||||
<li>Chris Stone</li>
|
||||
<li>Christof Böckler</li>
|
||||
<li>Claude</li>
|
||||
<li>CzLer</li>
|
||||
<li>Daniel Burridge</li>
|
||||
<li>dark</li>
|
||||
<li>Dave G</li>
|
||||
<li>David Bowers</li>
|
||||
<li>dickv</li>
|
||||
<li>fp4</li>
|
||||
<li>Huser IT Solutions GmbH</li>
|
||||
<li>irf</li>
|
||||
<li>Isaiah Rahmany</li>
|
||||
<li>JackNYC</li>
|
||||
<li>Jacob Emmert-Aronson</li>
|
||||
<li>John Donadeo</li>
|
||||
<li>Kosta Medinsky</li>
|
||||
<li>leinad987</li>
|
||||
<li>Lux</li>
|
||||
<li>marek</li>
|
||||
<li>mattlongname</li>
|
||||
<li>mattock</li>
|
||||
<li>Max Christian Pohle</li>
|
||||
<li>nta/norma</li>
|
||||
<li>picatsv</li>
|
||||
<li>proto</li>
|
||||
<li>Raymond Lau</li>
|
||||
<li>Waido</li>
|
||||
<li>Weinmann Willy</li>
|
||||
<li>WildMage</li>
|
||||
</ul>
|
||||
<h3>VIP GitHub Sponsors:</h3>
|
||||
<ul>
|
||||
<li>mercedes-benz</li>
|
||||
<li>tiangolo</li>
|
||||
<li>mrniko</li>
|
||||
<li>rszamszur</li>
|
||||
</ul>
|
||||
<h3>Notable Code Contributions:</h3>
|
||||
<ul>
|
||||
<li>droidmonkey</li>
|
||||
<li>phoerious</li>
|
||||
<li>varjolintu (Browser Integration)</li>
|
||||
<li>louib (CLI)</li>
|
||||
<li>varjolintu (Browser Integration)</li>
|
||||
<li>hifi (SSH Agent)</li>
|
||||
<li>xvallspl (Tags)</li>
|
||||
<li>Aetf (FdoSecrets Storage Server)</li>
|
||||
@@ -159,8 +109,6 @@ static const QString aboutContributors = R"(
|
||||
<li>brainplot (many improvements)</li>
|
||||
<li>kneitinger (many improvements)</li>
|
||||
<li>frostasm (many improvements)</li>
|
||||
<li>libklein (many improvements)</li>
|
||||
<li>w15eacre (many improvements)</li>
|
||||
<li>fonic (Entry Table View)</li>
|
||||
<li>kylemanna (YubiKey)</li>
|
||||
<li>c4rlo (Offline HIBP Checker)</li>
|
||||
@@ -173,145 +121,179 @@ static const QString aboutContributors = R"(
|
||||
</ul>
|
||||
<h3>Patreon Supporters:</h3>
|
||||
<ul>
|
||||
<li>Richard Ames</li>
|
||||
<li>Bernhard</li>
|
||||
<li>Christian Rasmussen</li>
|
||||
<li>Nuutti Toivola</li>
|
||||
<li>Lionel Laské</li>
|
||||
<li>Tyler Gass</li>
|
||||
<li>NZSmartie</li>
|
||||
<li>Darren</li>
|
||||
<li>Brad</li>
|
||||
<li>Oleksii Aleksieiev</li>
|
||||
<li>Julian Stier</li>
|
||||
<li>Daniel Epp</li>
|
||||
<li>Ruben Schade</li>
|
||||
<li>William Komanetsky</li>
|
||||
<li>Niels Ganser</li>
|
||||
<li>judd</li>
|
||||
<li>Tarek Sherif</li>
|
||||
<li>Eugene</li>
|
||||
<li>CYB3RL4MBD4</li>
|
||||
<li>Alexanderjb</li>
|
||||
<li>Justin Carroll</li>
|
||||
<li>Bart Libert</li>
|
||||
<li>Shintaro Matsushima</li>
|
||||
<li>Thammachart Chinvarapon</li>
|
||||
<li>Gernot Premper</li>
|
||||
<li>SLmanDR</li>
|
||||
<li>Paul Ellenbogen</li>
|
||||
<li>John C</li>
|
||||
<li>Markus</li>
|
||||
<li>Crimson Idol</li>
|
||||
<li>Steven</li>
|
||||
<li>Ellie</li>
|
||||
<li>Anthony Avina</li>
|
||||
<li>PlushElderGod</li>
|
||||
<li>Markus Wochnik</li>
|
||||
<li>Clark Henry</li>
|
||||
<li>zapscribe</li>
|
||||
<li>Salt Rock Lamp</li>
|
||||
<li>Steven Crowley</li>
|
||||
<li>Ralph Azucena</li>
|
||||
<li>Guruprasad Kulkarni</li>
|
||||
<li>jose</li>
|
||||
<li>Michael Gulick</li>
|
||||
<li>J Doty</li>
|
||||
<li>Synchro11</li>
|
||||
<li>Michael Soares</li>
|
||||
<li>Johannes Felko</li>
|
||||
<li>Ellie</li>
|
||||
<li>David Walluscheck</li>
|
||||
<li>Anthony Avina</li>
|
||||
<li>pro</li>
|
||||
<li>Mark Luxton</li>
|
||||
<li>Crimson Idol</li>
|
||||
<li>Björn König</li>
|
||||
<li>René Weselowski</li>
|
||||
<li>gonczor</li>
|
||||
<li>PlushElderGod</li>
|
||||
<li>gilgwath</li>
|
||||
<li>Tobias</li>
|
||||
<li>Christopher Hillenbrand</li>
|
||||
<li>Daddy's c$sh</li>
|
||||
<li>Ashura</li>
|
||||
<li>Florian</li>
|
||||
<li>Alexandre</li>
|
||||
<li>Dave Jones</li>
|
||||
<li>Brett</li>
|
||||
<li>Ralph Azucena</li>
|
||||
<li>Florian</li>
|
||||
<li>Jim Vanderbilt</li>
|
||||
<li>Brian McGuire</li>
|
||||
<li>Sid Beske</li>
|
||||
<li>Dmitrii Galinskii</li>
|
||||
<li>Johannes Erchen</li>
|
||||
<li>Brandon Zhang</li>
|
||||
<li>Maxley Fraser</li>
|
||||
<li>Nikul Savasadia</li>
|
||||
<li>Claude</li>
|
||||
<li>alga</li>
|
||||
<li>Philipp Jetschina</li>
|
||||
<li>Kristoffer Winther Balling</li>
|
||||
<li>Peter Link</li>
|
||||
<li>David S H Rosenthal</li>
|
||||
<li>Michael Soares</li>
|
||||
<li>Vlad Didenko</li>
|
||||
<li>henloo</li>
|
||||
<li>Erik Rigtorp</li>
|
||||
<li>Barry McKenzie</li>
|
||||
<li>Sebastian van der Est</li>
|
||||
<li>J.C. Polanycia</li>
|
||||
<li>Peter Upfold</li>
|
||||
<li>Josh Yates-Walker</li>
|
||||
<li>Adam</li>
|
||||
<li>HJ</li>
|
||||
<li>bjorndown</li>
|
||||
<li>Vlastimil Vondra</li>
|
||||
<li>Tony Wang</li>
|
||||
<li>John Sivak</li>
|
||||
<li>Nol Aders</li>
|
||||
<li>Dirk Bergstrom</li>
|
||||
<li>proco</li>
|
||||
<li>Philipp Baderschneider</li>
|
||||
<li>Charlie Drake</li>
|
||||
<li>Ryan Goldstein</li>
|
||||
<li>Doug Witt</li>
|
||||
<li>David S H Rosenthal</li>
|
||||
<li>Lance Simmons</li>
|
||||
<li>Mathew Woodyard</li>
|
||||
<li>GanderPL</li>
|
||||
<li>Neša</li>
|
||||
<li>Dimitris Kogias</li>
|
||||
<li>Robin Hellsten</li>
|
||||
<li>Scott Williams</li>
|
||||
<li>klepto68</li>
|
||||
<li>Uwe S.</li>
|
||||
<li>codiflow</li>
|
||||
<li>eugene</li>
|
||||
<li>Anton Fisher</li>
|
||||
<li>David Daly</li>
|
||||
<li>Crispy_Steak</li>
|
||||
<li>Cilestin</li>
|
||||
<li>Benjamin</li>
|
||||
<li>Daniel Lakeland</li>
|
||||
<li>erinacio</li>
|
||||
<li>Leo</li>
|
||||
<li>Payton</li>
|
||||
<li>Saicxs</li>
|
||||
<li>Gorund O</li>
|
||||
<li>Tony G</li>
|
||||
<li>Simonas S.</li>
|
||||
<li>LordKnoppers</li>
|
||||
<li>Fabien Duchaussois</li>
|
||||
<li>Tim Bahnes</li>
|
||||
<li>Aleksei Gusev</li>
|
||||
<li>J Hanssen</li>
|
||||
<li>schoepp</li>
|
||||
<li>Klaus</li>
|
||||
<li>Eric</li>
|
||||
<li>Griffondale</li>
|
||||
<li>Andy D</li>
|
||||
<li>YAMAMOTO Yuji</li>
|
||||
<li>elmiko</li>
|
||||
<li>David</li>
|
||||
<li>Nate Wynd</li>
|
||||
<li>Nicolas</li>
|
||||
<li>magila</li>
|
||||
<li>Bryan Fisher</li>
|
||||
<li>Mark Nicholson</li>
|
||||
<li>Asperatus</li>
|
||||
<li>Patrick Buchan-Hepburn</li>
|
||||
<li>Richárd Faragó</li>
|
||||
<li>David Koch</li>
|
||||
<li>cheese_cake</li>
|
||||
<li>duke_money</li>
|
||||
<li>lund</li>
|
||||
<li>Ivana Kellyer</li>
|
||||
<li>Skullzam</li>
|
||||
<li>Chris Bier</li>
|
||||
<li>Gustavo</li>
|
||||
<li>Henning_IdB</li>
|
||||
<li>Edd</li>
|
||||
<li>Net</li>
|
||||
<li>Sergei Slipchenko</li>
|
||||
<li>Amanita</li>
|
||||
<li>Gaara</li>
|
||||
<li>Max</li>
|
||||
<li>5h4d3</li>
|
||||
<li>James Taylor</li>
|
||||
<li>Alexei Bond</li>
|
||||
<li>cck</li>
|
||||
<li>David L</li>
|
||||
<li>devNull</li>
|
||||
<li>Erica</li>
|
||||
<li>Matthew O</li>
|
||||
<li>Druggo Yang</li>
|
||||
<li>Eric Stokes</li>
|
||||
</ul>
|
||||
<h3>GitHub Sponsors:</h3>
|
||||
<ul>
|
||||
<li>rszamszur</li>
|
||||
<li>Sidicas</li>
|
||||
<li>Mr-NH</li>
|
||||
<li>tolias</li>
|
||||
<li>Adam</li>
|
||||
</ul>
|
||||
<h3>Translations:</h3>
|
||||
<ul>
|
||||
<li><strong>Arabic:</strong> kmutahar</li>
|
||||
<li><strong>Chinese (China):</strong> Biggulu, Brandon_c, hoilc, ligyxy, Small_Ku, umi_neko, vc5</li>
|
||||
<li><strong>Chinese (Taiwan):</strong> BestSteve, flachesis, MiauLightouch, Small_Ku, yan12125, ymhuang0808</li>
|
||||
<li><strong>Czech:</strong> DanielMilde, pavelb, tpavelek</li>
|
||||
<li><strong>English (United Kingdom):</strong> YCMHARHZ</li>
|
||||
<li><strong>English (United States):</strong> alexandercrice, DarkHolme, nguyenlekhtn</li>
|
||||
<li><strong>Finnish:</strong> artnay, hif1, MawKKe, varjolintu</li>
|
||||
<li><strong>German:</strong> antsas, BasicBaer, Calyrx, codejunky, DavidHamburg, eth0, for1real, jensrutschmann,
|
||||
joe776, kflesch, marcbone, MarcEdinger, mcliquid, mfernau77, montilo, nursoda, omnisome4, origin_de, pcrcoding,
|
||||
rgloor, vlenzer, waster, Wyrrrd</li>
|
||||
<li><strong>Greek:</strong> magkopian, nplatis, tassos.b, xinomilo</li>
|
||||
<li><strong>Hungarian:</strong> bubu, meskobalazs, urbalazs</li>
|
||||
<li><strong>Indonesian:</strong> zk</li>
|
||||
<li><strong>Italian:</strong> amaxis, duncanmid, FranzMari, lucaim, NITAL, Peo, tosky, VosaxAlo</li>
|
||||
<li><strong>Japanese:</strong> masoo, p2635, Shinichirou_Yamada, vmemjp, yukinakato</li>
|
||||
<li><strong>Korean:</strong> cancantun, peremen</li>
|
||||
<li><strong>Lithuanian:</strong> Moo</li>
|
||||
<li><strong>Norwegian Bokmål:</strong> eothred, haarek, torgeirf</li>
|
||||
<li><strong>Polish:</strong> keypress, mrerexx, psobczak</li>
|
||||
<li><strong>Portuguese (Brazil):</strong> danielbibit, fabiom, flaviobn, newmanisaac, vitor895, weslly</li>
|
||||
<li><strong>Portuguese (Portugal):</strong> a.santos, American_Jesus, hds, lmagomes, smarquespt</li>
|
||||
<li><strong>Romanian:</strong> alexminza</li>
|
||||
<li><strong>Russian:</strong> _nomoretears_, agag11507, alexminza, anm, artemkonenko, denoos, KekcuHa, Mogost,
|
||||
netforhack, NetWormKido, RKuchma, ShareDVI, talvind, VictorR2007, vsvyatski, wkill95</li>
|
||||
<li><strong>Serbian:</strong> ArtBIT</li>
|
||||
<li><strong>Swedish:</strong> Anders_Bergqvist, henziger, jpyllman, peron, Thelin</li>
|
||||
<li><strong>Turkish:</strong> etc, N3pp</li>
|
||||
<li><strong>Ukrainian:</strong> brisk022, netforhack, ShareDVI, zoresvit</li>
|
||||
<li><strong>العربية (Arabic)</strong>: 3eani, 3nad, AboShanab, butterflyoffire_temp, jBailony, kmutahar, m.hemoudi,
|
||||
Marouane87, microtaha, mohame1d, muha_abdulaziz, Night1, omar.nsy, TheAhmed, zer0x</li>
|
||||
<li><strong>euskara (Basque)</strong>: azken_tximinoa, Galaipa, Hey_neken, porrumentzio</li>
|
||||
<li><strong>বাংলা (Bengali)</strong>: codesmite, Foxom, rediancool, RHJihan</li>
|
||||
<li><strong>ဗမာစာ (Burmese)</strong>: Christine.Ivy, hafe14, Snooooowwwwwman, tuntunaung</li>
|
||||
<li><strong>català (Catalan)</strong>: antoniopolonio, benLabcat, capitantrueno, dsoms, Ecron, jamalinu, jmaribau,
|
||||
MarcRiera, mcus, raulua, zeehio, ZJaume</li>
|
||||
<li><strong>中文 (Chinese (Simplified))</strong>: Biacke, Biggulu, Brandon_c, carp0129, Clafiok, deluxghost, Dy64,
|
||||
ef6, Felix2yu, hoilc, jy06308127, kikyous, kofzhanganguo, ligyxy, lxx4380, oksjd, remonli, ShuiHuo, sjdhome,
|
||||
slgray, Small_Ku, snhun, umi_neko, vc5, Wylmer_Wang, Z4HD</li>
|
||||
<li><strong>中文 (台灣) (Chinese (Traditional))</strong>: BestSteve, Biacke, flachesis, gojpdchx, ligyxy, MiauLightouch,
|
||||
plesry, priv, raymondtau, Siriusmart, Small_Ku, ssuhung, Superbil, th3lusive, yan12125, ymhuang0808</li>
|
||||
<li><strong>hrvatski jezik (Croatian)</strong>: krekrekre, mladenuzelac</li>
|
||||
<li><strong>čeština (Czech)</strong>: DanielMilde, jiri.jagos, pavelb, pavelz, S474N, stps, tpavelek, vojtechjurcik</li>
|
||||
<li><strong>dansk (Danish)</strong>: alfabetacain, dovecode, ebbe, ERYpTION, GimliDk, Grooty12, JakobPP, KalleDK,
|
||||
MannVera, nlkl, Saustrup, thniels</li>
|
||||
<li><strong>Nederlands (Dutch)</strong>: apie, bartlibert, Bubbel, bython, Dr.Default, e2jk, evanoosten, fourwood,
|
||||
fvw, glotzbach, JCKalman, keunes, KnooL, ms.vd.linden, ovisicnarf, pietermj, pvdl, rigrig, srgvg, Stephan_P,
|
||||
stijndubrul, theniels17, ThomasChurchman, timschreinemachers, Vistaus, wanderingidea, Zombaya1</li>
|
||||
<li><strong>Esperanto (Esperanto)</strong>: batisteo</li>
|
||||
<li><strong>eesti (Estonian)</strong>: Hermanio, okul, sarnane, tlend, V6lur</li>
|
||||
<li><strong>suomi (Finnish)</strong>: artnay, hif1, MawKKe, petri, tomisalmi, uusijani, varjolintu</li>
|
||||
<li><strong>français (French)</strong>: ayiniho, Beatussum, butterflyoffire_temp, Cabirto, francoisa, iannick,
|
||||
jean_pierre_raumer, John.Mickael, Jojo6375, lacnic, Marouane87, mohame1d, orion78fr, stephanecodes, swarmpan,
|
||||
t0mmy742, Takeçi, tenzap, webafrancois, x0rld</li>
|
||||
<li><strong>Galego (Galician)</strong>: damufo, enfeitizador, mustek</li>
|
||||
<li><strong>Deutsch (German)</strong>: andreas.maier, antsas, archer_321, ASDFGamer, Atalanttore, BasicBaer, blacksn0w,
|
||||
bwolkchen, Calyrx, clonejo, codejunky, DavidHamburg, eth0, fahstat, FlotterCodername, for1real, frlan, funny0facer,
|
||||
Gyges, h_h, Hativ, heynemax, hjonas, HoferJulian, hueku, janis91, jensrutschmann, jhit, joe776, kflesch, man_at_home,
|
||||
marcbone, MarcEdinger, markusd112, Marouane87, maxwxyz, mcliquid, mfernau77, mircsicz, montilo, MuehlburgPhoenix,
|
||||
muellerma, nautilusx, neon64, Nerzahd, Nightwriter, noodles101, NotAName, nursoda, OLLI_S, omnisome4, origin_de,
|
||||
pcrcoding, PFischbeck, phallobst, philje, pqtjhhBzDd5NuJ9, r3drock, rakekniven, revoltek, rgloor, Rheggie, RogueThorn,
|
||||
rugk, ScholliYT, scotwee, Silas_229, spacemanspiff, SteffoSpieler, testarossa47, TheForcer, thillux, transi_222, traschke,
|
||||
Unkn0wnCat, vlenzer, vpav, waster, wolfram.roesler, Wyrrrd, xf</li>
|
||||
<li><strong>ελληνικά (Greek)</strong>: anvo, arttor, Dkafetzis, giwrgosmant, GorianM, Jason_M, magkopian, nplatis, saglogog,
|
||||
tassos.b, xinomilo</li>
|
||||
<li><strong>עברית (Hebrew)</strong>: avimar, ronyala, shemeshg, shmag18, ThunderB0lt, tryandtry, ztwersky</li>
|
||||
<li><strong>magyar (Hungarian)</strong>: andras_tim, bubu, entaevau, kempelen, meskobalazs, spammy, typingseashell, urbalazs</li>
|
||||
<li><strong>Íslenska (Icelandic)</strong>: MannVera</li>
|
||||
<li><strong>Bahasa Indonesia (Indonesian)</strong>: achmad, algustionesa, bora_ach, racrbmr, zk</li>
|
||||
<li><strong>Italiano (Italian)</strong>: aleb2000, amaxis, bovirus, duncanmid, FranzMari, Gringoarg, idetao, lucaim, NITAL, Peo,
|
||||
Pietrog, salvatorecordiano, seatedscribe, Stemby, the.sailor, tosky, VosaxAlo</li>
|
||||
<li><strong>日本語 (Japanese)</strong>: AlCooo, gojpdchx, helloguys, masoo, p2635, Shinichirou_Yamada, shortarrow, ssuhung, tadasu,
|
||||
take100yen, Umoxfo, vargas.peniel, vmemjp, WatanabeShint, yukinakato</li>
|
||||
<li><strong>қазақ тілі (Kazakh)</strong>: sotrud_nik</li>
|
||||
<li><strong>한국어 (Korean)</strong>: BraINstinct0, cancantun, peremen</li>
|
||||
<li><strong>latine (Latin)</strong>: alexandercrice</li>
|
||||
<li><strong>latviešu valoda (Latvian)</strong>: andis.luksho, victormeirans, wakeeshi</li>
|
||||
<li><strong>lietuvių kalba (Lithuanian)</strong>: Kornelijus, Moo, pauliusbaulius, rookwood101, wakeeshi</li>
|
||||
<li><strong>Norsk Bokmål (Norwegian Bokmål)</strong>: bkvamme, eirikl, eothred, haarek, JardarBolin, jumpingmushroom, sattor,
|
||||
torgeirf, ysteinalver</li>
|
||||
<li><strong>ਪੰਜਾਬੀ (Punjabi)</strong>: aalam</li>
|
||||
<li><strong>فارسی (Farsi)</strong>: gnulover, siamax</li>
|
||||
<li><strong>فارسی (Farsi (Iran))</strong>: magnifico</li>
|
||||
<li><strong>język polski (Polish)</strong>: AreYouLoco, dedal123, EsEnZeT, hoek, keypress, konradmb, mrerexx, pabli, ply,
|
||||
psobczak, SebJez, verahawk</li>
|
||||
<li><strong>Português (Portuguese)</strong>: diraol, hugok, pfialho, rudahximenes, weslly, xendez</li>
|
||||
<li><strong>Português (Portuguese (Brazil))</strong>: alinda, amalvarenga, andersoniop, danielbibit, diraol, fabiom, flaviobn,
|
||||
fmilagres, furious_, gabrieljcs, Guilherme.Peev, guilherme__sr, Havokdan, igorruckert, josephelias94, keeBR, kiskadee, lecalam,
|
||||
lucasjsoliveira, mauri.andres, newmanisaac, rafaelnp, ruanmed, rudahximenes, ul1sses, vitor895, weslly, wtuemura, xendez,
|
||||
zodSilence</li>
|
||||
<li><strong>Português (Portuguese (Portugal))</strong>: a.santos, American_Jesus, arainho, hds, hugok, lecalam, lmagomes, pfialho,
|
||||
smarquespt, smiguel, xendez, xnenjm</li>
|
||||
<li><strong>Română (Romanian)</strong>: _parasite_, aduzsardi, alexminza, polearnik</li>
|
||||
<li><strong>русский (Russian)</strong>: 3nad, _nomoretears_, agag11507, alexandersokol, alexminza, anm, artemkonenko, ashed,
|
||||
BANOnotIT, burningalchemist, cl0ne, cnide, denoos, DG, DmitriyMaksimov, egorrabota, injseon, Japet, JayDi85, KekcuHa, kerastinell,
|
||||
laborxcom, leo9uinuo98, Mogost, Mr.GreyWolf, MustangDSG, netforhack, NetWormKido, nibir, Olesya_Gerasimenko, onix, Orianti,
|
||||
RKuchma, ruslan.denisenko, ShareDVI, Shevchuk, solodyagin, talvind, treylav, upupa, VictorR2007, vsvyatski, wakeeshi, Walter.S,
|
||||
wkill95, wtigga, zOrg1331</li>
|
||||
<li><strong>српски језик (Serbian)</strong>: ArtBIT, ozzii</li>
|
||||
<li><strong>Slovenčina (Slovak)</strong>: Asprotes, crazko, jose1711, l.martinicky, pecer, reisuya, Slavko</li>
|
||||
<li><strong>Slovenščina (Slovenian)</strong>: asasdasd, samodekleva</li>
|
||||
<li><strong>Español (Spanish)</strong>: adolfogc, antifaz, capitantrueno, cquike, cyphra, DarkHolme, doubleshuffle, e2jk,
|
||||
EdwardNavarro, fserrador, gabeweb, gonrial, jjtp, jorpilo, LeoBeltran, mauri.andres, piegope, pquin, puchrojo, rodolfo.guagnini,
|
||||
tierracomun, vsvyatski</li>
|
||||
<li><strong>Svenska (Swedish)</strong>: 0x9fff00, aiix, Anders_Bergqvist, ArmanB, Autom, baxtex, eson, henziger, jpyllman, malkus,
|
||||
merikan, peron, peterkz, Thelin, theschitz, victorhggqvst</li>
|
||||
<li><strong>ไทย (Thai)</strong>: arthit, ben_cm, chumaporn.t, darika, digitalthailandproject, GitJirasamatakij, ll3an, minoplhy,
|
||||
muhammadmumean, nimid, nipattra, ordinaryjane, rayg, sirawat, Socialister, Wipanee</li>
|
||||
<li><strong>Türkçe (Turkish)</strong>: abcmen, ahmed.ulusoy, cagries, denizoglu, desc4rtes, etc, ethem578, kayazeren, mcveri, N3pp,
|
||||
rgucluer, SeLeNLeR, sprlptr48, TeknoMobil, Ven_Zallow, veysiertekin</li>
|
||||
<li><strong>Українська (Ukrainian)</strong>: brisk022, chulivska, cl0ne, exlevan, m0stik, moudrick, netforhack, olko, onix, paul_sm,
|
||||
ShareDVI, upupa, zoresvit</li>
|
||||
</ul>
|
||||
)";
|
||||
|
||||
|
||||
@@ -348,8 +348,15 @@ void ApplicationSettingsWidget::loadSettings()
|
||||
m_secUi->hideTotpCheckBox->setChecked(config()->get(Config::Security_HideTotpPreviewPanel).toBool());
|
||||
m_secUi->hideNotesCheckBox->setChecked(config()->get(Config::Security_HideNotes).toBool());
|
||||
|
||||
m_secUi->quickUnlockCheckBox->setEnabled(getQuickUnlock()->isAvailable());
|
||||
m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
|
||||
m_secUi->quickUnlockRememberCheckBox->setChecked(config()->get(Config::Security_QuickUnlockRemember).toBool());
|
||||
#ifdef Q_OS_LINUX
|
||||
// Remembering quick unlock is not supported on Linux
|
||||
m_secUi->quickUnlockRememberCheckBox->setVisible(false);
|
||||
#else
|
||||
// Only show this option if Touch ID or Windows Hello are available for use
|
||||
m_secUi->quickUnlockRememberCheckBox->setVisible(getQuickUnlock()->isNativeAvailable());
|
||||
#endif
|
||||
|
||||
for (const ExtraPage& page : asConst(m_extraPages)) {
|
||||
page.loadSettings();
|
||||
@@ -469,9 +476,8 @@ void ApplicationSettingsWidget::saveSettings()
|
||||
config()->set(Config::Security_HideTotpPreviewPanel, m_secUi->hideTotpCheckBox->isChecked());
|
||||
config()->set(Config::Security_HideNotes, m_secUi->hideNotesCheckBox->isChecked());
|
||||
|
||||
if (m_secUi->quickUnlockCheckBox->isEnabled()) {
|
||||
config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked());
|
||||
}
|
||||
config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked());
|
||||
config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked());
|
||||
|
||||
// Security: clear storage if related settings are disabled
|
||||
if (!config()->get(Config::RememberLastDatabases).toBool()) {
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -174,7 +174,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -210,7 +210,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -250,7 +250,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -315,7 +315,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -483,7 +483,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -516,7 +516,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -648,7 +648,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -681,7 +681,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -743,7 +743,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -971,7 +971,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -1019,7 +1019,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -1076,7 +1076,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>364</width>
|
||||
<height>505</height>
|
||||
<width>437</width>
|
||||
<height>529</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
@@ -138,7 +138,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -168,7 +168,14 @@
|
||||
<item>
|
||||
<widget class="QCheckBox" name="quickUnlockCheckBox">
|
||||
<property name="text">
|
||||
<string>Enable database quick unlock (Touch ID / Windows Hello)</string>
|
||||
<string>Enable database quick unlock by default</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="quickUnlockRememberCheckBox">
|
||||
<property name="text">
|
||||
<string>Remember quick unlock after database is closed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -100,9 +100,8 @@ void Clipboard::clearCopiedText()
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_lastCopied.isEmpty()
|
||||
&& (m_lastCopied == clipboard->text(QClipboard::Clipboard)
|
||||
|| m_lastCopied == clipboard->text(QClipboard::Selection))) {
|
||||
if (m_lastCopied == clipboard->text(QClipboard::Clipboard)
|
||||
|| m_lastCopied == clipboard->text(QClipboard::Selection)) {
|
||||
clipboard->clear(QClipboard::Clipboard);
|
||||
clipboard->clear(QClipboard::Selection);
|
||||
#ifdef Q_OS_UNIX
|
||||
|
||||
@@ -84,9 +84,8 @@ void DatabaseOpenDialog::showEvent(QShowEvent* event)
|
||||
{
|
||||
QDialog::showEvent(event);
|
||||
QTimer::singleShot(100, this, [this] {
|
||||
if (m_view->isOnQuickUnlockScreen() && !m_view->unlockingDatabase()) {
|
||||
m_view->triggerQuickUnlock();
|
||||
}
|
||||
// Automatically trigger quick unlock if it's available
|
||||
m_view->triggerQuickUnlock();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -38,14 +38,6 @@
|
||||
namespace
|
||||
{
|
||||
constexpr int clearFormsDelay = 30000;
|
||||
|
||||
bool isQuickUnlockAvailable()
|
||||
{
|
||||
if (config()->get(Config::Security_QuickUnlock).toBool()) {
|
||||
return getQuickUnlock()->isAvailable();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
|
||||
@@ -68,17 +60,10 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
|
||||
m_ui->editPassword->setShowPassword(false);
|
||||
});
|
||||
|
||||
QFont font;
|
||||
font.setPointSize(font.pointSize() + 4);
|
||||
font.setBold(true);
|
||||
m_ui->labelHeadline->setFont(font);
|
||||
|
||||
m_ui->quickUnlockButton->setFont(font);
|
||||
m_ui->quickUnlockButton->setIcon(
|
||||
icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText)));
|
||||
m_ui->quickUnlockButton->setIconSize({32, 32});
|
||||
|
||||
connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile()));
|
||||
QFont largeFont;
|
||||
largeFont.setPointSize(largeFont.pointSize() + 4);
|
||||
largeFont.setBold(true);
|
||||
m_ui->labelHeadline->setFont(largeFont);
|
||||
|
||||
auto okBtn = m_ui->buttonBox->button(QDialogButtonBox::Ok);
|
||||
okBtn->setText(tr("Unlock"));
|
||||
@@ -86,16 +71,19 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
|
||||
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase()));
|
||||
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
|
||||
|
||||
// Key file components
|
||||
m_ui->selectKeyFileComponent->setVisible(false);
|
||||
connect(m_ui->addKeyFileLinkLabel, &QLabel::linkActivated, this, &DatabaseOpenWidget::browseKeyFile);
|
||||
connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile()));
|
||||
connect(m_ui->keyFileLineEdit, &PasswordWidget::textChanged, this, [&](const QString& text) {
|
||||
bool state = !text.isEmpty();
|
||||
m_ui->addKeyFileLinkLabel->setVisible(!state);
|
||||
m_ui->selectKeyFileComponent->setVisible(state);
|
||||
});
|
||||
connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled);
|
||||
|
||||
m_ui->selectKeyFileComponent->setVisible(false);
|
||||
// Hardware key components
|
||||
toggleHardwareKeyComponent(false);
|
||||
connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled);
|
||||
|
||||
QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy();
|
||||
sp.setRetainSizeWhenHidden(true);
|
||||
@@ -127,13 +115,24 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
|
||||
m_ui->refreshHardwareKeys->setVisible(false);
|
||||
#endif
|
||||
|
||||
// QuickUnlock actions
|
||||
// QuickUnlock components
|
||||
m_ui->quickUnlockButton->setFont(largeFont);
|
||||
m_ui->quickUnlockButton->setIcon(
|
||||
icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText)));
|
||||
|
||||
connect(m_ui->quickUnlockButton, &QPushButton::pressed, this, [this] { openDatabase(); });
|
||||
connect(m_ui->resetQuickUnlockButton, &QPushButton::pressed, this, [this] { resetQuickUnlock(); });
|
||||
connect(m_ui->closeQuickUnlockButton, &QPushButton::pressed, this, [this] { reject(); });
|
||||
m_ui->resetQuickUnlockButton->setShortcut(Qt::Key_Escape);
|
||||
}
|
||||
|
||||
DatabaseOpenWidget::~DatabaseOpenWidget() = default;
|
||||
DatabaseOpenWidget::~DatabaseOpenWidget()
|
||||
{
|
||||
// Reset quick unlock if we are not remembering it
|
||||
if (!config()->get(Config::Security_QuickUnlockRemember).toBool()) {
|
||||
resetQuickUnlock();
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::toggleHardwareKeyComponent(bool state)
|
||||
{
|
||||
@@ -189,7 +188,7 @@ bool DatabaseOpenWidget::event(QEvent* event)
|
||||
auto type = event->type();
|
||||
|
||||
if (type == QEvent::Show || type == QEvent::WindowActivate) {
|
||||
if (isOnQuickUnlockScreen() && (m_db.isNull() || !canPerformQuickUnlock())) {
|
||||
if (isOnQuickUnlockScreen() && !canPerformQuickUnlock()) {
|
||||
resetQuickUnlock();
|
||||
}
|
||||
toggleQuickUnlockScreen();
|
||||
@@ -294,6 +293,7 @@ void DatabaseOpenWidget::load(const QString& filename)
|
||||
}
|
||||
|
||||
toggleQuickUnlockScreen();
|
||||
m_ui->enableQuickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
|
||||
|
||||
#ifdef WITH_XC_YUBIKEY
|
||||
// Do initial auto-poll
|
||||
@@ -335,16 +335,12 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
|
||||
|
||||
m_ui->editPassword->setText(pw);
|
||||
m_ui->keyFileLineEdit->setText(keyFile);
|
||||
m_blockQuickUnlock = true;
|
||||
m_ui->enableQuickUnlockCheckBox->setChecked(false);
|
||||
openDatabase();
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::openDatabase()
|
||||
{
|
||||
// Cache this variable for future use then reset
|
||||
bool blockQuickUnlock = m_blockQuickUnlock || isOnQuickUnlockScreen();
|
||||
m_blockQuickUnlock = false;
|
||||
|
||||
setUserInteractionLock(true);
|
||||
m_ui->editPassword->setShowPassword(false);
|
||||
m_ui->messageWidget->hide();
|
||||
@@ -386,10 +382,13 @@ void DatabaseOpenWidget::openDatabase()
|
||||
}
|
||||
}
|
||||
|
||||
// Save Quick Unlock credentials if available
|
||||
if (!blockQuickUnlock && isQuickUnlockAvailable()) {
|
||||
// Save Quick Unlock credentials if available and enabled
|
||||
if (!isOnQuickUnlockScreen() && isQuickUnlockAvailable() && m_ui->enableQuickUnlockCheckBox->isChecked()) {
|
||||
auto keyData = databaseKey->serialize();
|
||||
getQuickUnlock()->setKey(m_db->publicUuid(), keyData);
|
||||
auto qu = getQuickUnlock()->interface();
|
||||
if (!qu->setKey(m_db->publicUuid(), keyData) && !qu->errorString().isEmpty()) {
|
||||
getMainWindow()->displayTabMessage(qu->errorString(), MessageWidget::MessageType::Warning);
|
||||
}
|
||||
m_ui->messageWidget->hideMessage();
|
||||
}
|
||||
|
||||
@@ -434,13 +433,16 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
|
||||
{
|
||||
auto databaseKey = QSharedPointer<CompositeKey>::create();
|
||||
|
||||
if (!m_db.isNull() && canPerformQuickUnlock()) {
|
||||
// try to retrieve the stored password using Windows Hello
|
||||
if (canPerformQuickUnlock()) {
|
||||
// try to retrieve the stored password using quick unlock
|
||||
QByteArray keyData;
|
||||
if (!getQuickUnlock()->getKey(m_db->publicUuid(), keyData)) {
|
||||
m_ui->messageWidget->showMessage(
|
||||
tr("Failed to authenticate with Quick Unlock: %1").arg(getQuickUnlock()->errorString()),
|
||||
MessageWidget::Error);
|
||||
auto qu = getQuickUnlock()->interface();
|
||||
if (!qu->getKey(m_db->publicUuid(), keyData)) {
|
||||
m_ui->messageWidget->showMessage(tr("Failed to authenticate with Quick Unlock: %1").arg(qu->errorString()),
|
||||
MessageWidget::Error);
|
||||
if (!qu->hasKey(m_db->publicUuid())) {
|
||||
resetQuickUnlock();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
databaseKey->setRawKey(keyData);
|
||||
@@ -627,9 +629,15 @@ void DatabaseOpenWidget::setUserInteractionLock(bool state)
|
||||
m_unlockingDatabase = state;
|
||||
}
|
||||
|
||||
bool DatabaseOpenWidget::isQuickUnlockAvailable() const
|
||||
{
|
||||
auto qu = getQuickUnlock()->interface();
|
||||
return qu && qu->isAvailable();
|
||||
}
|
||||
|
||||
bool DatabaseOpenWidget::canPerformQuickUnlock() const
|
||||
{
|
||||
return !m_db.isNull() && isQuickUnlockAvailable() && getQuickUnlock()->hasKey(m_db->publicUuid());
|
||||
return m_db && isQuickUnlockAvailable() && getQuickUnlock()->interface()->hasKey(m_db->publicUuid());
|
||||
}
|
||||
|
||||
bool DatabaseOpenWidget::isOnQuickUnlockScreen() const
|
||||
@@ -656,7 +664,7 @@ void DatabaseOpenWidget::toggleQuickUnlockScreen()
|
||||
|
||||
void DatabaseOpenWidget::triggerQuickUnlock()
|
||||
{
|
||||
if (isOnQuickUnlockScreen()) {
|
||||
if (isOnQuickUnlockScreen() && !unlockingDatabase()) {
|
||||
m_ui->quickUnlockButton->click();
|
||||
}
|
||||
}
|
||||
@@ -668,11 +676,9 @@ void DatabaseOpenWidget::triggerQuickUnlock()
|
||||
*/
|
||||
void DatabaseOpenWidget::resetQuickUnlock()
|
||||
{
|
||||
if (!isQuickUnlockAvailable()) {
|
||||
return;
|
||||
}
|
||||
if (!m_db.isNull()) {
|
||||
getQuickUnlock()->reset(m_db->publicUuid());
|
||||
auto qu = getQuickUnlock()->interface();
|
||||
if (m_db && qu) {
|
||||
qu->reset(m_db->publicUuid());
|
||||
}
|
||||
load(m_filename);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#ifndef KEEPASSX_DATABASEOPENWIDGET_H
|
||||
#define KEEPASSX_DATABASEOPENWIDGET_H
|
||||
|
||||
#include <QPointer>
|
||||
#include <QScopedPointer>
|
||||
#include <QTimer>
|
||||
|
||||
@@ -46,21 +45,17 @@ class DatabaseOpenWidget : public DialogyWidget
|
||||
public:
|
||||
explicit DatabaseOpenWidget(QWidget* parent = nullptr);
|
||||
~DatabaseOpenWidget() override;
|
||||
|
||||
void load(const QString& filename);
|
||||
QString filename();
|
||||
QSharedPointer<Database> database();
|
||||
|
||||
void clearForms();
|
||||
void enterKey(const QString& pw, const QString& keyFile);
|
||||
QSharedPointer<Database> database();
|
||||
void triggerQuickUnlock();
|
||||
bool unlockingDatabase();
|
||||
void showMessage(const QString& text, MessageWidget::MessageType type, int autoHideTimeout);
|
||||
|
||||
// Quick Unlock helper functions
|
||||
bool canPerformQuickUnlock() const;
|
||||
bool isOnQuickUnlockScreen() const;
|
||||
void toggleQuickUnlockScreen();
|
||||
void triggerQuickUnlock();
|
||||
void resetQuickUnlock();
|
||||
|
||||
signals:
|
||||
void dialogFinished(bool accepted);
|
||||
|
||||
@@ -85,14 +80,20 @@ private slots:
|
||||
void closeDatabase();
|
||||
void pollHardwareKey(bool manualTrigger = false, int delay = 0);
|
||||
void hardwareKeyResponse(bool found);
|
||||
void resetQuickUnlock();
|
||||
|
||||
private:
|
||||
// Quick Unlock helper functions
|
||||
bool isQuickUnlockAvailable() const;
|
||||
bool canPerformQuickUnlock() const;
|
||||
bool isOnQuickUnlockScreen() const;
|
||||
void toggleQuickUnlockScreen();
|
||||
|
||||
#ifdef WITH_XC_YUBIKEY
|
||||
QPointer<DeviceListener> m_deviceListener;
|
||||
#endif
|
||||
bool m_pollingHardwareKey = false;
|
||||
bool m_manualHardwareKeyRefresh = false;
|
||||
bool m_blockQuickUnlock = false;
|
||||
bool m_unlockingDatabase = false;
|
||||
bool m_triedToQuit = false;
|
||||
QTimer m_hideTimer;
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="passwordLabel">
|
||||
@@ -192,7 +192,7 @@
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="PasswordWidget" name="editPassword" native="true">
|
||||
@@ -250,13 +250,13 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>10</number>
|
||||
<number>15</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="selectKeyFileLabel">
|
||||
@@ -399,7 +399,7 @@
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
@@ -465,6 +465,48 @@
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="enableQuickUnlockCheckBox">
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::RightToLeft</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Quick Unlock</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_7">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>8</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item alignment="Qt::AlignRight">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="focusPolicy">
|
||||
@@ -511,6 +553,9 @@
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_7">
|
||||
<property name="orientation">
|
||||
@@ -542,17 +587,81 @@
|
||||
<property name="text">
|
||||
<string>Unlock Database</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="resetQuickUnlockButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
<spacer name="verticalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>4</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="resetQuickUnlockButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Reset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_8">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>4</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="closeQuickUnlockButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Close Database</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_8">
|
||||
@@ -645,8 +754,6 @@
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>quickUnlockButton</tabstop>
|
||||
<tabstop>resetQuickUnlockButton</tabstop>
|
||||
<tabstop>editPassword</tabstop>
|
||||
<tabstop>keyFileLineEdit</tabstop>
|
||||
<tabstop>buttonBrowseFile</tabstop>
|
||||
@@ -654,7 +761,11 @@
|
||||
<tabstop>hardwareKeyCombo</tabstop>
|
||||
<tabstop>refreshHardwareKeys</tabstop>
|
||||
<tabstop>addKeyFileLinkLabel</tabstop>
|
||||
<tabstop>enableQuickUnlockCheckBox</tabstop>
|
||||
<tabstop>buttonBox</tabstop>
|
||||
<tabstop>quickUnlockButton</tabstop>
|
||||
<tabstop>resetQuickUnlockButton</tabstop>
|
||||
<tabstop>closeQuickUnlockButton</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
|
||||
@@ -674,7 +674,7 @@ void DatabaseTabWidget::updateTabName(int index)
|
||||
return;
|
||||
}
|
||||
index = indexOf(dbWidget);
|
||||
setTabText(index, Tools::escapeAccelerators(tabName(index)));
|
||||
setTabText(index, tabName(index));
|
||||
setTabToolTip(index, dbWidget->displayFilePath());
|
||||
auto iconIndex = dbWidget->database()->publicIcon();
|
||||
if (iconIndex >= 0 && iconIndex < databaseIcons()->count()) {
|
||||
|
||||
@@ -1978,8 +1978,6 @@ void DatabaseWidget::closeEvent(QCloseEvent* event)
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
m_databaseOpenWidget->resetQuickUnlock();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
|
||||
@@ -92,14 +92,12 @@ QIcon Icons::trayIcon(bool unlocked)
|
||||
}
|
||||
|
||||
QIcon i;
|
||||
#if defined(Q_OS_WIN)
|
||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||
if (osUtils->isStatusBarDark()) {
|
||||
i = icon(QString("keepassxc-monochrome-light%1").arg(suffix), false);
|
||||
} else {
|
||||
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
|
||||
i = icon(QString("%1-%2%3").arg(applicationIconName(), iconAppearance, suffix), false);
|
||||
#endif
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
#include "autotype/AutoType.h"
|
||||
#include "core/InactivityTimer.h"
|
||||
#include "core/Resources.h"
|
||||
#include "core/Tools.h"
|
||||
#include "gui/AboutDialog.h"
|
||||
#include "gui/ActionCollection.h"
|
||||
#include "gui/Icons.h"
|
||||
@@ -194,6 +193,9 @@ MainWindow::MainWindow()
|
||||
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::databaseUnlocked, this, &MainWindow::databaseUnlocked);
|
||||
connect(m_ui->tabWidget, &DatabaseTabWidget::activeDatabaseChanged, this, &MainWindow::activeDatabaseChanged);
|
||||
@@ -639,13 +641,12 @@ MainWindow::MainWindow()
|
||||
auto* hidePreRelWarn = new QAction(tr("Don't show again for this version"), m_ui->globalMessageWidget);
|
||||
m_ui->globalMessageWidget->addAction(hidePreRelWarn);
|
||||
auto hidePreRelWarnConn = QSharedPointer<QMetaObject::Connection>::create();
|
||||
*hidePreRelWarnConn = connect(
|
||||
m_ui->globalMessageWidget, &KMessageWidget::hideAnimationFinished, [this, hidePreRelWarn, hidePreRelWarnConn] {
|
||||
m_ui->globalMessageWidget->removeAction(hidePreRelWarn);
|
||||
disconnect(*hidePreRelWarnConn);
|
||||
hidePreRelWarn->deleteLater();
|
||||
});
|
||||
connect(hidePreRelWarn, &QAction::triggered, [this] {
|
||||
*hidePreRelWarnConn = connect(m_ui->globalMessageWidget, &KMessageWidget::hideAnimationFinished, [=] {
|
||||
m_ui->globalMessageWidget->removeAction(hidePreRelWarn);
|
||||
disconnect(*hidePreRelWarnConn);
|
||||
hidePreRelWarn->deleteLater();
|
||||
});
|
||||
connect(hidePreRelWarn, &QAction::triggered, [=] {
|
||||
m_ui->globalMessageWidget->animatedHide();
|
||||
config()->set(Config::Messages_HidePreReleaseWarning, KEEPASSXC_VERSION);
|
||||
});
|
||||
@@ -788,7 +789,7 @@ void MainWindow::updateLastDatabasesMenu()
|
||||
|
||||
const QStringList lastDatabases = config()->get(Config::LastDatabases).toStringList();
|
||||
for (const QString& database : lastDatabases) {
|
||||
QAction* action = m_ui->menuRecentDatabases->addAction(Tools::escapeAccelerators(database));
|
||||
QAction* action = m_ui->menuRecentDatabases->addAction(database);
|
||||
action->setData(database);
|
||||
m_lastDatabasesActions->addAction(action);
|
||||
}
|
||||
@@ -1380,12 +1381,6 @@ void MainWindow::showEvent(QShowEvent* event)
|
||||
// Qt Hack - Prevent white flicker when showing window
|
||||
QTimer::singleShot(50, this, [=] { setProperty("windowOpacity", 1.0); });
|
||||
#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)
|
||||
@@ -1543,12 +1538,6 @@ void MainWindow::saveWindowInformation()
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::restoreWindowInformation()
|
||||
{
|
||||
restoreGeometry(config()->get(Config::GUI_MainWindowGeometry).toByteArray());
|
||||
restoreState(config()->get(Config::GUI_MainWindowState).toByteArray());
|
||||
}
|
||||
|
||||
bool MainWindow::saveLastDatabases()
|
||||
{
|
||||
if (config()->get(Config::OpenPreviousDatabasesOnStartup).toBool()) {
|
||||
|
||||
@@ -160,7 +160,6 @@ private:
|
||||
static const QString BaseWindowTitle;
|
||||
|
||||
void saveWindowInformation();
|
||||
void restoreWindowInformation();
|
||||
bool saveLastDatabases();
|
||||
bool isTrayIconEnabled() const;
|
||||
void customOpenUrl(QString url);
|
||||
@@ -193,7 +192,6 @@ private:
|
||||
|
||||
Q_DISABLE_COPY(MainWindow)
|
||||
|
||||
bool m_windowInformationRestored = false;
|
||||
bool m_appExitCalled = false;
|
||||
bool m_appExiting = false;
|
||||
bool m_restartRequested = false;
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
namespace
|
||||
{
|
||||
// Extract group names from nested path and return the last group created
|
||||
Group* createGroupStructure(Database* db, const QString& groupPath, const QString& rootGroupToSkip)
|
||||
Group* createGroupStructure(Database* db, const QString& groupPath)
|
||||
{
|
||||
auto group = db->rootGroup();
|
||||
if (!group || groupPath.isEmpty()) {
|
||||
@@ -42,10 +42,8 @@ namespace
|
||||
}
|
||||
|
||||
auto nameList = groupPath.split("/", Qt::SkipEmptyParts);
|
||||
|
||||
// Skip the identified root group name if present
|
||||
if (!rootGroupToSkip.isEmpty() && !nameList.isEmpty()
|
||||
&& nameList.first().compare(rootGroupToSkip, Qt::CaseInsensitive) == 0) {
|
||||
// Skip over first group name if root
|
||||
if (nameList.first().compare("root", Qt::CaseInsensitive) == 0) {
|
||||
nameList.removeFirst();
|
||||
}
|
||||
|
||||
@@ -243,26 +241,8 @@ QSharedPointer<Database> CsvImportWidget::buildDatabase()
|
||||
db->rootGroup()->setNotes(tr("Imported from CSV file: %1").arg(m_filename));
|
||||
|
||||
auto rows = m_parserModel->rowCount() - m_parserModel->skippedRows();
|
||||
|
||||
// Check for common root group
|
||||
QString rootGroupName;
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
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);
|
||||
auto group = createGroupStructure(db.data(), m_parserModel->data(m_parserModel->index(r, 0)).toString());
|
||||
if (!group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -132,9 +132,6 @@ void KeyFileEditWidget::browseKeyFile()
|
||||
QString filters = QString("%1 (*.keyx *.key);;%2 (*)").arg(tr("Key files"), tr("All files"));
|
||||
QString fileName = fileDialog()->getOpenFileName(this, tr("Select a key file"), QString(), filters);
|
||||
|
||||
if (fileName.isEmpty()) { // user clicked on cancel
|
||||
return;
|
||||
}
|
||||
if (QFileInfo(fileName).canonicalFilePath() == m_parent->getDatabase()->canonicalFilePath()) {
|
||||
MessageBox::critical(getMainWindow(),
|
||||
tr("Invalid Key File"),
|
||||
|
||||
@@ -229,7 +229,7 @@ bool DatabaseSettingsWidgetDatabaseKey::saveSettings()
|
||||
|
||||
m_db->setKey(newKey, true, false, false);
|
||||
|
||||
getQuickUnlock()->reset(m_db->publicUuid());
|
||||
getQuickUnlock()->interface()->reset(m_db->publicUuid());
|
||||
|
||||
emit editFinished(true);
|
||||
if (m_isDirty) {
|
||||
|
||||
@@ -72,6 +72,14 @@ public:
|
||||
virtual bool canPreventScreenCapture() const = 0;
|
||||
virtual bool setPreventScreenCapture(QWindow* window, bool allow) const;
|
||||
|
||||
/**
|
||||
* Platform specific secrets storage/handling
|
||||
*/
|
||||
virtual bool saveSecret(const QString& key, const QByteArray& secretData) const = 0;
|
||||
virtual bool getSecret(const QString& key, QByteArray& secretData) const = 0;
|
||||
virtual bool removeSecret(const QString& key) const = 0;
|
||||
virtual bool removeAllSecrets() const = 0;
|
||||
|
||||
signals:
|
||||
void globalShortcutTriggered(const QString& name, const QString& search = {});
|
||||
|
||||
|
||||
@@ -17,14 +17,22 @@
|
||||
*/
|
||||
|
||||
#import "AppKitImpl.h"
|
||||
#import "MacUtils.h"
|
||||
|
||||
#import <QWindow>
|
||||
#import <QMenu>
|
||||
#import <QMenuBar>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <CoreFoundation/CoreFoundation.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <LocalAuthentication/LocalAuthentication.h>
|
||||
#import <Security/Security.h>
|
||||
#if __clang_major__ >= 13 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_12_3
|
||||
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
||||
#endif
|
||||
|
||||
#include "config-keepassx.h"
|
||||
|
||||
@implementation AppKitImpl
|
||||
|
||||
- (id) initWithObject:(AppKit*)appkit
|
||||
@@ -340,3 +348,222 @@ void AppKit::configureWindowAndHelpMenus(QMainWindow* window, QMenu* helpMenu)
|
||||
{
|
||||
[static_cast<id>(self) configureWindowAndHelpMenus:window helpMenu:helpMenu];
|
||||
}
|
||||
|
||||
// Common prefix for saved secrets
|
||||
static const auto s_touchIdKeyPrefix = QStringLiteral("KeepassXC_Keys_");
|
||||
|
||||
// Convert macOS error codes to strings
|
||||
inline std::string StatusToErrorMessage(OSStatus status)
|
||||
{
|
||||
CFStringRef text = SecCopyErrorMessageString(status, NULL);
|
||||
if (!text) {
|
||||
return std::to_string(status);
|
||||
}
|
||||
|
||||
auto msg = CFStringGetCStringPtr(text, kCFStringEncodingUTF8);
|
||||
std::string result;
|
||||
if (msg) {
|
||||
result = msg;
|
||||
}
|
||||
CFRelease(text);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Report status errors if not successful
|
||||
inline void LogStatusError(const char *message, OSStatus status)
|
||||
{
|
||||
if (status) {
|
||||
std::string msg = StatusToErrorMessage(status);
|
||||
qWarning("%s: %s", message, msg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Create an access control object to govern use of the saved secret
|
||||
SecAccessControlRef createAccessControl(bool useTouchId)
|
||||
{
|
||||
// We need both runtime and compile time checks here to solve the following problems:
|
||||
// - Not all flags are available in all OS versions, so we have to check it at compile time
|
||||
// - Requesting Biometry/TouchID/DevicePassword when no fingerprint sensor is available will result in runtime error
|
||||
SecAccessControlCreateFlags accessControlFlags = 0;
|
||||
|
||||
// When TouchID is not enrolled and the flag is set, the method call fails with an error.
|
||||
// We still want to set this flag if TouchID is enrolled but temporarily unavailable due to closed lid
|
||||
//
|
||||
// Sometimes, the enrolled-check does not work, LAErrorBiometryNotAvailable is returned instead of LAErrorBiometryNotEnrolled.
|
||||
// To fallback gracefully, we have to try to save the key a second time without this flag.
|
||||
|
||||
if (useTouchId) {
|
||||
#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
|
||||
// This is the non-deprecated and preferred flag
|
||||
accessControlFlags = kSecAccessControlBiometryCurrentSet;
|
||||
#elif XC_COMPILER_SUPPORT(TOUCH_ID)
|
||||
accessControlFlags = kSecAccessControlTouchIDCurrentSet;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Add support for watch authentication if available
|
||||
#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
|
||||
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch;
|
||||
#endif
|
||||
|
||||
// Check if password fallback is possible and add that as an option
|
||||
#if XC_COMPILER_SUPPORT(TOUCH_ID)
|
||||
if (macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::PasswordFallback)) {
|
||||
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlDevicePasscode;
|
||||
}
|
||||
#endif
|
||||
|
||||
CFErrorRef error = nullptr;
|
||||
auto sacObject = SecAccessControlCreateWithFlags(
|
||||
kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
|
||||
|
||||
if (!sacObject || error) {
|
||||
auto e = static_cast<NSError*>(error);
|
||||
qWarning("MacUtils::saveSecret - Error creating security flags: %s", e.localizedDescription.UTF8String);
|
||||
return nullptr;
|
||||
}
|
||||
return sacObject;
|
||||
}
|
||||
|
||||
bool MacUtils::saveSecret(const QString& key, const QByteArray& secretData) const
|
||||
{
|
||||
const auto keyName = s_touchIdKeyPrefix + key;
|
||||
|
||||
// Delete any existing entry since macOS does not allow overwrite
|
||||
if (!removeSecret(key)) {
|
||||
qWarning("MacUtils::saveSecret - Failed to remove existing secret for key '%s'", qPrintable(key));
|
||||
}
|
||||
|
||||
// Add new entry
|
||||
auto keyBase64 = secretData.toBase64();
|
||||
auto keyValueData = CFDataCreateWithBytesNoCopy(
|
||||
kCFAllocatorDefault, reinterpret_cast<const UInt8*>(keyBase64.data()),
|
||||
keyBase64.length(), kCFAllocatorDefault);
|
||||
|
||||
auto attributes = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(attributes, kSecAttrAccount, static_cast<CFStringRef>(keyName.toNSString()));
|
||||
CFDictionarySetValue(attributes, kSecValueData, keyValueData);
|
||||
CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
|
||||
CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
|
||||
// First, attempt with TouchID enabled
|
||||
CFDictionarySetValue(attributes, kSecAttrAccessControl, createAccessControl(true));
|
||||
|
||||
auto status = SecItemAdd(attributes, nullptr);
|
||||
if (status != errSecSuccess) {
|
||||
qDebug("MacUtils::saveSecret - Failed to save secret with TouchID enabled");
|
||||
// Try again without TouchID enabled
|
||||
CFDictionarySetValue(attributes, kSecAttrAccessControl, createAccessControl(false));
|
||||
status = SecItemAdd(attributes, nullptr);
|
||||
if (status != errSecSuccess) {
|
||||
qWarning("MacUtils::saveSecret - Failed to save secret to keystore");
|
||||
}
|
||||
}
|
||||
|
||||
CFRelease(keyValueData);
|
||||
CFRelease(attributes);
|
||||
|
||||
return status == errSecSuccess;
|
||||
}
|
||||
|
||||
bool MacUtils::getSecret(const QString& key, QByteArray& secretData) const
|
||||
{
|
||||
const auto keyName = s_touchIdKeyPrefix + key;
|
||||
secretData.clear();
|
||||
|
||||
auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(query, kSecAttrAccount, static_cast<CFStringRef>(keyName.toNSString()));
|
||||
CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
|
||||
|
||||
CFTypeRef dataTypeRef = nullptr;
|
||||
auto status = SecItemCopyMatching(query, &dataTypeRef);
|
||||
CFRelease(query);
|
||||
|
||||
if (status == errSecUserCanceled) {
|
||||
// user canceled the authentication, return true with empty key
|
||||
return true;
|
||||
} else if (status != errSecSuccess || !dataTypeRef) {
|
||||
// TODO: Log failure
|
||||
return false;
|
||||
}
|
||||
|
||||
auto valueData = static_cast<CFDataRef>(dataTypeRef);
|
||||
secretData = QByteArray::fromBase64(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)),
|
||||
CFDataGetLength(valueData)));
|
||||
CFRelease(dataTypeRef);
|
||||
|
||||
return !secretData.isEmpty();
|
||||
}
|
||||
|
||||
bool MacUtils::removeSecret(const QString& key) const
|
||||
{
|
||||
const auto keyName = s_touchIdKeyPrefix + key;
|
||||
auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(query, kSecAttrAccount, static_cast<CFStringRef>(keyName.toNSString()));
|
||||
CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
|
||||
// TODO: Log failure to delete?
|
||||
SecItemDelete(query);
|
||||
CFRelease(query);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacUtils::removeAllSecrets() const
|
||||
{
|
||||
auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(query, kSecReturnAttributes, kCFBooleanTrue);
|
||||
CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitAll);
|
||||
|
||||
CFTypeRef result = nullptr;
|
||||
auto status = SecItemCopyMatching(query, &result);
|
||||
if (status == errSecSuccess && result) {
|
||||
for (NSDictionary* item in static_cast<NSArray*>(result)) {
|
||||
NSString* account = item[static_cast<id>(kSecAttrAccount)];
|
||||
if (account && [account hasPrefix:s_touchIdKeyPrefix.toNSString()]) {
|
||||
auto delQuery = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
CFDictionarySetValue(delQuery, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(delQuery, kSecAttrAccount, static_cast<CFStringRef>(account));
|
||||
// TODO: Log failure to delete?
|
||||
SecItemDelete(delQuery);
|
||||
CFRelease(delQuery);
|
||||
}
|
||||
}
|
||||
CFRelease(result);
|
||||
}
|
||||
CFRelease(query);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacUtils::isAuthPolicyAvailable(AuthPolicy policy) const
|
||||
{
|
||||
LAPolicy policyCode;
|
||||
switch (policy) {
|
||||
case AuthPolicy::TouchId:
|
||||
policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
|
||||
break;
|
||||
case AuthPolicy::Watch:
|
||||
policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch;
|
||||
break;
|
||||
case AuthPolicy::PasswordFallback:
|
||||
policyCode = LAPolicyDeviceOwnerAuthentication;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@try {
|
||||
LAContext *context = [[LAContext alloc] init];
|
||||
NSError *error = nil;
|
||||
bool available = [context canEvaluatePolicy:policyCode error:&error];
|
||||
[context release];
|
||||
if (error) {
|
||||
qDebug("MacUtils::isPolicyAvailable - Policy not available: %s", error.localizedDescription.UTF8String);
|
||||
}
|
||||
return available;
|
||||
} @catch (NSException *exception) {
|
||||
qWarning("MacUtils::isPolicyAvailable - Exception occurred: %s", exception.reason.UTF8String);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,21 @@ public:
|
||||
bool canPreventScreenCapture() const override;
|
||||
bool setPreventScreenCapture(QWindow* window, bool prevent) const override;
|
||||
|
||||
// Key management API (TouchID)
|
||||
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
|
||||
bool getSecret(const QString& key, QByteArray& secretData) const override;
|
||||
bool removeSecret(const QString& key) const override;
|
||||
bool removeAllSecrets() const override;
|
||||
|
||||
enum class AuthPolicy
|
||||
{
|
||||
TouchId,
|
||||
Watch,
|
||||
PasswordFallback
|
||||
};
|
||||
|
||||
bool isAuthPolicyAvailable(AuthPolicy policy) const;
|
||||
|
||||
signals:
|
||||
void userSwitched();
|
||||
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
#include <QStandardPaths>
|
||||
#include <QStyle>
|
||||
#include <QTextStream>
|
||||
|
||||
extern "C" {
|
||||
#include <keyutils.h>
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_X11
|
||||
#include <QX11Info>
|
||||
|
||||
@@ -411,3 +416,74 @@ quint64 NixUtils::getProcessStartTime() const
|
||||
qDebug() << "nixutils: failed to find ')' in " << processStatPath;
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
key_serial_t getKeyring()
|
||||
{
|
||||
auto keyring = keyctl_get_persistent(-1, KEY_SPEC_PROCESS_KEYRING);
|
||||
if (keyring == -1) {
|
||||
// Return the non-persistent keyring as a fallback
|
||||
qWarning("nixutils: failed to get persistent keyring: %s", strerror(errno));
|
||||
keyring = KEY_SPEC_PROCESS_KEYRING;
|
||||
}
|
||||
return keyring;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool NixUtils::saveSecret(const QString& key, const QByteArray& secretData) const
|
||||
{
|
||||
auto keyserial =
|
||||
add_key("user", key.toStdString().c_str(), secretData.constData(), secretData.size(), getKeyring());
|
||||
if (keyserial < 0) {
|
||||
qWarning("nixutils: failed to save secret: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
// Only allow this process to read/write this key
|
||||
keyctl_setperm(keyserial, KEY_POS_ALL);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NixUtils::getSecret(const QString& key, QByteArray& secretData) const
|
||||
{
|
||||
secretData.clear();
|
||||
|
||||
auto keyserial = request_key("user", key.toStdString().c_str(), nullptr, getKeyring());
|
||||
if (keyserial < 0) {
|
||||
qWarning("nixutils: failed to find secret: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
secretData.resize(512);
|
||||
auto size = keyctl_read(keyserial, secretData.data(), secretData.size());
|
||||
if (size == -1) {
|
||||
qWarning("nixutils: failed to read secret: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
secretData.resize(size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NixUtils::removeSecret(const QString& key) const
|
||||
{
|
||||
auto keyserial = request_key("user", key.toStdString().c_str(), nullptr, getKeyring());
|
||||
if (keyserial < 0) {
|
||||
qWarning("nixutils: failed to find secret: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyctl_unlink(keyserial, getKeyring()) < 0) {
|
||||
qWarning("nixutils: failed to remove secret: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NixUtils::removeAllSecrets() const
|
||||
{
|
||||
// NixUtils does not support clearing all keys
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,11 @@ public:
|
||||
|
||||
quint64 getProcessStartTime() const;
|
||||
|
||||
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
|
||||
bool getSecret(const QString& key, QByteArray& secretData) const override;
|
||||
bool removeSecret(const QString& key) const override;
|
||||
bool removeAllSecrets() const override;
|
||||
|
||||
private slots:
|
||||
void handleColorSchemeRead(QDBusVariant value);
|
||||
void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value);
|
||||
|
||||
@@ -56,6 +56,7 @@ void DeviceListenerWin::registerHotplugCallback(bool arrived,
|
||||
regex += QString("PID_%1&").arg(productId, 0, 16).toUpper();
|
||||
}
|
||||
}
|
||||
regex += QString(".*$"); // Qt won't match otherwise
|
||||
m_deviceIdMatch = QRegularExpression(regex);
|
||||
|
||||
DEV_BROADCAST_DEVICEINTERFACE_W notificationFilter{
|
||||
@@ -94,7 +95,7 @@ bool DeviceListenerWin::nativeEventFilter(const QByteArray& eventType, void* mes
|
||||
|| (m_handleRemoval && m->wParam == DBT_DEVICEREMOVECOMPLETE)) {
|
||||
const auto pBrHdr = reinterpret_cast<PDEV_BROADCAST_HDR>(m->lParam);
|
||||
const auto pDevIface = reinterpret_cast<PDEV_BROADCAST_DEVICEINTERFACE_W>(pBrHdr);
|
||||
const auto name = QString::fromWCharArray(pDevIface->dbcc_name);
|
||||
const auto name = QString::fromWCharArray(pDevIface->dbcc_name, pDevIface->dbcc_size);
|
||||
if (m_deviceIdMatch.match(name).hasMatch()) {
|
||||
emit devicePlugged(m->wParam == DBT_DEVICEARRIVAL, nullptr, pDevIface);
|
||||
return true;
|
||||
|
||||
@@ -20,11 +20,24 @@
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QUuid>
|
||||
#include <QWindow>
|
||||
|
||||
#include <Windows.h>
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/windows.foundation.collections.h>
|
||||
#include <winrt/windows.security.credentials.h>
|
||||
#undef MessageBox
|
||||
|
||||
using namespace winrt;
|
||||
using namespace Windows::Foundation::Collections;
|
||||
using namespace Windows::Security::Credentials;
|
||||
|
||||
namespace
|
||||
{
|
||||
const std::wstring s_winKeyStoreName{L"keepassxc"};
|
||||
}
|
||||
|
||||
QPointer<WinUtils> WinUtils::m_instance = nullptr;
|
||||
|
||||
WinUtils* WinUtils::instance()
|
||||
@@ -361,3 +374,59 @@ DWORD WinUtils::qtToNativeModifiers(Qt::KeyboardModifiers modifiers)
|
||||
|
||||
return nativeModifiers;
|
||||
}
|
||||
|
||||
bool WinUtils::saveSecret(const QString& key, const QByteArray& secretData) const
|
||||
{
|
||||
try {
|
||||
auto vault = PasswordVault();
|
||||
vault.Add({s_winKeyStoreName,
|
||||
winrt::hstring(key.toStdWString()),
|
||||
winrt::to_hstring(secretData.toBase64().toStdString())});
|
||||
return true;
|
||||
} catch (winrt::hresult_error const&) {
|
||||
qWarning("WinUtils - Failed to add key to password vault");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool WinUtils::getSecret(const QString& key, QByteArray& secretData) const
|
||||
{
|
||||
secretData.clear();
|
||||
try {
|
||||
auto vault = PasswordVault();
|
||||
auto credential = vault.Retrieve(s_winKeyStoreName, winrt::hstring(key.toStdWString()));
|
||||
secretData = QByteArray::fromBase64(QByteArray::fromStdString(winrt::to_string(credential.Password())));
|
||||
} catch (winrt::hresult_error const&) {
|
||||
qWarning("WinUtils - Failed to retrieve key from password vault");
|
||||
return false;
|
||||
}
|
||||
return !secretData.isEmpty();
|
||||
}
|
||||
|
||||
bool WinUtils::removeSecret(const QString& key) const
|
||||
{
|
||||
try {
|
||||
auto vault = PasswordVault();
|
||||
vault.Remove({s_winKeyStoreName, winrt::hstring(key.toStdWString()), L"nodata"});
|
||||
return true;
|
||||
} catch (winrt::hresult_error const&) {
|
||||
qWarning("WinUtils - Failed to clear key from password vault");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool WinUtils::removeAllSecrets() const
|
||||
{
|
||||
auto vault = PasswordVault();
|
||||
auto credentials = vault.FindAllByResource(s_winKeyStoreName);
|
||||
bool allSuccess = true;
|
||||
for (const auto& credential : credentials) {
|
||||
try {
|
||||
vault.Remove(credential);
|
||||
} catch (winrt::hresult_error const&) {
|
||||
qWarning("WinUtils - Failed to clear key from password vault");
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
return allSuccess;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ public:
|
||||
bool canPreventScreenCapture() const override;
|
||||
bool setPreventScreenCapture(QWindow* window, bool prevent) const override;
|
||||
|
||||
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
|
||||
bool getSecret(const QString& key, QByteArray& secretData) const override;
|
||||
bool removeSecret(const QString& key) const override;
|
||||
bool removeAllSecrets() const override;
|
||||
|
||||
protected:
|
||||
explicit WinUtils(QObject* parent = nullptr);
|
||||
~WinUtils() override = default;
|
||||
|
||||
@@ -213,7 +213,7 @@ namespace KeeShareSettings
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qDebug("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
|
||||
qWarning("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
@@ -253,7 +253,7 @@ namespace KeeShareSettings
|
||||
} else if (reader.name() == "PublicKey") {
|
||||
own.certificate = Certificate::deserialize(reader);
|
||||
} else {
|
||||
qDebug("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
|
||||
qWarning("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
@@ -262,7 +262,8 @@ namespace KeeShareSettings
|
||||
}
|
||||
|
||||
Reference::Reference()
|
||||
: uuid(QUuid::createUuid())
|
||||
: type(Inactive)
|
||||
, uuid(QUuid::createUuid())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -319,21 +320,12 @@ namespace KeeShareSettings
|
||||
writer.writeStartElement("Password");
|
||||
writer.writeCharacters(reference.password.toUtf8().toBase64());
|
||||
writer.writeEndElement();
|
||||
writer.writeStartElement("KeepGroups");
|
||||
writer.writeCharacters(reference.keepGroups ? "True" : "False");
|
||||
writer.writeEndElement();
|
||||
});
|
||||
}
|
||||
|
||||
Reference Reference::deserialize(const QString& raw)
|
||||
{
|
||||
if (raw.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Reference reference;
|
||||
// If KeepGroups is not present, default to false for backward compatibility
|
||||
reference.keepGroups = false;
|
||||
xmlDeserialize(raw, [&](QXmlStreamReader& reader) {
|
||||
while (!reader.error() && reader.readNextStartElement()) {
|
||||
if (reader.name() == "Type") {
|
||||
@@ -354,10 +346,8 @@ namespace KeeShareSettings
|
||||
reference.path = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1()));
|
||||
} else if (reader.name() == "Password") {
|
||||
reference.password = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1()));
|
||||
} else if (reader.name() == "KeepGroups") {
|
||||
reference.keepGroups = reader.readElementText().compare("True") == 0;
|
||||
} else {
|
||||
qDebug("Unknown Reference element %s", qPrintable(reader.name().toString()));
|
||||
qWarning("Unknown Reference element %s", qPrintable(reader.name().toString()));
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
@@ -373,11 +363,7 @@ namespace KeeShareSettings
|
||||
|
||||
// Extract RSA key data to serialize an ssh-rsa public key.
|
||||
// ssh-rsa keys are currently not built into Botan
|
||||
// need a dynamic_cast here, because the base class is virtual
|
||||
const auto rsaKey = dynamic_cast<Botan::RSA_PrivateKey*>(sign.certificate.key.data());
|
||||
if (!rsaKey) {
|
||||
return {};
|
||||
}
|
||||
const auto rsaKey = static_cast<Botan::RSA_PrivateKey*>(sign.certificate.key.data());
|
||||
|
||||
std::vector<uint8_t> rsaE(rsaKey->get_e().bytes());
|
||||
rsaKey->get_e().binary_encode(rsaE.data());
|
||||
|
||||
@@ -122,11 +122,10 @@ namespace KeeShareSettings
|
||||
|
||||
struct Reference
|
||||
{
|
||||
Type type = Inactive;
|
||||
Type type;
|
||||
QUuid uuid;
|
||||
QString path;
|
||||
QString password;
|
||||
bool keepGroups = true;
|
||||
|
||||
Reference();
|
||||
bool isNull() const;
|
||||
|
||||
@@ -62,39 +62,6 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void cloneIcon(Metadata* targetMetadata, const Database* sourceDb, const QUuid& iconUuid)
|
||||
{
|
||||
if (!iconUuid.isNull() && !targetMetadata->hasCustomIcon(iconUuid)) {
|
||||
targetMetadata->addCustomIcon(iconUuid, sourceDb->metadata()->customIcon(iconUuid));
|
||||
}
|
||||
}
|
||||
|
||||
void cloneEntries(Metadata* targetMetadata, const Group* sourceGroup, Group* targetGroup)
|
||||
{
|
||||
for (const Entry* sourceEntry : sourceGroup->entries()) {
|
||||
auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
|
||||
const bool updateTimeinfoEntry = targetEntry->canUpdateTimeinfo();
|
||||
targetEntry->setUpdateTimeinfo(false);
|
||||
targetEntry->setGroup(targetGroup);
|
||||
targetEntry->setUpdateTimeinfo(updateTimeinfoEntry);
|
||||
cloneIcon(targetMetadata, sourceEntry->database(), targetEntry->iconUuid());
|
||||
}
|
||||
}
|
||||
|
||||
void cloneChildren(Metadata* targetMetadata, const Group* sourceRoot, Group* targetRoot)
|
||||
{
|
||||
for (const Group* sourceGroup : sourceRoot->children()) {
|
||||
auto* targetGroup = sourceGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
|
||||
const bool updateTimeinfo = targetGroup->canUpdateTimeinfo();
|
||||
targetGroup->setUpdateTimeinfo(false);
|
||||
targetGroup->setParent(targetRoot);
|
||||
targetGroup->setUpdateTimeinfo(updateTimeinfo);
|
||||
cloneIcon(targetMetadata, sourceRoot->database(), targetGroup->iconUuid());
|
||||
cloneEntries(targetMetadata, sourceGroup, targetGroup);
|
||||
cloneChildren(targetMetadata, sourceGroup, targetGroup);
|
||||
}
|
||||
}
|
||||
|
||||
Database* extractIntoDatabase(const KeeShareSettings::Reference& reference, const Group* sourceRoot)
|
||||
{
|
||||
const auto* sourceDb = sourceRoot->database();
|
||||
@@ -108,10 +75,17 @@ namespace
|
||||
targetRoot->setUpdateTimeinfo(false);
|
||||
KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference());
|
||||
targetRoot->setUpdateTimeinfo(updateTimeinfo);
|
||||
cloneIcon(targetMetadata, sourceRoot->database(), targetRoot->iconUuid());
|
||||
cloneEntries(targetMetadata, sourceRoot, targetRoot);
|
||||
if (reference.keepGroups) {
|
||||
cloneChildren(targetMetadata, sourceRoot, targetRoot);
|
||||
const auto sourceEntries = sourceRoot->entriesRecursive(false);
|
||||
for (const Entry* sourceEntry : sourceEntries) {
|
||||
auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
|
||||
const bool updateTimeinfoEntry = targetEntry->canUpdateTimeinfo();
|
||||
targetEntry->setUpdateTimeinfo(false);
|
||||
targetEntry->setGroup(targetRoot);
|
||||
targetEntry->setUpdateTimeinfo(updateTimeinfoEntry);
|
||||
const auto iconUuid = targetEntry->iconUuid();
|
||||
if (!iconUuid.isNull() && !targetMetadata->hasCustomIcon(iconUuid)) {
|
||||
targetMetadata->addCustomIcon(iconUuid, sourceEntry->database()->metadata()->customIcon(iconUuid));
|
||||
}
|
||||
}
|
||||
|
||||
auto key = QSharedPointer<CompositeKey>::create();
|
||||
|
||||
@@ -43,7 +43,6 @@ EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent)
|
||||
connect(m_ui->pathEdit, SIGNAL(editingFinished()), SLOT(selectPath()));
|
||||
connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog()));
|
||||
connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType()));
|
||||
connect(m_ui->keepGroupsCheckbox, SIGNAL(toggled(bool)), SLOT(keepGroupsToggled(bool)));
|
||||
connect(m_ui->clearButton, SIGNAL(clicked(bool)), SLOT(clearInputs()));
|
||||
|
||||
connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(updateSharingState()));
|
||||
@@ -98,7 +97,6 @@ void EditGroupWidgetKeeShare::updateSharingState()
|
||||
m_ui->pathEdit->setEnabled(isEnabled);
|
||||
m_ui->pathSelectionButton->setEnabled(isEnabled);
|
||||
m_ui->passwordEdit->setEnabled(isEnabled);
|
||||
m_ui->keepGroupsCheckbox->setEnabled(isEnabled);
|
||||
|
||||
if (!m_temporaryGroup || !isEnabled) {
|
||||
m_ui->messageWidget->hideMessage();
|
||||
@@ -190,7 +188,6 @@ void EditGroupWidgetKeeShare::update()
|
||||
m_ui->typeComboBox->setCurrentIndex(reference.type);
|
||||
m_ui->passwordEdit->setText(reference.password);
|
||||
m_ui->pathEdit->setText(reference.path);
|
||||
m_ui->keepGroupsCheckbox->setChecked(reference.keepGroups);
|
||||
}
|
||||
|
||||
updateSharingState();
|
||||
@@ -294,13 +291,3 @@ void EditGroupWidgetKeeShare::selectType()
|
||||
|
||||
updateSharingState();
|
||||
}
|
||||
|
||||
void EditGroupWidgetKeeShare::keepGroupsToggled(bool toggled)
|
||||
{
|
||||
if (!m_temporaryGroup) {
|
||||
return;
|
||||
}
|
||||
auto reference = KeeShare::referenceOf(m_temporaryGroup);
|
||||
reference.keepGroups = toggled;
|
||||
KeeShare::setReferenceTo(m_temporaryGroup, reference);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ private slots:
|
||||
void selectPassword();
|
||||
void launchPathSelectionDialog();
|
||||
void selectPath();
|
||||
void keepGroupsToggled(bool);
|
||||
|
||||
private:
|
||||
QScopedPointer<Ui::EditGroupWidgetKeeShare> m_ui;
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
@@ -171,7 +171,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="4" column="0">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@@ -184,19 +184,6 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QCheckBox" name="keepGroupsCheckbox">
|
||||
<property name="toolTip">
|
||||
<string>Maintain group structure with shared database</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Keep Group Structure</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
||||
@@ -73,29 +73,31 @@ bool YubiKey::isInitialized()
|
||||
|
||||
bool YubiKey::findValidKeys()
|
||||
{
|
||||
// Block operations on hardware keys while scanning
|
||||
QMutexLocker lock(&s_interfaceMutex);
|
||||
|
||||
m_connectedKeys = 0;
|
||||
m_findingKeys = true;
|
||||
m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys(m_connectedKeys);
|
||||
m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys(m_connectedKeys);
|
||||
m_findingKeys = false;
|
||||
findValidKeys(lock);
|
||||
|
||||
return !m_usbKeys.isEmpty() || !m_pcscKeys.isEmpty();
|
||||
}
|
||||
|
||||
void YubiKey::findValidKeys(const QMutexLocker& locker)
|
||||
{
|
||||
// Check QMutexLocker since version 6.4
|
||||
Q_UNUSED(locker);
|
||||
|
||||
m_connectedKeys = 0;
|
||||
m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys(m_connectedKeys);
|
||||
m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys(m_connectedKeys);
|
||||
}
|
||||
|
||||
void YubiKey::findValidKeysAsync()
|
||||
{
|
||||
// Don't start another scan if we are already doing one
|
||||
if (!m_findingKeys) {
|
||||
m_findingKeys = true;
|
||||
QtConcurrent::run([this] { emit detectComplete(findValidKeys()); });
|
||||
}
|
||||
QtConcurrent::run([this] { emit detectComplete(findValidKeys()); });
|
||||
}
|
||||
|
||||
YubiKey::KeyMap YubiKey::foundKeys()
|
||||
{
|
||||
QMutexLocker lock(&s_interfaceMutex);
|
||||
KeyMap foundKeys = m_usbKeys;
|
||||
foundKeys.unite(m_pcscKeys);
|
||||
|
||||
@@ -104,12 +106,38 @@ YubiKey::KeyMap YubiKey::foundKeys()
|
||||
|
||||
int YubiKey::connectedKeys()
|
||||
{
|
||||
QMutexLocker lock(&s_interfaceMutex);
|
||||
|
||||
return m_connectedKeys;
|
||||
}
|
||||
|
||||
QString YubiKey::errorMessage()
|
||||
{
|
||||
return m_error;
|
||||
QMutexLocker lock(&s_interfaceMutex);
|
||||
|
||||
QString error;
|
||||
error.clear();
|
||||
if (!m_error.isNull()) {
|
||||
error += tr("General: ") + m_error;
|
||||
}
|
||||
|
||||
QString usb_error = YubiKeyInterfaceUSB::instance()->errorMessage();
|
||||
if (!usb_error.isNull()) {
|
||||
if (!error.isNull()) {
|
||||
error += " | ";
|
||||
}
|
||||
error += "USB: " + usb_error;
|
||||
}
|
||||
|
||||
QString pcsc_error = YubiKeyInterfacePCSC::instance()->errorMessage();
|
||||
if (!pcsc_error.isNull()) {
|
||||
if (!error.isNull()) {
|
||||
error += " | ";
|
||||
}
|
||||
error += "PCSC: " + pcsc_error;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,31 +175,25 @@ bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
|
||||
YubiKey::ChallengeResult
|
||||
YubiKey::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
|
||||
{
|
||||
m_error.clear();
|
||||
|
||||
// Prevent re-entrant access to hardware keys
|
||||
QMutexLocker lock(&s_interfaceMutex);
|
||||
|
||||
// Try finding key on the USB interface first
|
||||
auto ret = YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
|
||||
if (ret == ChallengeResult::YCR_ERROR) {
|
||||
m_error = YubiKeyInterfaceUSB::instance()->errorMessage();
|
||||
return ret;
|
||||
m_error.clear();
|
||||
|
||||
// Make sure we tried to find available keys
|
||||
if (m_usbKeys.isEmpty() && m_pcscKeys.isEmpty()) {
|
||||
findValidKeys(lock);
|
||||
}
|
||||
|
||||
// If a USB key was not found, try PC/SC interface
|
||||
if (ret == ChallengeResult::YCR_KEYNOTFOUND) {
|
||||
ret = YubiKeyInterfacePCSC::instance()->challenge(slot, challenge, response);
|
||||
if (ret == ChallengeResult::YCR_ERROR) {
|
||||
m_error = YubiKeyInterfacePCSC::instance()->errorMessage();
|
||||
return ret;
|
||||
}
|
||||
if (m_usbKeys.contains(slot)) {
|
||||
return YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
|
||||
}
|
||||
|
||||
if (ret == ChallengeResult::YCR_KEYNOTFOUND) {
|
||||
m_error =
|
||||
tr("Could not find hardware key with serial number %1. Please connect it to continue.").arg(slot.first);
|
||||
if (m_pcscKeys.contains(slot)) {
|
||||
return YubiKeyInterfacePCSC::instance()->challenge(slot, challenge, response);
|
||||
}
|
||||
|
||||
return ret;
|
||||
m_error = tr("Could not find interface for hardware key with serial number %1. Please connect it to continue.")
|
||||
.arg(slot.first);
|
||||
|
||||
return ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
@@ -44,8 +44,7 @@ public:
|
||||
{
|
||||
YCR_ERROR = 0,
|
||||
YCR_SUCCESS = 1,
|
||||
YCR_WOULDBLOCK = 2,
|
||||
YCR_KEYNOTFOUND = 3,
|
||||
YCR_WOULDBLOCK = 2
|
||||
};
|
||||
|
||||
static YubiKey* instance();
|
||||
@@ -85,14 +84,14 @@ signals:
|
||||
private:
|
||||
explicit YubiKey();
|
||||
|
||||
void findValidKeys(const QMutexLocker& locker);
|
||||
|
||||
static YubiKey* m_instance;
|
||||
|
||||
QTimer m_interactionTimer;
|
||||
bool m_initialized = false;
|
||||
bool m_findingKeys = false;
|
||||
QString m_error;
|
||||
|
||||
// Prevents multiple simultaneous operations on hardware keys
|
||||
static QMutex s_interfaceMutex;
|
||||
|
||||
KeyMap m_usbKeys;
|
||||
|
||||
@@ -679,7 +679,7 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
|
||||
m_error.clear();
|
||||
if (!m_initialized) {
|
||||
m_error = tr("The YubiKey PC/SC interface has not been initialized.");
|
||||
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
// Try for a few seconds to find the key
|
||||
@@ -710,8 +710,11 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
|
||||
}
|
||||
}
|
||||
|
||||
m_error = tr("Could not find or access hardware key with serial number %1. Please present it to continue. ")
|
||||
.arg(slot.first)
|
||||
+ m_error;
|
||||
emit challengeCompleted();
|
||||
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
YubiKey::ChallengeResult YubiKeyInterfacePCSC::performChallenge(void* key,
|
||||
|
||||
@@ -237,7 +237,7 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
|
||||
m_error.clear();
|
||||
if (!m_initialized) {
|
||||
m_error = tr("The YubiKey USB interface has not been initialized.");
|
||||
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
auto yk_key = openKeySerial(slot.first);
|
||||
@@ -245,11 +245,12 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
|
||||
// Key with specified serial number is not connected
|
||||
m_error =
|
||||
tr("Could not find hardware key with serial number %1. Please plug it in to continue.").arg(slot.first);
|
||||
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
emit challengeStarted();
|
||||
auto ret = performChallenge(yk_key.get(), slot.second, true, challenge, response);
|
||||
|
||||
emit challengeCompleted();
|
||||
|
||||
return ret;
|
||||
|
||||
207
src/quickunlock/PinUnlock.cpp
Normal file
207
src/quickunlock/PinUnlock.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "PinUnlock.h"
|
||||
|
||||
#include "crypto/CryptoHash.h"
|
||||
#include "crypto/Random.h"
|
||||
#include "crypto/SymmetricCipher.h"
|
||||
#include "crypto/kdf/Argon2Kdf.h"
|
||||
#include "gui/osutils/OSUtils.h"
|
||||
|
||||
#include <QInputDialog>
|
||||
#include <QRegularExpression>
|
||||
|
||||
const int PinUnlock::MIN_PIN_LENGTH = 6;
|
||||
const int PinUnlock::MAX_PIN_LENGTH = 10;
|
||||
const int PinUnlock::MAX_PIN_ATTEMPTS = 3;
|
||||
|
||||
bool PinUnlock::isAvailable() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PinUnlock::promptPin(int attempt, QByteArray& sessionKey)
|
||||
{
|
||||
QString pin;
|
||||
|
||||
if (attempt == 0) {
|
||||
// Loop until a valid pin has been entered or canceled
|
||||
QRegularExpression pinRegex("^\\d+$");
|
||||
while (true) {
|
||||
bool ok = false;
|
||||
pin = QInputDialog::getText(
|
||||
nullptr,
|
||||
QObject::tr("Quick Unlock Pin Entry"),
|
||||
QObject::tr("Enter a %1–%2 digit pin to use for quick unlock:").arg(MIN_PIN_LENGTH).arg(MAX_PIN_LENGTH),
|
||||
QLineEdit::Password,
|
||||
{},
|
||||
&ok);
|
||||
|
||||
if (!ok) {
|
||||
m_error = QObject::tr("Pin setup was canceled. Quick unlock has not been enabled.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate pin criteria
|
||||
if (pin.length() >= MIN_PIN_LENGTH && pin.length() <= MAX_PIN_LENGTH && pinRegex.match(pin).hasMatch()) {
|
||||
// Pin is valid, move to hashing
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bool ok = false;
|
||||
pin = QInputDialog::getText(
|
||||
nullptr,
|
||||
QObject::tr("Quick Unlock Pin Entry"),
|
||||
QObject::tr("Enter quick unlock pin (%1 of %2 attempts):").arg(attempt).arg(MAX_PIN_ATTEMPTS),
|
||||
QLineEdit::Password,
|
||||
{},
|
||||
&ok);
|
||||
|
||||
if (!ok) {
|
||||
// User canceled the pin entry dialog, record pin attempts
|
||||
m_error = QObject::tr("Pin entry was canceled.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the pin then run it through Argon2 to derive the encryption key
|
||||
sessionKey.fill('\0', 32);
|
||||
Argon2Kdf kdf(Argon2Kdf::Type::Argon2id);
|
||||
CryptoHash hash(CryptoHash::Sha256);
|
||||
hash.addData(pin.toLatin1());
|
||||
if (!kdf.transform(hash.result(), sessionKey)) {
|
||||
m_error = QObject::tr("Failed to derive key using Argon2");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data)
|
||||
{
|
||||
QByteArray key;
|
||||
if (!promptPin(0, key)) {
|
||||
// Pin entry was canceled or failed, error set by promptPin
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate a random IV
|
||||
const auto iv = Random::instance()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
|
||||
|
||||
// Encrypt the data using AES-256-GCM
|
||||
SymmetricCipher cipher;
|
||||
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) {
|
||||
m_error = QObject::tr("Failed to init KeePassXC crypto.");
|
||||
return false;
|
||||
}
|
||||
QByteArray encrypted = data;
|
||||
if (!cipher.finish(encrypted)) {
|
||||
m_error = QObject::tr("Failed to encrypt key data.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store the encrypted data
|
||||
saveKey(dbUuid, encrypted.prepend(iv));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PinUnlock::getKey(const QUuid& dbUuid, QByteArray& data)
|
||||
{
|
||||
data.clear();
|
||||
bool hasSecret = m_encryptedKeys.contains(dbUuid);
|
||||
if (!hasSecret) {
|
||||
// Check if the OS has a secret stored for this database UUID
|
||||
QByteArray tmp;
|
||||
if (osUtils->getSecret(dbUuid.toString(), tmp)) {
|
||||
// Cache the secret in memory
|
||||
m_encryptedKeys.insert(dbUuid, qMakePair(1, tmp));
|
||||
} else {
|
||||
m_error = QObject::tr("Failed to get credentials for quick unlock.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Restrict pin attempts per database
|
||||
const auto& pairData = m_encryptedKeys.value(dbUuid);
|
||||
for (int pinAttempts = pairData.first; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) {
|
||||
QByteArray key;
|
||||
if (!promptPin(pinAttempts, key)) {
|
||||
// Pin entry was canceled or failed, error set by promptPin
|
||||
m_encryptedKeys.insert(dbUuid, qMakePair(pinAttempts, pairData.second));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the previously used challenge and encrypted data
|
||||
const auto& ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
|
||||
const auto& iv = pairData.second.left(ivSize);
|
||||
|
||||
// Decrypt the data using the generated key and IV from above
|
||||
SymmetricCipher cipher;
|
||||
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
|
||||
m_error = QObject::tr("Failed to init KeePassXC crypto.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attempt to decrypt the key data
|
||||
data = pairData.second.mid(ivSize);
|
||||
if (cipher.finish(data)) {
|
||||
// Decryption succeeded, reset the pin attempts
|
||||
m_encryptedKeys.insert(dbUuid, qMakePair(1, pairData.second));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
data.clear();
|
||||
m_error = QObject::tr("Too many pin attempts.");
|
||||
reset(dbUuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
void PinUnlock::saveKey(const QUuid& dbUuid, const QByteArray& data)
|
||||
{
|
||||
// Save the key to the OS secret store
|
||||
if (!osUtils->saveSecret(dbUuid.toString(), data)) {
|
||||
qWarning("PinUnlock - Failed to save quick unlock credentials.");
|
||||
}
|
||||
// Store the encrypted key in memory
|
||||
m_encryptedKeys.insert(dbUuid, qMakePair(1, data));
|
||||
}
|
||||
|
||||
bool PinUnlock::hasKey(const QUuid& dbUuid) const
|
||||
{
|
||||
bool hasSecret = m_encryptedKeys.contains(dbUuid);
|
||||
if (!hasSecret) {
|
||||
// Check if the OS has a secret stored for this database UUID
|
||||
QByteArray tmp;
|
||||
hasSecret = osUtils->getSecret(dbUuid.toString(), tmp);
|
||||
}
|
||||
return hasSecret;
|
||||
}
|
||||
|
||||
void PinUnlock::reset(const QUuid& dbUuid)
|
||||
{
|
||||
m_encryptedKeys.remove(dbUuid);
|
||||
osUtils->removeSecret(dbUuid.toString());
|
||||
}
|
||||
|
||||
void PinUnlock::reset()
|
||||
{
|
||||
m_encryptedKeys.clear();
|
||||
osUtils->removeAllSecrets();
|
||||
}
|
||||
54
src/quickunlock/PinUnlock.h
Normal file
54
src/quickunlock/PinUnlock.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_PINUNLOCK_H
|
||||
#define KEEPASSXC_PINUNLOCK_H
|
||||
|
||||
#include "QuickUnlockInterface.h"
|
||||
|
||||
#include <QHash>
|
||||
|
||||
class PinUnlock : public QuickUnlockInterface
|
||||
{
|
||||
public:
|
||||
PinUnlock() = default;
|
||||
|
||||
bool isAvailable() const override;
|
||||
|
||||
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
|
||||
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
|
||||
bool hasKey(const QUuid& dbUuid) const override;
|
||||
|
||||
void reset(const QUuid& dbUuid) override;
|
||||
void reset() override;
|
||||
|
||||
static const int MIN_PIN_LENGTH;
|
||||
static const int MAX_PIN_LENGTH;
|
||||
static const int MAX_PIN_ATTEMPTS;
|
||||
|
||||
protected:
|
||||
bool promptPin(int attempt, QByteArray& sessionKey);
|
||||
|
||||
private:
|
||||
void saveKey(const QUuid& dbUuid, const QByteArray& key);
|
||||
|
||||
QHash<QUuid, QPair<int, QByteArray>> m_encryptedKeys;
|
||||
|
||||
Q_DISABLE_COPY(PinUnlock)
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_PINUNLOCK_H
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
* 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
|
||||
@@ -23,8 +23,8 @@
|
||||
#include "gui/osutils/nixutils/NixUtils.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QtDBus>
|
||||
|
||||
#include <botan/mem_ops.h>
|
||||
#include <cerrno>
|
||||
|
||||
@@ -35,19 +35,11 @@ extern "C" {
|
||||
const QString polkit_service = "org.freedesktop.PolicyKit1";
|
||||
const QString polkit_object = "/org/freedesktop/PolicyKit1/Authority";
|
||||
|
||||
namespace
|
||||
{
|
||||
QString getKeyName(const QUuid& dbUuid)
|
||||
{
|
||||
static const QString keyPrefix = "keepassxc_polkit_keys_";
|
||||
return keyPrefix + dbUuid.toString();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Polkit::Polkit()
|
||||
{
|
||||
PolkitSubject::registerMetaType();
|
||||
PolkitAuthorizationResults::registerMetaType();
|
||||
PolkitActionDescription::registerMetaType();
|
||||
|
||||
/* Note we explicitly use our own dbus path here, as the ::systemBus() method could be overridden
|
||||
through an environment variable to return an alternative bus path. This bus could have an application
|
||||
@@ -61,18 +53,34 @@ Polkit::Polkit()
|
||||
|
||||
m_available = bus.isConnected();
|
||||
if (!m_available) {
|
||||
qDebug() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)";
|
||||
qWarning() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)";
|
||||
return;
|
||||
}
|
||||
|
||||
m_available = bus.interface()->isServiceRegistered(polkit_service);
|
||||
|
||||
if (!m_available) {
|
||||
qDebug() << "polkit: Polkit is not registered on dbus";
|
||||
qWarning() << "polkit: Polkit is not registered on dbus";
|
||||
return;
|
||||
}
|
||||
|
||||
// Initiate the Polkit dbus interface
|
||||
m_polkit.reset(new org::freedesktop::PolicyKit1::Authority(polkit_service, polkit_object, bus));
|
||||
|
||||
// Reset available state and check Polkit registered actions for KeePassXC
|
||||
m_available = false;
|
||||
auto kpxcAction = QStringLiteral("org.keepassxc.KeePassXC.unlockDatabase");
|
||||
auto actions = m_polkit->EnumerateActions("");
|
||||
for (const auto& action : actions.value()) {
|
||||
if (action.actionId == kpxcAction) {
|
||||
m_available = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_available) {
|
||||
qWarning() << "polkit: KeePassXC Polkit action is not installed";
|
||||
}
|
||||
}
|
||||
|
||||
Polkit::~Polkit()
|
||||
@@ -81,7 +89,8 @@ Polkit::~Polkit()
|
||||
|
||||
void Polkit::reset(const QUuid& dbUuid)
|
||||
{
|
||||
m_encryptedMasterKeys.remove(dbUuid);
|
||||
m_sessionKeys.remove(dbUuid);
|
||||
nixUtils()->removeSecret(dbUuid.toString());
|
||||
}
|
||||
|
||||
bool Polkit::isAvailable() const
|
||||
@@ -89,67 +98,100 @@ bool Polkit::isAvailable() const
|
||||
return m_available;
|
||||
}
|
||||
|
||||
QString Polkit::errorString() const
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
void Polkit::reset()
|
||||
{
|
||||
m_encryptedMasterKeys.clear();
|
||||
m_sessionKeys.clear();
|
||||
nixUtils()->removeAllSecrets();
|
||||
}
|
||||
|
||||
bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key)
|
||||
bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& data)
|
||||
{
|
||||
reset(dbUuid);
|
||||
|
||||
// Generate a random iv/key pair to encrypt the master password with
|
||||
QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
|
||||
QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
|
||||
QByteArray keychainKeyValue = randomKey + randomIV;
|
||||
// Prompt for a pin to use as session key
|
||||
QByteArray key;
|
||||
if (!promptPin(0, key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto iv = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
|
||||
|
||||
SymmetricCipher aes256Encrypt;
|
||||
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
|
||||
m_error = QObject::tr("AES initialization failed");
|
||||
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) {
|
||||
m_error = QObject::tr("Failed to init KeePassXC crypto.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Encrypt the master password
|
||||
QByteArray encryptedMasterKey = key;
|
||||
if (!aes256Encrypt.finish(encryptedMasterKey)) {
|
||||
m_error = QObject::tr("AES encrypt failed");
|
||||
qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString();
|
||||
// Encrypt the database key
|
||||
QByteArray encrypted = data;
|
||||
if (!aes256Encrypt.finish(encrypted)) {
|
||||
m_error = QObject::tr("Failed to encrypt key data.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the iv/key pair into the linux keyring
|
||||
key_serial_t key_serial = add_key("user",
|
||||
getKeyName(dbUuid).toStdString().c_str(),
|
||||
keychainKeyValue.constData(),
|
||||
keychainKeyValue.size(),
|
||||
KEY_SPEC_PROCESS_KEYRING);
|
||||
if (key_serial < 0) {
|
||||
m_error = QObject::tr("Failed to store in Linux Keyring");
|
||||
qDebug() << "polkit keyring failed to store: " << errno;
|
||||
return false;
|
||||
}
|
||||
// Store the session key and save the encrypted master key to the keyring
|
||||
m_sessionKeys.insert(dbUuid, key);
|
||||
nixUtils()->saveSecret(dbUuid.toString(), encrypted.prepend(iv));
|
||||
|
||||
// Scrub the keys from ram
|
||||
Botan::secure_scrub_memory(randomKey.data(), randomKey.size());
|
||||
Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
|
||||
Botan::secure_scrub_memory(keychainKeyValue.data(), keychainKeyValue.size());
|
||||
|
||||
// Store encrypted master password and return
|
||||
m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
|
||||
bool Polkit::getKey(const QUuid& dbUuid, QByteArray& data)
|
||||
{
|
||||
if (!m_polkit || !hasKey(dbUuid)) {
|
||||
if (!m_available || !hasKey(dbUuid)) {
|
||||
m_error = QObject::tr("No key is stored for this database.");
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray key;
|
||||
for (int pinAttempts = 1; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) {
|
||||
if (!m_sessionKeys.contains(dbUuid)) {
|
||||
// Request pin to obtain a session key
|
||||
if (!promptPin(pinAttempts, key)) {
|
||||
m_error = QObject::tr("Failed to obtain session key.");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// We already have the session key, prompt using polkit to authorize use
|
||||
if (!promptPolkit()) {
|
||||
// Error set in promptPolkit call
|
||||
return false;
|
||||
}
|
||||
key = m_sessionKeys.value(dbUuid);
|
||||
}
|
||||
|
||||
// Retrieve the encrypted master key from the OS secret store
|
||||
QByteArray encData;
|
||||
if (!nixUtils()->getSecret(dbUuid.toString(), encData)) {
|
||||
m_error = QObject::tr("Failed to get credentials for quick unlock.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
|
||||
const auto& iv = encData.left(ivSize);
|
||||
|
||||
// Decrypt the data using the generated key and IV from above
|
||||
SymmetricCipher cipher;
|
||||
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
|
||||
m_error = QObject::tr("Failed to init KeePassXC crypto.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attempt to decrypt the key data
|
||||
data = encData.mid(ivSize);
|
||||
if (cipher.finish(data)) {
|
||||
// Decryption succeeded, store the session key used
|
||||
m_sessionKeys.insert(dbUuid, key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
m_error = QObject::tr("Too many pin attempts.");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Polkit::promptPolkit()
|
||||
{
|
||||
PolkitSubject subject;
|
||||
subject.kind = "unix-process";
|
||||
subject.details.insert("pid", static_cast<uint>(QCoreApplication::applicationPid()));
|
||||
@@ -170,78 +212,26 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
|
||||
if (result.isError()) {
|
||||
auto msg = result.error().message();
|
||||
m_error = QObject::tr("Polkit returned an error: %1").arg(msg);
|
||||
qDebug() << "polkit returned an error: " << msg;
|
||||
return false;
|
||||
}
|
||||
|
||||
PolkitAuthorizationResults authResult = result.value();
|
||||
if (authResult.is_authorized) {
|
||||
QByteArray encryptedMasterKey = m_encryptedMasterKeys.value(dbUuid);
|
||||
key_serial_t keySerial =
|
||||
find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING);
|
||||
|
||||
if (keySerial == -1) {
|
||||
m_error = QObject::tr("Could not locate key in keyring");
|
||||
qDebug() << "polkit keyring failed to find: " << errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
void* keychainBuffer;
|
||||
long keychainDataSize = keyctl_read_alloc(keySerial, &keychainBuffer);
|
||||
|
||||
if (keychainDataSize == -1) {
|
||||
m_error = QObject::tr("Could not read key in keyring");
|
||||
qDebug() << "polkit keyring failed to read: " << errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray keychainBytes(static_cast<const char*>(keychainBuffer), keychainDataSize);
|
||||
|
||||
Botan::secure_scrub_memory(keychainBuffer, keychainDataSize);
|
||||
free(keychainBuffer);
|
||||
|
||||
QByteArray keychainKey = keychainBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
|
||||
QByteArray keychainIv = keychainBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
|
||||
|
||||
SymmetricCipher aes256Decrypt;
|
||||
if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, keychainKey, keychainIv)) {
|
||||
m_error = QObject::tr("AES initialization failed");
|
||||
qDebug() << "polkit aes init failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
key = encryptedMasterKey;
|
||||
if (!aes256Decrypt.finish(key)) {
|
||||
key.clear();
|
||||
m_error = QObject::tr("AES decrypt failed");
|
||||
qDebug() << "polkit aes decrypt failed: " << aes256Decrypt.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scrub the keys from ram
|
||||
Botan::secure_scrub_memory(keychainKey.data(), keychainKey.size());
|
||||
Botan::secure_scrub_memory(keychainIv.data(), keychainIv.size());
|
||||
Botan::secure_scrub_memory(keychainBytes.data(), keychainBytes.size());
|
||||
Botan::secure_scrub_memory(encryptedMasterKey.data(), encryptedMasterKey.size());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Failed to authenticate
|
||||
if (authResult.is_challenge) {
|
||||
m_error = QObject::tr("No Polkit authentication agent was available");
|
||||
m_error = QObject::tr("No Polkit authentication agent was available.");
|
||||
} else {
|
||||
m_error = QObject::tr("Polkit authorization failed");
|
||||
m_error = QObject::tr("Polkit authorization failed.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Polkit::hasKey(const QUuid& dbUuid) const
|
||||
{
|
||||
if (!m_encryptedMasterKeys.contains(dbUuid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING) != -1;
|
||||
// Check if the OS has a secret stored for this database UUID
|
||||
QByteArray tmp;
|
||||
return nixUtils()->getSecret(dbUuid.toString(), tmp);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
* 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
|
||||
@@ -15,36 +15,34 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSX_POLKIT_H
|
||||
#define KEEPASSX_POLKIT_H
|
||||
#pragma once
|
||||
|
||||
#include "QuickUnlockInterface.h"
|
||||
#include "PinUnlock.h"
|
||||
#include "polkit_dbus.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QScopedPointer>
|
||||
|
||||
class Polkit : public QuickUnlockInterface
|
||||
class Polkit : public PinUnlock
|
||||
{
|
||||
public:
|
||||
Polkit();
|
||||
~Polkit() override;
|
||||
|
||||
bool isAvailable() const override;
|
||||
QString errorString() const override;
|
||||
|
||||
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
|
||||
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
|
||||
bool setKey(const QUuid& dbUuid, const QByteArray& data) override;
|
||||
bool getKey(const QUuid& dbUuid, QByteArray& data) override;
|
||||
bool hasKey(const QUuid& dbUuid) const override;
|
||||
|
||||
void reset(const QUuid& dbUuid) override;
|
||||
void reset() override;
|
||||
|
||||
private:
|
||||
bool promptPolkit();
|
||||
|
||||
bool m_available;
|
||||
QString m_error;
|
||||
QHash<QUuid, QByteArray> m_encryptedMasterKeys;
|
||||
QHash<QUuid, QByteArray> m_sessionKeys;
|
||||
|
||||
QScopedPointer<org::freedesktop::PolicyKit1::Authority> m_polkit;
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_POLKIT_H
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "PolkitDbusTypes.h"
|
||||
|
||||
void PolkitSubject::registerMetaType()
|
||||
@@ -43,3 +60,32 @@ const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizati
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
void PolkitActionDescription::registerMetaType()
|
||||
{
|
||||
qRegisterMetaType<PolkitActionDescription>("PolkitActionDescription");
|
||||
qDBusRegisterMetaType<PolkitActionDescription>();
|
||||
|
||||
qRegisterMetaType<PolkitActionDescriptionList>("PolkitActionDescriptionList");
|
||||
qDBusRegisterMetaType<PolkitActionDescriptionList>();
|
||||
}
|
||||
|
||||
QDBusArgument& operator<<(QDBusArgument& argument, const PolkitActionDescription& action)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument << action.actionId << action.description << action.message << action.vendorName << action.vendorUrl
|
||||
<< action.iconName << action.implicitAny << action.implicitInactive << action.implicitActive
|
||||
<< action.annotations;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitActionDescription& action)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument >> action.actionId >> action.description >> action.message >> action.vendorName >> action.vendorUrl
|
||||
>> action.iconName >> action.implicitAny >> action.implicitInactive >> action.implicitActive
|
||||
>> action.annotations;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
#ifndef KEEPASSX_POLKITDBUSTYPES_H
|
||||
#define KEEPASSX_POLKITDBUSTYPES_H
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtDBus>
|
||||
|
||||
@@ -30,7 +46,30 @@ public:
|
||||
friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizationResults& subject);
|
||||
};
|
||||
|
||||
class PolkitActionDescription
|
||||
{
|
||||
public:
|
||||
QString actionId;
|
||||
QString description;
|
||||
QString message;
|
||||
QString vendorName;
|
||||
QString vendorUrl;
|
||||
QString iconName;
|
||||
uint implicitAny;
|
||||
uint implicitInactive;
|
||||
uint implicitActive;
|
||||
QMap<QString, QString> annotations;
|
||||
|
||||
static void registerMetaType();
|
||||
|
||||
friend QDBusArgument& operator<<(QDBusArgument& argument, const PolkitActionDescription& action);
|
||||
|
||||
friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitActionDescription& action);
|
||||
};
|
||||
|
||||
typedef QList<PolkitActionDescription> PolkitActionDescriptionList;
|
||||
|
||||
Q_DECLARE_METATYPE(PolkitSubject);
|
||||
Q_DECLARE_METATYPE(PolkitAuthorizationResults);
|
||||
|
||||
#endif // KEEPASSX_POLKITDBUSTYPES_H
|
||||
Q_DECLARE_METATYPE(PolkitActionDescription);
|
||||
Q_DECLARE_METATYPE(PolkitActionDescriptionList);
|
||||
|
||||
@@ -16,66 +16,55 @@
|
||||
*/
|
||||
|
||||
#include "QuickUnlockInterface.h"
|
||||
#include "PinUnlock.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#if defined(Q_OS_MACOS)
|
||||
#include "TouchID.h"
|
||||
#define QUICKUNLOCK_IMPLEMENTATION TouchID
|
||||
#elif defined(Q_CC_MSVC)
|
||||
#include "WindowsHello.h"
|
||||
#define QUICKUNLOCK_IMPLEMENTATION WindowsHello
|
||||
#elif defined(Q_OS_LINUX)
|
||||
#include "Polkit.h"
|
||||
#define QUICKUNLOCK_IMPLEMENTATION Polkit
|
||||
#else
|
||||
#define QUICKUNLOCK_IMPLEMENTATION NoQuickUnlock
|
||||
#endif
|
||||
|
||||
QUICKUNLOCK_IMPLEMENTATION* quickUnlockInstance = {nullptr};
|
||||
QuickUnlockManager* g_quickUnlockManager = nullptr;
|
||||
|
||||
QuickUnlockInterface* getQuickUnlock()
|
||||
QuickUnlockManager* getQuickUnlock()
|
||||
{
|
||||
if (!quickUnlockInstance) {
|
||||
quickUnlockInstance = new QUICKUNLOCK_IMPLEMENTATION();
|
||||
if (!g_quickUnlockManager) {
|
||||
g_quickUnlockManager = new QuickUnlockManager();
|
||||
}
|
||||
return quickUnlockInstance;
|
||||
return g_quickUnlockManager;
|
||||
}
|
||||
|
||||
bool NoQuickUnlock::isAvailable() const
|
||||
QuickUnlockManager::QuickUnlockManager()
|
||||
{
|
||||
return false;
|
||||
// Create the native interface based on the platform
|
||||
#if defined(Q_OS_MACOS)
|
||||
m_nativeInterface.reset(new TouchID());
|
||||
#elif defined(Q_CC_MSVC)
|
||||
m_nativeInterface.reset(new WindowsHello());
|
||||
#elif defined(Q_OS_LINUX)
|
||||
m_nativeInterface.reset(new Polkit());
|
||||
#endif
|
||||
// Always create the fallback interface
|
||||
m_fallbackInterface.reset(new PinUnlock());
|
||||
}
|
||||
|
||||
QString NoQuickUnlock::errorString() const
|
||||
{
|
||||
return QObject::tr("No Quick Unlock provider is available");
|
||||
}
|
||||
|
||||
void NoQuickUnlock::reset()
|
||||
QuickUnlockManager::~QuickUnlockManager()
|
||||
{
|
||||
}
|
||||
|
||||
bool NoQuickUnlock::setKey(const QUuid& dbUuid, const QByteArray& key)
|
||||
QSharedPointer<QuickUnlockInterface> QuickUnlockManager::interface() const
|
||||
{
|
||||
Q_UNUSED(dbUuid)
|
||||
Q_UNUSED(key)
|
||||
return false;
|
||||
if (isNativeAvailable()) {
|
||||
return m_nativeInterface;
|
||||
}
|
||||
return m_fallbackInterface;
|
||||
}
|
||||
|
||||
bool NoQuickUnlock::getKey(const QUuid& dbUuid, QByteArray& key)
|
||||
bool QuickUnlockManager::isNativeAvailable() const
|
||||
{
|
||||
Q_UNUSED(dbUuid)
|
||||
Q_UNUSED(key)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const
|
||||
{
|
||||
Q_UNUSED(dbUuid)
|
||||
return false;
|
||||
}
|
||||
|
||||
void NoQuickUnlock::reset(const QUuid& dbUuid)
|
||||
{
|
||||
Q_UNUSED(dbUuid)
|
||||
return m_nativeInterface && m_nativeInterface->isAvailable();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#ifndef KEEPASSXC_QUICKUNLOCKINTERFACE_H
|
||||
#define KEEPASSXC_QUICKUNLOCKINTERFACE_H
|
||||
|
||||
#include <QSharedPointer>
|
||||
#include <QUuid>
|
||||
|
||||
class QuickUnlockInterface
|
||||
@@ -29,7 +30,6 @@ public:
|
||||
virtual ~QuickUnlockInterface() = default;
|
||||
|
||||
virtual bool isAvailable() const = 0;
|
||||
virtual QString errorString() const = 0;
|
||||
|
||||
virtual bool setKey(const QUuid& dbUuid, const QByteArray& key) = 0;
|
||||
virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0;
|
||||
@@ -37,22 +37,32 @@ public:
|
||||
|
||||
virtual void reset(const QUuid& dbUuid) = 0;
|
||||
virtual void reset() = 0;
|
||||
|
||||
virtual QString errorString() const
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
protected:
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
class NoQuickUnlock : public QuickUnlockInterface
|
||||
class QuickUnlockManager final
|
||||
{
|
||||
Q_DISABLE_COPY(QuickUnlockManager)
|
||||
|
||||
public:
|
||||
bool isAvailable() const override;
|
||||
QString errorString() const override;
|
||||
QuickUnlockManager();
|
||||
~QuickUnlockManager();
|
||||
|
||||
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
|
||||
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
|
||||
bool hasKey(const QUuid& dbUuid) const override;
|
||||
QSharedPointer<QuickUnlockInterface> interface() const;
|
||||
bool isNativeAvailable() const;
|
||||
|
||||
void reset(const QUuid& dbUuid) override;
|
||||
void reset() override;
|
||||
private:
|
||||
QSharedPointer<QuickUnlockInterface> m_nativeInterface;
|
||||
QSharedPointer<QuickUnlockInterface> m_fallbackInterface;
|
||||
};
|
||||
|
||||
QuickUnlockInterface* getQuickUnlock();
|
||||
QuickUnlockManager* getQuickUnlock();
|
||||
|
||||
#endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H
|
||||
|
||||
72
src/quickunlock/TouchID.cpp
Normal file
72
src/quickunlock/TouchID.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "quickunlock/TouchID.h"
|
||||
#include "gui/osutils/OSUtils.h"
|
||||
|
||||
/**
|
||||
* Store the serialized database key into the macOS key store. The OS handles encrypt/decrypt operations.
|
||||
* https://developer.apple.com/documentation/security/keychain_services/keychain_items
|
||||
*/
|
||||
bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& key)
|
||||
{
|
||||
if (key.isEmpty()) {
|
||||
qWarning("TouchID::setKey - provided key is empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
return osUtils->saveSecret(dbUuid.toString(), key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve serialized key data from the macOS Keychain after successful authentication
|
||||
* with TouchID or Watch interface.
|
||||
*/
|
||||
bool TouchID::getKey(const QUuid& dbUuid, QByteArray& key)
|
||||
{
|
||||
key.clear();
|
||||
|
||||
if (!hasKey(dbUuid)) {
|
||||
qWarning("TouchID::getKey - No stored key found");
|
||||
return false;
|
||||
}
|
||||
|
||||
return osUtils->getSecret(dbUuid.toString(), key);
|
||||
}
|
||||
|
||||
bool TouchID::hasKey(const QUuid& dbUuid) const
|
||||
{
|
||||
QByteArray tmp;
|
||||
return osUtils->getSecret(dbUuid.toString(), tmp);
|
||||
}
|
||||
|
||||
bool TouchID::isAvailable() const
|
||||
{
|
||||
return macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::TouchId)
|
||||
|| macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::Watch)
|
||||
|| macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::PasswordFallback);
|
||||
}
|
||||
|
||||
void TouchID::reset(const QUuid& dbUuid)
|
||||
{
|
||||
osUtils->removeSecret(dbUuid.toString());
|
||||
}
|
||||
|
||||
void TouchID::reset()
|
||||
{
|
||||
osUtils->removeAllSecrets();
|
||||
}
|
||||
@@ -15,17 +15,14 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSX_TOUCHID_H
|
||||
#define KEEPASSX_TOUCHID_H
|
||||
#pragma once
|
||||
|
||||
#include "QuickUnlockInterface.h"
|
||||
#include <QHash>
|
||||
|
||||
class TouchID : public QuickUnlockInterface
|
||||
{
|
||||
public:
|
||||
bool isAvailable() const override;
|
||||
QString errorString() const override;
|
||||
|
||||
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey) override;
|
||||
bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override;
|
||||
@@ -33,17 +30,4 @@ public:
|
||||
|
||||
void reset(const QUuid& dbUuid = "") override;
|
||||
void reset() override;
|
||||
|
||||
private:
|
||||
static bool isWatchAvailable();
|
||||
static bool isTouchIdAvailable();
|
||||
static bool isPasswordFallbackPossible();
|
||||
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID);
|
||||
|
||||
static void deleteKeyEntry(const QString& accountName);
|
||||
static QString databaseKeyName(const QUuid& dbUuid);
|
||||
|
||||
QHash<QUuid, QByteArray> m_encryptedMasterKeys;
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TOUCHID_H
|
||||
|
||||
@@ -1,408 +0,0 @@
|
||||
#include "quickunlock/TouchID.h"
|
||||
|
||||
#include "crypto/Random.h"
|
||||
#include "crypto/SymmetricCipher.h"
|
||||
#include "crypto/CryptoHash.h"
|
||||
#include "config-keepassx.h"
|
||||
|
||||
#include <botan/mem_ops.h>
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <LocalAuthentication/LocalAuthentication.h>
|
||||
#include <Security/Security.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QString>
|
||||
|
||||
#define TOUCH_ID_ENABLE_DEBUG_LOGS() 0
|
||||
#if TOUCH_ID_ENABLE_DEBUG_LOGS()
|
||||
#define debug(...) qWarning(__VA_ARGS__)
|
||||
#else
|
||||
inline void debug(const char *message, ...)
|
||||
{
|
||||
Q_UNUSED(message);
|
||||
}
|
||||
#endif
|
||||
|
||||
inline std::string StatusToErrorMessage(OSStatus status)
|
||||
{
|
||||
CFStringRef text = SecCopyErrorMessageString(status, NULL);
|
||||
if (!text) {
|
||||
return std::to_string(status);
|
||||
}
|
||||
|
||||
auto msg = CFStringGetCStringPtr(text, kCFStringEncodingUTF8);
|
||||
std::string result;
|
||||
if (msg) {
|
||||
result = msg;
|
||||
}
|
||||
CFRelease(text);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline void LogStatusError(const char *message, OSStatus status)
|
||||
{
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string msg = StatusToErrorMessage(status);
|
||||
debug("%s: %s", message, msg.c_str());
|
||||
}
|
||||
|
||||
inline CFMutableDictionaryRef makeDictionary() {
|
||||
return CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
}
|
||||
|
||||
//! Try to delete an existing keychain entry
|
||||
void TouchID::deleteKeyEntry(const QString& accountName)
|
||||
{
|
||||
NSString* nsAccountName = accountName.toNSString(); // The NSString is released by Qt
|
||||
|
||||
// try to delete an existing entry
|
||||
CFMutableDictionaryRef query = makeDictionary();
|
||||
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) nsAccountName);
|
||||
CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
|
||||
|
||||
// get data from the KeyChain
|
||||
OSStatus status = SecItemDelete(query);
|
||||
LogStatusError("TouchID::deleteKeyEntry - Status deleting existing entry", status);
|
||||
}
|
||||
|
||||
QString TouchID::databaseKeyName(const QUuid& dbUuid)
|
||||
{
|
||||
static const QString keyPrefix = "KeepassXC_TouchID_Keys_";
|
||||
return keyPrefix + dbUuid.toString();
|
||||
}
|
||||
|
||||
QString TouchID::errorString() const
|
||||
{
|
||||
// TODO
|
||||
return "";
|
||||
}
|
||||
|
||||
void TouchID::reset()
|
||||
{
|
||||
m_encryptedMasterKeys.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID)
|
||||
{
|
||||
if (passwordKey.isEmpty()) {
|
||||
debug("TouchID::setKey - illegal arguments");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_encryptedMasterKeys.contains(dbUuid)) {
|
||||
debug("TouchID::setKey - Already stored key for this database");
|
||||
return true;
|
||||
}
|
||||
|
||||
// generate random AES 256bit key and IV
|
||||
QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
|
||||
QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
|
||||
|
||||
SymmetricCipher aes256Encrypt;
|
||||
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
|
||||
debug("TouchID::setKey - AES initialisation failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// encrypt and keep result in memory
|
||||
QByteArray encryptedMasterKey = passwordKey;
|
||||
if (!aes256Encrypt.finish(encryptedMasterKey)) {
|
||||
debug("TouchID::getKey - AES encrypt failed: %s", aes256Encrypt.errorString().toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString keyName = databaseKeyName(dbUuid);
|
||||
|
||||
deleteKeyEntry(keyName); // Try to delete the existing key entry
|
||||
|
||||
// prepare adding secure entry to the macOS KeyChain
|
||||
CFErrorRef error = NULL;
|
||||
|
||||
// We need both runtime and compile time checks here to solve the following problems:
|
||||
// - Not all flags are available in all OS versions, so we have to check it at compile time
|
||||
// - Requesting Biometry/TouchID/DevicePassword when to fingerprint sensor is available will result in runtime error
|
||||
SecAccessControlCreateFlags accessControlFlags = 0;
|
||||
#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
|
||||
// Needs a special check to work with SecItemAdd, when TouchID is not enrolled and the flag
|
||||
// is set, the method call fails with an error. But we want to still set this flag if TouchID is
|
||||
// enrolled but temporarily unavailable due to closed lid
|
||||
//
|
||||
// At least on a Hackintosh the enrolled-check does not work, there LAErrorBiometryNotAvailable gets returned instead of
|
||||
// LAErrorBiometryNotEnrolled.
|
||||
//
|
||||
// That's kinda unfortunate, because now you cannot know for sure if TouchID hardware is either temporarily unavailable or not present
|
||||
// at all, because LAErrorBiometryNotAvailable is used for both cases.
|
||||
//
|
||||
// So to make quick unlock fallbacks possible on these machines you have to try to save the key a second time without this flag, if the
|
||||
// first try fails with an error.
|
||||
if (!ignoreTouchID) {
|
||||
// Prefer the non-deprecated flag when available
|
||||
accessControlFlags = kSecAccessControlBiometryCurrentSet;
|
||||
}
|
||||
#elif XC_COMPILER_SUPPORT(TOUCH_ID)
|
||||
if (!ignoreTouchID) {
|
||||
accessControlFlags = kSecAccessControlTouchIDCurrentSet;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
|
||||
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch;
|
||||
#endif
|
||||
|
||||
#if XC_COMPILER_SUPPORT(TOUCH_ID)
|
||||
if (isPasswordFallbackPossible()) {
|
||||
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlDevicePasscode;
|
||||
}
|
||||
#endif
|
||||
|
||||
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(
|
||||
kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
|
||||
|
||||
if (sacObject == NULL || error != NULL) {
|
||||
NSError* e = (__bridge NSError*) error;
|
||||
debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String);
|
||||
return false;
|
||||
}
|
||||
|
||||
NSString *accountName = keyName.toNSString(); // The NSString is released by Qt
|
||||
|
||||
// prepare data (key) to be stored
|
||||
QByteArray keychainKeyValue = (randomKey + randomIV).toHex();
|
||||
CFDataRef keychainValueData =
|
||||
CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, reinterpret_cast<UInt8 *>(keychainKeyValue.data()),
|
||||
keychainKeyValue.length(), kCFAllocatorDefault);
|
||||
|
||||
CFMutableDictionaryRef attributes = makeDictionary();
|
||||
CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName);
|
||||
CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keychainValueData);
|
||||
CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
|
||||
CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
|
||||
CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject);
|
||||
|
||||
// add to KeyChain
|
||||
OSStatus status = SecItemAdd(attributes, NULL);
|
||||
LogStatusError("TouchID::setKey - Status adding new entry", status);
|
||||
|
||||
CFRelease(sacObject);
|
||||
CFRelease(attributes);
|
||||
|
||||
// Cleanse the key information from the memory
|
||||
Botan::secure_scrub_memory(randomKey.data(), randomKey.size());
|
||||
Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
|
||||
|
||||
if (status != errSecSuccess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// memorize which database the stored key is for
|
||||
m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
|
||||
debug("TouchID::setKey - Success!");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random AES 256bit key and uses it to encrypt the PasswordKey that
|
||||
* protects the database. The encrypted PasswordKey is kept in memory while the
|
||||
* AES key is stored in the macOS KeyChain protected by either TouchID or Apple Watch.
|
||||
*/
|
||||
bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey)
|
||||
{
|
||||
if (!setKey(dbUuid,passwordKey, false)) {
|
||||
debug("TouchID::setKey failed with error trying fallback method without TouchID flag");
|
||||
return setKey(dbUuid, passwordKey, true);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an encrypted PasswordKey is available for the given database, tries to
|
||||
* decrypt it using the KeyChain and if successful, returns it.
|
||||
*/
|
||||
bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey)
|
||||
{
|
||||
passwordKey.clear();
|
||||
|
||||
if (!hasKey(dbUuid)) {
|
||||
debug("TouchID::getKey - No stored key found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// query the KeyChain for the AES key
|
||||
CFMutableDictionaryRef query = makeDictionary();
|
||||
|
||||
const QString keyName = databaseKeyName(dbUuid);
|
||||
NSString* accountName = keyName.toNSString(); // The NSString is released by Qt
|
||||
NSString* touchPromptMessage =
|
||||
QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database")
|
||||
.toNSString(); // The NSString is released by Qt
|
||||
|
||||
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName);
|
||||
CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
|
||||
CFDictionarySetValue(query, kSecUseOperationPrompt, (__bridge CFStringRef) touchPromptMessage);
|
||||
|
||||
// get data from the KeyChain
|
||||
CFTypeRef dataTypeRef = NULL;
|
||||
OSStatus status = SecItemCopyMatching(query, &dataTypeRef);
|
||||
CFRelease(query);
|
||||
|
||||
if (status == errSecUserCanceled) {
|
||||
// user canceled the authentication, return true with empty key
|
||||
debug("TouchID::getKey - User canceled authentication");
|
||||
return true;
|
||||
} else if (status != errSecSuccess || dataTypeRef == NULL) {
|
||||
LogStatusError("TouchID::getKey - key query error", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
CFDataRef valueData = static_cast<CFDataRef>(dataTypeRef);
|
||||
QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)),
|
||||
CFDataGetLength(valueData)));
|
||||
CFRelease(dataTypeRef);
|
||||
|
||||
// extract AES key and IV from data bytes
|
||||
QByteArray key = dataBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
|
||||
QByteArray iv = dataBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
|
||||
|
||||
SymmetricCipher aes256Decrypt;
|
||||
if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
|
||||
debug("TouchID::getKey - AES initialization failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// decrypt PasswordKey from memory using AES
|
||||
passwordKey = m_encryptedMasterKeys[dbUuid];
|
||||
if (!aes256Decrypt.finish(passwordKey)) {
|
||||
passwordKey.clear();
|
||||
debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cleanse the key information from the memory
|
||||
Botan::secure_scrub_memory(key.data(), key.size());
|
||||
Botan::secure_scrub_memory(iv.data(), iv.size());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TouchID::hasKey(const QUuid& dbUuid) const
|
||||
{
|
||||
return m_encryptedMasterKeys.contains(dbUuid);
|
||||
}
|
||||
|
||||
// TODO: Both functions below should probably handle the returned errors to
|
||||
// provide more information on availability. E.g.: the closed laptop lid results
|
||||
// in an error (because touch id is not unavailable). That error could be
|
||||
// displayed to the user when we first check for availability instead of just
|
||||
// hiding the checkbox.
|
||||
|
||||
//! @return true if Apple Watch is available for authentication.
|
||||
bool TouchID::isWatchAvailable()
|
||||
{
|
||||
#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
|
||||
@try {
|
||||
LAContext *context = [[LAContext alloc] init];
|
||||
|
||||
LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch;
|
||||
NSError *error;
|
||||
|
||||
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
|
||||
[context release];
|
||||
if (error) {
|
||||
debug("Apple Wach available: %d (%ld / %s / %s)", canAuthenticate,
|
||||
(long)error.code, error.description.UTF8String,
|
||||
error.localizedDescription.UTF8String);
|
||||
} else {
|
||||
debug("Apple Wach available: %d", canAuthenticate);
|
||||
}
|
||||
return canAuthenticate;
|
||||
} @catch (NSException *) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
//! @return true if Touch ID is available for authentication.
|
||||
bool TouchID::isTouchIdAvailable()
|
||||
{
|
||||
#if XC_COMPILER_SUPPORT(TOUCH_ID)
|
||||
@try {
|
||||
LAContext *context = [[LAContext alloc] init];
|
||||
|
||||
LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
|
||||
NSError *error;
|
||||
|
||||
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
|
||||
[context release];
|
||||
if (error) {
|
||||
debug("Touch ID available: %d (%ld / %s / %s)", canAuthenticate,
|
||||
(long)error.code, error.description.UTF8String,
|
||||
error.localizedDescription.UTF8String);
|
||||
} else {
|
||||
debug("Touch ID available: %d", canAuthenticate);
|
||||
}
|
||||
return canAuthenticate;
|
||||
} @catch (NSException *) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool TouchID::isPasswordFallbackPossible()
|
||||
{
|
||||
#if XC_COMPILER_SUPPORT(TOUCH_ID)
|
||||
@try {
|
||||
LAContext *context = [[LAContext alloc] init];
|
||||
|
||||
LAPolicy policyCode = LAPolicyDeviceOwnerAuthentication;
|
||||
NSError *error;
|
||||
|
||||
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
|
||||
[context release];
|
||||
if (error) {
|
||||
debug("Password fallback available: %d (%ld / %s / %s)", canAuthenticate,
|
||||
(long)error.code, error.description.UTF8String,
|
||||
error.localizedDescription.UTF8String);
|
||||
} else {
|
||||
debug("Password fallback available: %d", canAuthenticate);
|
||||
}
|
||||
return canAuthenticate;
|
||||
} @catch (NSException *) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
//! @return true if either TouchID or Apple Watch is available at the moment.
|
||||
bool TouchID::isAvailable() const
|
||||
{
|
||||
// note: we cannot cache the check results because the configuration
|
||||
// is dynamic in its nature. User can close the laptop lid or take off
|
||||
// the watch, thus making one (or both) of the authentication types unavailable.
|
||||
return isWatchAvailable() || isTouchIdAvailable() || isPasswordFallbackPossible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the inner state either for all or for the given database
|
||||
*/
|
||||
void TouchID::reset(const QUuid& dbUuid)
|
||||
{
|
||||
m_encryptedMasterKeys.remove(dbUuid);
|
||||
}
|
||||
@@ -17,8 +17,9 @@
|
||||
|
||||
#include "WindowsHello.h"
|
||||
|
||||
#include <Userconsentverifierinterop.h>
|
||||
#include <Windows.h>
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/windows.foundation.collections.h>
|
||||
#include <winrt/windows.foundation.h>
|
||||
#include <winrt/windows.security.credentials.h>
|
||||
#include <winrt/windows.security.cryptography.h>
|
||||
@@ -28,12 +29,14 @@
|
||||
#include "crypto/CryptoHash.h"
|
||||
#include "crypto/Random.h"
|
||||
#include "crypto/SymmetricCipher.h"
|
||||
#include "gui/osutils/OSUtils.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QWindow>
|
||||
|
||||
using namespace winrt;
|
||||
using namespace Windows::Foundation;
|
||||
using namespace Windows::Foundation::Collections;
|
||||
using namespace Windows::Security::Credentials;
|
||||
using namespace Windows::Security::Cryptography;
|
||||
using namespace Windows::Storage::Streams;
|
||||
@@ -43,17 +46,20 @@ namespace
|
||||
const std::wstring s_winHelloKeyName{L"keepassxc_winhello"};
|
||||
int g_promptFocusCount = 0;
|
||||
|
||||
void queueSecurityPromptFocus(int delay = 500)
|
||||
void queueSecurityPromptFocus(bool initial, int delay = 500)
|
||||
{
|
||||
if (initial) {
|
||||
g_promptFocusCount = 0;
|
||||
}
|
||||
|
||||
QTimer::singleShot(delay, [] {
|
||||
auto hWnd = ::FindWindowA("Credential Dialog Xaml Host", nullptr);
|
||||
if (hWnd) {
|
||||
::SetForegroundWindow(hWnd);
|
||||
} else if (++g_promptFocusCount <= 3) {
|
||||
queueSecurityPromptFocus();
|
||||
return;
|
||||
qDebug("WindowsHello - Could not find security prompt window");
|
||||
queueSecurityPromptFocus(false);
|
||||
}
|
||||
g_promptFocusCount = 0;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,14 +111,9 @@ bool WindowsHello::isAvailable() const
|
||||
return task.get();
|
||||
}
|
||||
|
||||
QString WindowsHello::errorString() const
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
|
||||
{
|
||||
queueSecurityPromptFocus();
|
||||
queueSecurityPromptFocus(true);
|
||||
|
||||
// Generate a random challenge that will be signed by Windows Hello
|
||||
// to create the key. The challenge is also used as the IV.
|
||||
@@ -120,6 +121,7 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
|
||||
auto challenge = Random::instance()->randomArray(ivSize);
|
||||
QByteArray key;
|
||||
if (!deriveEncryptionKey(challenge, key, m_error)) {
|
||||
m_error = QObject::tr("Windows Hello setup was canceled or failed. Quick unlock has not been enabled.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -137,28 +139,28 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
|
||||
|
||||
// Prepend the challenge/IV to the encrypted data
|
||||
encrypted.prepend(challenge);
|
||||
m_encryptedKeys.insert(dbUuid, encrypted);
|
||||
return true;
|
||||
return osUtils->saveSecret(dbUuid.toString(), encrypted);
|
||||
}
|
||||
|
||||
bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
|
||||
{
|
||||
data.clear();
|
||||
if (!hasKey(dbUuid)) {
|
||||
m_error = QObject::tr("Failed to get Windows Hello credential.");
|
||||
QByteArray keydata;
|
||||
if (!osUtils->getSecret(dbUuid.toString(), keydata)) {
|
||||
m_error = QObject::tr("Failed to retrieve Windows Hello credential.");
|
||||
return false;
|
||||
}
|
||||
|
||||
queueSecurityPromptFocus();
|
||||
queueSecurityPromptFocus(true);
|
||||
|
||||
// Read the previously used challenge and encrypted data
|
||||
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
|
||||
const auto& keydata = m_encryptedKeys.value(dbUuid);
|
||||
auto challenge = keydata.left(ivSize);
|
||||
auto encrypted = keydata.mid(ivSize);
|
||||
QByteArray key;
|
||||
|
||||
QByteArray key;
|
||||
if (!deriveEncryptionKey(challenge, key, m_error)) {
|
||||
// Error is set in deriveEncryptionKey
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -182,15 +184,16 @@ bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
|
||||
|
||||
void WindowsHello::reset(const QUuid& dbUuid)
|
||||
{
|
||||
m_encryptedKeys.remove(dbUuid);
|
||||
osUtils->removeSecret(dbUuid.toString());
|
||||
}
|
||||
|
||||
bool WindowsHello::hasKey(const QUuid& dbUuid) const
|
||||
{
|
||||
return m_encryptedKeys.contains(dbUuid);
|
||||
QByteArray tmp;
|
||||
return osUtils->getSecret(dbUuid.toString(), tmp);
|
||||
}
|
||||
|
||||
void WindowsHello::reset()
|
||||
{
|
||||
m_encryptedKeys.clear();
|
||||
osUtils->removeAllSecrets();
|
||||
}
|
||||
|
||||
@@ -20,26 +20,22 @@
|
||||
|
||||
#include "QuickUnlockInterface.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
|
||||
class WindowsHello : public QuickUnlockInterface
|
||||
{
|
||||
public:
|
||||
WindowsHello() = default;
|
||||
|
||||
bool isAvailable() const override;
|
||||
QString errorString() const override;
|
||||
void reset() override;
|
||||
|
||||
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
|
||||
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
|
||||
bool hasKey(const QUuid& dbUuid) const override;
|
||||
|
||||
void reset(const QUuid& dbUuid) override;
|
||||
void reset() override;
|
||||
|
||||
private:
|
||||
QString m_error;
|
||||
QHash<QUuid, QByteArray> m_encryptedKeys;
|
||||
Q_DISABLE_COPY(WindowsHello);
|
||||
Q_DISABLE_COPY(WindowsHello)
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_WINDOWSHELLO_H
|
||||
|
||||
@@ -12,5 +12,10 @@
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="PolkitSubject"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QMap<QString, QString>"/>
|
||||
</method>
|
||||
<method name="EnumerateActions">
|
||||
<arg type="s" name="locale" direction="in" />
|
||||
<arg type="a(ssssssuuua{ss})" name="action_descriptions" direction="out" />
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="PolkitActionDescriptionList"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include "core/Config.h"
|
||||
#include "core/Group.h"
|
||||
#include "core/Resources.h"
|
||||
#include "core/Totp.h"
|
||||
#include "crypto/Crypto.h"
|
||||
#include "gui/MessageBox.h"
|
||||
#include "gui/osutils/OSUtils.h"
|
||||
@@ -76,9 +75,6 @@ void TestAutoType::init()
|
||||
association.window = "custom window";
|
||||
association.sequence = "{username}association{password}";
|
||||
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->setGroup(m_group);
|
||||
@@ -474,24 +470,3 @@ void TestAutoType::testAutoTypeEmptyWindowAssociation()
|
||||
assoc = m_entry6->autoTypeSequences("Some Other Window");
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ private slots:
|
||||
void testAutoTypeSyntaxChecks();
|
||||
void testAutoTypeEffectiveSequences();
|
||||
void testAutoTypeEmptyWindowAssociation();
|
||||
void testAutoTypeTotpDelay();
|
||||
|
||||
private:
|
||||
AutoTypePlatformInterface* m_platform;
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include <QTest>
|
||||
|
||||
#include "core/Group.h"
|
||||
#include "core/Tools.h"
|
||||
#include "core/Totp.h"
|
||||
#include "crypto/Crypto.h"
|
||||
#include "format/CsvExporter.h"
|
||||
@@ -111,218 +110,3 @@ void TestCsvExporter::testNestedGroups()
|
||||
.append(ExpectedHeaderLine)
|
||||
.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,10 +39,6 @@ private slots:
|
||||
void testExport();
|
||||
void testEmptyDatabase();
|
||||
void testNestedGroups();
|
||||
void testRoundTripWithCustomRootName();
|
||||
void testRoundTripWithDefaultRootName();
|
||||
void testSingleLevelGroup();
|
||||
void testAbsolutePaths();
|
||||
|
||||
private:
|
||||
QSharedPointer<Database> m_db;
|
||||
|
||||
@@ -626,17 +626,6 @@ void TestEntry::testResolveReplacePlaceholders()
|
||||
// Test complicated and nested replacements
|
||||
QCOMPARE(entry2->resolveMultiplePlaceholders(entry2->url()),
|
||||
QString("cmd://sap.exe -system=server1 -client=12345 -user=Username2 -pw=Password1"));
|
||||
|
||||
auto* entry3 = new Entry();
|
||||
entry3->setGroup(root);
|
||||
entry3->setUuid(QUuid::createUuid());
|
||||
entry3->setTitle("Entry 3");
|
||||
entry3->setUsername("HMAC-SHA-256");
|
||||
entry3->setUrl("{T-REPLACE-RX:!{USERNAME}!\\{USERNAME\\}!!}");
|
||||
|
||||
// Test escaped enclosures
|
||||
QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->url()), entry3->username());
|
||||
|
||||
// Test invalid syntax
|
||||
QString error;
|
||||
entry1->resolveRegexPlaceholder("{T-REPLACE-RX:/{USERNAME}/.*+?/test/}", &error); // invalid regex
|
||||
|
||||
@@ -403,8 +403,8 @@ void TestTools::testGetMimeTypeByFileInfo()
|
||||
|
||||
const QStringList Markdowns = {"test.md", "test.markdown"};
|
||||
|
||||
for (const auto& markdown : Markdowns) {
|
||||
QCOMPARE(Tools::getMimeType(QFileInfo(markdown)), Tools::MimeType::Markdown);
|
||||
for (const auto& makdown : Markdowns) {
|
||||
QCOMPARE(Tools::getMimeType(QFileInfo(makdown)), Tools::MimeType::Markdown);
|
||||
}
|
||||
|
||||
const QStringList UnknownHeaders = {"test.doc", "test.pdf", "test.docx"};
|
||||
@@ -451,14 +451,3 @@ void TestTools::testCleanUsername_data()
|
||||
QTest::newRow("Trailing dots and spaces") << "username... " << "username";
|
||||
QTest::newRow("Combination of issues") << R"( user<>:"/\|?*name... )" << "user_________name";
|
||||
}
|
||||
|
||||
void TestTools::testEscapeAccelerators()
|
||||
{
|
||||
QCOMPARE(Tools::escapeAccelerators(""), "");
|
||||
QCOMPARE(Tools::escapeAccelerators("NoAccelerator"), "NoAccelerator");
|
||||
QCOMPARE(Tools::escapeAccelerators("&Accelerator"), "&&Accelerator");
|
||||
QCOMPARE(Tools::escapeAccelerators("Accelerator&"), "Accelerator&&");
|
||||
QCOMPARE(Tools::escapeAccelerators("Accel&erator&"), "Accel&&erator&&");
|
||||
QCOMPARE(Tools::escapeAccelerators("Accel&&erator"), "Accel&&&&erator");
|
||||
QCOMPARE(Tools::escapeAccelerators("Some & text"), "Some && text");
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ private slots:
|
||||
void testIsTextMimeType();
|
||||
void testCleanUsername();
|
||||
void testCleanUsername_data();
|
||||
void testEscapeAccelerators();
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TESTTOOLS_H
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
module github.com/keepassxreboot/keepassxc/keepassxc-cr-recovery
|
||||
|
||||
go 1.24.0
|
||||
go 1.13
|
||||
|
||||
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
|
||||
)
|
||||
require golang.org/x/crypto v0.35.0
|
||||
|
||||
@@ -1,6 +1,67 @@
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
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,70 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib import request
|
||||
import os
|
||||
|
||||
txrc = Path.home() / '.transifexrc'
|
||||
if not txrc.exists():
|
||||
print('No Transifex config found. Run tx init first.')
|
||||
sys.exit(1)
|
||||
# Download Transifex languages dump at: https://www.transifex.com/api/2/project/keepassxc/languages
|
||||
# Language information from https://www.wikiwand.com/en/List_of_ISO_639-1_codes and http://www.lingoes.net/en/translator/langcode.htm
|
||||
|
||||
org = 'o:keepassxc'
|
||||
proj = f'{org}:p:keepassxc'
|
||||
resource = f'{proj}:r:share-translations-keepassxc-en-ts--master'
|
||||
token = [l for l in open(txrc, 'r') if l.startswith('token')][0].split('=', 1)[1].strip()
|
||||
member_blacklist = ['u:droidmonkey', 'u:phoerious']
|
||||
LANGS = {
|
||||
"ar" : "العربية (Arabic)",
|
||||
"bn" : "বাংলা (Bengali)",
|
||||
"ca" : "català (Catalan)",
|
||||
"cs" : "čeština (Czech)",
|
||||
"da" : "dansk (Danish)",
|
||||
"de" : "Deutsch (German)",
|
||||
"el" : "ελληνικά (Greek)",
|
||||
"eo" : "Esperanto (Esperanto)",
|
||||
"es" : "Español (Spanish)",
|
||||
"et" : "eesti (Estonian)",
|
||||
"eu" : "euskara (Basque)",
|
||||
"fa" : "فارسی (Farsi)",
|
||||
"fa_IR" : "فارسی (Farsi (Iran))",
|
||||
"fi" : "suomi (Finnish)",
|
||||
"fr" : "français (French)",
|
||||
"gl" : "Galego (Galician)",
|
||||
"he" : "עברית (Hebrew)",
|
||||
"hr_HR" : "hrvatski jezik (Croatian)",
|
||||
"hu" : "magyar (Hungarian)",
|
||||
"id" : "Bahasa Indonesia (Indonesian)",
|
||||
"is_IS" : "Íslenska (Icelandic)",
|
||||
"it" : "Italiano (Italian)",
|
||||
"ja" : "日本語 (Japanese)",
|
||||
"kk" : "қазақ тілі (Kazakh)",
|
||||
"ko" : "한국어 (Korean)",
|
||||
"la" : "latine (Latin)",
|
||||
"lt" : "lietuvių kalba (Lithuanian)",
|
||||
"lv" : "latviešu valoda (Latvian)",
|
||||
"nb" : "Norsk Bokmål (Norwegian Bokmål)",
|
||||
"nl_NL" : "Nederlands (Dutch)",
|
||||
"my" : "ဗမာစာ (Burmese)",
|
||||
"pa" : "ਪੰਜਾਬੀ (Punjabi)",
|
||||
"pa_IN" : "ਪੰਜਾਬੀ (Punjabi (India))",
|
||||
"pl" : "język polski (Polish)",
|
||||
"pt" : "Português (Portuguese)",
|
||||
"pt_BR" : "Português (Portuguese (Brazil))",
|
||||
"pt_PT" : "Português (Portuguese (Portugal))",
|
||||
"ro" : "Română (Romanian)",
|
||||
"ru" : "русский (Russian)",
|
||||
"sk" : "Slovenčina (Slovak)",
|
||||
"sl_SI" : "Slovenščina (Slovenian)",
|
||||
"sr" : "српски језик (Serbian)",
|
||||
"sv" : "Svenska (Swedish)",
|
||||
"th" : "ไทย (Thai)",
|
||||
"tr" : "Türkçe (Turkish)",
|
||||
"uk" : "Українська (Ukrainian)",
|
||||
"zh_CN" : "中文 (Chinese (Simplified))",
|
||||
"zh_TW" : "中文 (台灣) (Chinese (Traditional))",
|
||||
}
|
||||
|
||||
TEMPLATE = "<li><strong>{0}</strong>: {1}</li>\n"
|
||||
|
||||
def get_url(url):
|
||||
req = request.Request(url)
|
||||
req.add_header('Content-Type', 'application/vnd.api+json')
|
||||
req.add_header('Authorization', f'Bearer {token}')
|
||||
with request.urlopen(req) as resp:
|
||||
return json.load(resp)
|
||||
if not os.path.exists("languages.json"):
|
||||
print("Could not find 'languages.json' in current directory!")
|
||||
print("Save the output from https://www.transifex.com/api/2/project/keepassxc/languages")
|
||||
exit(0)
|
||||
|
||||
|
||||
print('Fetching languages...', file=sys.stderr)
|
||||
languages_json = get_url(f'https://rest.api.transifex.com/projects/{proj}/languages')
|
||||
languages = {}
|
||||
for lang in languages_json['data']:
|
||||
languages[lang['id']] = lang['attributes']['name']
|
||||
|
||||
print('Fetching language stats...', file=sys.stderr)
|
||||
language_stats_json = get_url('https://rest.api.transifex.com/resource_language_stats?'
|
||||
f'filter[project]={proj}&filter[resource]={resource}')
|
||||
completion = {}
|
||||
for stat in language_stats_json['data']:
|
||||
completion = stat['attributes']['translated_strings'] / stat['attributes']['total_strings']
|
||||
if completion < .6:
|
||||
languages.pop(stat['relationships']['language']['data']['id'])
|
||||
|
||||
print('Fetching language members...', end='', file=sys.stderr)
|
||||
members_json = get_url(f'https://rest.api.transifex.com/team_memberships?filter[organization]={org}')
|
||||
members = defaultdict(set)
|
||||
for member in members_json['data']:
|
||||
print('.', end='', file=sys.stderr)
|
||||
if member['relationships']['user']['data']['id'] in member_blacklist:
|
||||
continue
|
||||
lid = member['relationships']['language']['data']['id']
|
||||
if lid not in languages:
|
||||
continue
|
||||
user = get_url(member['relationships']['user']['links']['related'])['data']['attributes']['username']
|
||||
members[lid].add(user)
|
||||
print(file=sys.stderr)
|
||||
|
||||
print('<ul>')
|
||||
for lang in sorted(languages, key=lambda x: languages[x]):
|
||||
if not members[lang]:
|
||||
continue
|
||||
lines = [f' <li><strong>{languages[lang]}:</strong> ']
|
||||
for i, m in enumerate(sorted(members[lang], key=lambda x: x.lower())):
|
||||
if len(lines[-1]) + len(m) >= 120:
|
||||
lines.append(' ')
|
||||
lines[-1] += m
|
||||
if i < len(members[lang]) - 1:
|
||||
lines[-1] += ', '
|
||||
lines[-1] += '</li>'
|
||||
print('\n'.join(lines))
|
||||
print('</ul>')
|
||||
with open("languages.json") as json_file:
|
||||
output = open("translators.html", "w", encoding="utf-8")
|
||||
languages = json.load(json_file)
|
||||
for lang in languages:
|
||||
code = lang["language_code"]
|
||||
if code not in LANGS:
|
||||
print("WARNING: Could not find language code:", code)
|
||||
continue
|
||||
translators = ", ".join(sorted(lang["reviewers"] + lang["translators"], key=str.casefold))
|
||||
output.write(TEMPLATE.format(LANGS[code], translators))
|
||||
output.close()
|
||||
print("Language translators written to 'translators.html'!")
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "keepassxc",
|
||||
"version-string": "2.8.0",
|
||||
"builtin-baseline": "dfb72f61c5a066ab75cd0bdcb2e007228bfc3270",
|
||||
"builtin-baseline": "74e6536215718009aae747d86d84b78376bf9e09",
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "argon2",
|
||||
|
||||
Reference in New Issue
Block a user