Compare commits

..

5 Commits

Author SHA1 Message Date
Jonathan White
f15ba49fc6 WIP: Enable centralized secret storage
* Also enables pin unlock to be stored

TODO: Clean up pin unlock interface with polkit
2025-11-05 19:54:13 -05:00
Jonathan White
67b550bb6e Address PR comments 2025-11-05 19:22:52 -05:00
Jonathan White
4e2c06b943 Add safeguard to using Argon2 function 2025-11-05 19:22:51 -05:00
Jonathan White
656e0c71a3 Add Pin Quick Unlock option
* Introduce QuickUnlockManager to fall back to pin unlock if OS native options are not available.
2025-11-05 19:22:51 -05:00
Jonathan White
d2ad2a95fe Add support to remember quick unlock on Windows and macOS 2025-11-05 19:22:51 -05:00
93 changed files with 2994 additions and 12451 deletions

View File

@@ -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
View File

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

View File

@@ -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

View File

@@ -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)

View File

@@ -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})

View File

@@ -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})

View File

@@ -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()

View File

@@ -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()

View 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()

View File

@@ -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:&lt;FIELD&gt;@&lt;SEARCH_IN&gt;:&lt;SEARCH_TEXT&gt;}`

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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 &quot;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>&lt;a href=&quot;#&quot; style=&quot;text-decoration: underline&quot;&gt;I have a key file&lt;/a&gt;</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 &apos;xml&apos;, &apos;csv&apos; or &apos;html&apos;. Defaults to &apos;xml&apos;.</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 &apos;xml&apos;, &apos;csv&apos; or &apos;html&apos;. Defaults to &apos;xml&apos;.</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>

View File

@@ -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" />

View File

@@ -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})

View File

@@ -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);
}

View File

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

View File

@@ -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}},

View File

@@ -136,6 +136,7 @@ public:
Security_NoConfirmMoveEntryToRecycleBin,
Security_EnableCopyOnDoubleClick,
Security_QuickUnlock,
Security_QuickUnlockRemember,
Security_DatabasePasswordMinimumQuality,
Browser_Enabled,

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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)
{

View File

@@ -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

View File

@@ -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>
)";

View File

@@ -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()) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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();
});
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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/>

View File

@@ -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()) {

View File

@@ -1978,8 +1978,6 @@ void DatabaseWidget::closeEvent(QCloseEvent* event)
event->ignore();
return;
}
m_databaseOpenWidget->resetQuickUnlock();
event->accept();
}

View File

@@ -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

View File

@@ -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()) {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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"),

View 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) {

View File

@@ -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 = {});

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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());

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -48,7 +48,6 @@ private slots:
void selectPassword();
void launchPathSelectionDialog();
void selectPath();
void keepGroupsToggled(bool);
private:
QScopedPointer<Ui::EditGroupWidgetKeeShare> m_ui;

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View 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();
}

View 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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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

View 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();
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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

View File

@@ -12,5 +12,10 @@
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="PolkitSubject"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QMap&lt;QString, QString&gt;"/>
</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>

View File

@@ -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());
}

View File

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

View File

@@ -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"));
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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");
}

View File

@@ -43,7 +43,6 @@ private slots:
void testIsTextMimeType();
void testCleanUsername();
void testCleanUsername_data();
void testEscapeAccelerators();
};
#endif // KEEPASSX_TESTTOOLS_H

View File

@@ -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

View File

@@ -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=

View File

@@ -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'!")

View File

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