Compare commits

...

26 Commits

Author SHA1 Message Date
Jonathan White
66ff42c78b Final fixes 2025-11-23 18:26:38 +01:00
copilot-swe-agent[bot]
fc5f504509 Implement fix for auto-closing database unlock dialog when file is unavailable
Co-authored-by: droidmonkey <2809491+droidmonkey@users.noreply.github.com>
2025-11-23 18:26:35 +01:00
copilot-swe-agent[bot]
6c963e0000 Initial plan for issue 2025-11-23 18:26:32 +01:00
Jonathan White
72308a1706 Prevent launch on installer finish when run as SYSTEM
* This condition will only happen when KeePassXC is installed by MECM or similar deployment tool. This prevents accidental launch on exit if the packager forgot to set LAUNCHAPPONEXIT=0 in the msiexec call. Allowing launch on exit in these conditions would potentially allow a non-privileged user to assume the role of SYSTEM through the KeePassXC application.

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

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

Implement heuristic approach for CSV import root group detection:

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

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

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

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

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

- Fix deprecation warnings about implicit capturing of "this"

- Advance vcpkg baseline to fix macOS Qt building
  Fixes Qt build errors on macOS 26 Tahoe.
  See https://github.com/microsoft/vcpkg/pull/48298
2025-11-22 21:26:40 +01:00
Janek Bevendorff
f484d7f5ed Change Security/LockDatabaseIdle default to true 2025-11-16 17:22:00 +01:00
Janek Bevendorff
10bd651355 Enable CodeQL for all PRs and production branches 2025-11-16 09:51:48 +01:00
Janek Bevendorff
b3dbc49161 Remove theme-based menubar icon toggle on macOS
The menubar theme detection on macOS has always been wonky, and with Liquid Glass it has become entirely useless. This removes the icon theme switch and uses the monochrome light icon as a mask until we find a better solution. This should look okay in most cases, unless the user has a very bright wallpaper.
2025-11-15 20:14:15 +01:00
Janek Bevendorff
eefee1f092 Install macOS bundle icons on build
Installs bundle icons to the Resources folder during the build stage and not during the install stage. This ensures that the app has an icon when run directly from the .app folder inside the IDE. Previously, the icon would be installed only when running make install or cpack.
2025-11-15 00:57:30 +01:00
Janek Bevendorff
5332075193 Update sponsors list and translators fetch script 2025-11-12 16:24:22 +01:00
Jonathan White
964bb59f71 Fix error in hardware key detection code on Windows 2025-11-09 16:11:38 -05:00
Jonathan White
5acfcc6a1f Prevent interface lockups during startup with multiple tabs
Fixes #11998

Avoids UI lockups by removing several unnecessary mutex blocks  and avoiding redundant key detection calls.

Detect Yubikeys dynamically when challenging:

Prevents issue where correct key cannot be found if the internal state was reset prior to saving

This can occur if a user has multiple tabs open and multiple keys connected. Then switches to a locked tab without their DB key inserted which resets detection state.

Side Benefit - ensures proper cascade between USB and PC/SC interfaces so users can switch between the two modes seamlessly.
2025-11-09 16:11:38 -05:00
Jonathan White
d87554e6d2 Implement Group sync for KeeShare (#11593)
---------

Co-authored-by: ever <ever@brokenmouse.studio>
Co-authored-by: Ben Kluwe <ben.kl@go4more.de>
2025-11-09 16:10:45 -05:00
Jonathan White
e542070902 Allow for escape syntax to enable literal placeholders
* Fixes #11890
2025-11-09 10:45:07 -05:00
Jonathan White
301c64d68c Don't clear clipboard if previously cleared
* Fixes #12591
2025-11-09 10:44:33 -05:00
Janek Bevendorff
77746846da Add Liquid Glass icon 2025-11-09 15:53:11 +01:00
Sven Strickroth
002b8fee15 Do not show misleading error message if user clicked cancel
Signed-off-by: Sven Strickroth <email@cs-ware.de>
2025-11-09 09:25:00 -05:00
Sven Strickroth
44daca921a Escape accelerators
(fixes issue #12037)

Signed-off-by: Sven Strickroth <email@cs-ware.de>
2025-11-07 15:47:14 -05:00
Markus Theil
fedcbf60c5 fix build with Botan 3.10
This fixes a compiler error I got,
when trying to build with Botan 3.10.

A static_cast to RSA_PrivateKey was not possible,
as the base class is virtual.

Fix by using a dynamic_cast instead.

Signed-off-by: Markus Theil <theil.markus@gmail.com>
2025-11-07 15:46:34 -05:00
60 changed files with 11519 additions and 1625 deletions

View File

@@ -2,10 +2,10 @@ name: "CodeQL"
on:
push:
branches: [ 'develop', 'release/2.7.x' ]
branches:
- 'develop'
- 'release/**'
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,6 +27,7 @@ CMakePresets.json
CMakeUserPresets.json
.vs/
out/
\.clangd
# vcpkg
vcpkg_installed*/

View File

@@ -60,10 +60,17 @@ option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF)
option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON)
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)
set(WITH_XC_X11 ON CACHE BOOL "Enable building with X11 deps")
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()
if(APPLE)
# Perform the platform checks before applying the stricter compiler flags.
@@ -222,17 +229,23 @@ if("${CMAKE_SIZEOF_VOID_P}" EQUAL "4")
set(IS_32BIT TRUE)
endif()
set(CLANG_COMPILER_ID_REGEX "^(Apple)?[Cc]lang$")
if("${CMAKE_C_COMPILER}" MATCHES "clang$"
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()
endif()
if("${CMAKE_CXX_COMPILER}" MATCHES "clang(\\+\\+)?$"
if("${CMAKE_CXX_COMPILER}" MATCHES "clang(\\+\\+)?$"
OR "${CMAKE_EXTRA_GENERATOR_CXX_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__"
OR "${CMAKE_CXX_COMPILER_ID}" MATCHES ${CLANG_COMPILER_ID_REGEX})
set(CMAKE_COMPILER_IS_CLANGXX 1)
endif()
endif()
macro(add_gcc_compiler_cxxflags FLAGS)
@@ -395,13 +408,17 @@ 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 /MP)
add_compile_options(/permissive- /utf-8)
# Clang-cl does not support /MP, /Zf, or /fsanitize=address
if (NOT CMAKE_COMPILER_IS_CLANG_MSVC)
add_compile_options(/MP)
if(IS_DEBUG_BUILD)
add_compile_options(/Zf)
if(MSVC_TOOLSET_VERSION GREATER 141)
add_compile_definitions(/fsanitize=address)
endif()
endif()
endif()
endif()
if(WIN32)
@@ -415,7 +432,7 @@ if(WIN32)
# By default MSVC enables NXCOMPAT
add_compile_options(/guard:cf)
add_link_options(/DYNAMICBASE /HIGHENTROPYVA /GUARD:CF)
else(MINGW)
else()
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase")
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase")
# Enable high entropy ASLR for 64-bit builds
@@ -425,6 +442,8 @@ 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(MSVC)
if(WIN32 AND NOT MINGW)
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(MSVC)
if(WIN32 AND NOT MINGW)
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 MSVC)
if(WIN32 AND NOT MINGW)
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

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

View File

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

View File

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

View File

@@ -49,6 +49,8 @@ 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

@@ -342,6 +342,48 @@ 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
###########################################################################################
@@ -585,6 +627,13 @@ 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).',
@@ -594,8 +643,10 @@ 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-cert', help='SHA1 fingerprint of the signing certificate (optional).')
parser.set_defaults(cmake_generator='Ninja', no_source_tarball=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('-c', '--cmake-opts', nargs=argparse.REMAINDER,
help='Additional CMake options (no other arguments can be specified after this).')
@@ -674,15 +725,15 @@ class Build(Command):
# noinspection PyMethodMayBeStatic
def build_windows(self, version, src_dir, output_dir, *, parallelism, cmake_opts, platform_target,
sign, sign_cert, with_tests, **_):
sign, sign_identity, sign_timestamp_url, with_tests, **_):
# Check for required tools
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('-DWITH_XC_SIGNINSTALL=ON')
cmake_opts.append(f'-DWITH_XC_SIGNINSTALL_CERT={sign_cert}')
cmake_opts.append(f'-DWITH_XC_CODESIGN_IDENTITY={sign_identity}')
cmake_opts.append(f'-WITH_XC_CODESIGN_TIMESTAMP_URL={sign_timestamp_url}')
# Use vcpkg for dependency deployment
cmake_opts.append('-DX_VCPKG_APPLOCAL_DEPS_INSTALL=ON')
@@ -716,13 +767,20 @@ 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, **_):
macos_target, platform_target, with_tests, sign, sign_identity, notarize, keychain_profile, **_):
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)
@@ -736,8 +794,12 @@ class Build(Command):
_run(['cpack', '-G', 'DragNDrop'], cwd=build_dir, capture_output=False)
output_file = Path(build_dir) / f'KeePassXC-{version}.dmg'
output_file.rename(output_dir / f'KeePassXC-{version}-{platform_target}-unsigned.dmg')
unsigned_suffix = '-unsigned' if not sign else ''
output_file.rename(output_dir / f'KeePassXC-{version}-{platform_target}{unsigned_suffix}.dmg')
if sign:
logger.info('All done!')
else:
logger.info('All done! Please don\'t forget to sign the binaries before distribution.')
@staticmethod
@@ -888,162 +950,37 @@ class BuildSrc(Command):
tmp_comp.rename(output_file)
class AppSign(Command):
"""Sign binaries with code signing certificates on Windows and macOS."""
class Notarize(Command):
"""Notarize a signed macOS DMG app bundle."""
@classmethod
def setup_arg_parser(cls, parser: argparse.ArgumentParser):
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='.')
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',
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).')
def run(self, file, identity, src_dir, **kwargs):
def run(self, file, keychain_profile, **_):
if sys.platform != 'darwin':
raise Error('Unsupported platform.')
logger.warning('This tool is meant primarily for testing purposes. '
'For production use, add the --notarize flag to the build command.')
_macos_validate_keychain_profile(keychain_profile)
for i, f in enumerate(file):
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
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.')
self.notarize_macos(f, keychain_profile)
logger.info('All done.')
# noinspection PyMethodMayBeStatic
def sign_windows(self, file, identity, src_dir):
# Check for signtool
if not _cmd_exists('signtool.exe'):
raise Error('signtool was not found on the PATH.')
signtool_args = ['signtool', 'sign', '/fd', 'sha256', '/tr', 'http://timestamp.digicert.com', '/td', 'sha256']
if not identity:
logger.info('Using automatic selection of signing certificate.')
signtool_args += ['/a']
else:
logger.info('Using specified signing certificate: %s', identity)
signtool_args += ['/sha1', identity]
signtool_args += ['/d', file.name, str(file.resolve())]
_run(signtool_args, cwd=src_dir, capture_output=False)
# noinspection PyMethodMayBeStatic
def _macos_validate_keychain_profile(self, keychain_profile):
if _run(['security', 'find-generic-password', '-a',
f'com.apple.gke.notary.tool.saved-creds.{keychain_profile}'], cwd=None, check=False).returncode != 0:
raise Error(f'Keychain profile "%s" not found! Run\n'
f' {fmt.bold("xcrun notarytool store-credentials %s [...]" % keychain_profile)}\n'
f'to store your Apple notary service credentials in a keychain as "%s".',
keychain_profile, keychain_profile)
# noinspection PyMethodMayBeStatic
def _macos_get_codesigning_identity(self, user_choice=None):
result = _run(['security', 'find-identity', '-v', '-p', 'codesigning'], cwd=None, text=True)
identities = [i.strip() for i in result.stdout.strip().split('\n')[:-1]]
identities = [i.split(' ', 2)[1:] for i in identities]
if not identities:
raise Error('No codesigning identities found.')
if not user_choice and len(identities) == 1:
logger.info('Using codesigning identity %s.', identities[0][1])
return identities[0][0]
elif not user_choice:
return identities[_choice_prompt(
'The following code signing identities were found. Which one do you want to use?',
[' '.join(i) for i in identities])][0]
else:
for i in identities:
# Exact match of ID or substring match of description
if user_choice == i[0] or user_choice in i[1]:
return i[0]
raise Error('Invalid identity: %s', user_choice)
# noinspection PyMethodMayBeStatic
def sign_macos(self, file, identity, src_dir):
logger.info('Signing "%s"', file)
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp).absolute()
app_dir = tmp / 'app'
out_file = file.parent / file.name.replace('-unsigned', '')
if file.is_file() and file.suffix == '.dmg':
logger.debug('Unpacking disk image...')
mnt = tmp / 'mnt'
mnt.mkdir()
try:
_run(['hdiutil', 'attach', '-noautoopen', '-mountpoint', mnt.as_posix(), file.as_posix()], cwd=None)
shutil.copytree(mnt, app_dir, symlinks=True)
finally:
_run(['hdiutil', 'detach', mnt.as_posix()], cwd=None)
elif file.is_dir() and file.suffix == '.app':
logger.debug('Copying .app directory...')
shutil.copytree(file, app_dir, symlinks=True)
else:
logger.warning('Skipping non-app file "%s"', file)
return None
app_dir_app = list(app_dir.glob('*.app'))[0]
logger.debug('Signing libraries and frameworks...')
_run(['xcrun', 'codesign', f'--sign={identity}', '--force', '--options=runtime', '--deep',
app_dir_app.as_posix()], cwd=None)
# (Re-)Sign main executable with --entitlements
logger.debug('Signing main executable...')
_run(['xcrun', 'codesign', f'--sign={identity}', '--force', '--options=runtime',
'--entitlements', (src_dir / 'share/macosx/keepassxc.entitlements').as_posix(),
(app_dir_app / 'Contents/MacOS/KeePassXC').as_posix()], cwd=None)
tmp_out = out_file.with_suffix(f'.{"".join(random.choices(string.ascii_letters, k=8))}{file.suffix}')
try:
if file.suffix == '.dmg':
logger.debug('Repackaging disk image...')
dmg_size = sum(f.stat().st_size for f in app_dir.rglob('*'))
_run(['hdiutil', 'create', '-volname', 'KeePassXC', '-srcfolder', app_dir.as_posix(),
'-fs', 'HFS+', '-fsargs', '-c c=64,a=16,e=16', '-format', 'UDBZ',
'-size', f'{dmg_size}k', tmp_out.as_posix()],
cwd=None)
elif file.suffix == '.app':
shutil.copytree(app_dir, tmp_out, symlinks=True)
except Exception:
if tmp_out.is_file():
tmp_out.unlink()
elif tmp_out.is_dir():
shutil.rmtree(tmp_out, ignore_errors=True)
raise
finally:
# Replace original file if all went well
if tmp_out.exists():
if tmp_out.is_dir():
shutil.rmtree(file)
else:
file.unlink()
tmp_out.rename(out_file)
logger.info('File signed successfully and written to: "%s".', out_file)
return out_file
# noinspection PyMethodMayBeStatic
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)
@@ -1271,9 +1208,10 @@ def main():
BuildSrc.setup_arg_parser(build_src_parser)
build_src_parser.set_defaults(_cmd=BuildSrc)
appsign_parser = subparsers.add_parser('appsign', help=AppSign.__doc__)
AppSign.setup_arg_parser(appsign_parser)
appsign_parser.set_defaults(_cmd=AppSign)
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)
gpgsign_parser = subparsers.add_parser('gpgsign', help=GPGSign.__doc__)
GPGSign.setup_arg_parser(gpgsign_parser)

View File

@@ -67,10 +67,6 @@ 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()
@@ -85,7 +81,16 @@ add_custom_command(TARGET icons
if(APPLE)
add_custom_command(TARGET icons
POST_BUILD
COMMAND png2icns macosx/keepassxc.icns icons/application/256x256/apps/keepassxc.png
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
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endif()

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,15 @@
#!/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" ] \
|| [ -v CHROME_WRAPPER ] || [ -v MOZ_LAUNCHED_CHILD ]; then
elif [ "$1" == "proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy.AppImage" ]; 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

BIN
share/macosx/Assets.car Normal file

Binary file not shown.

View File

@@ -13,9 +13,11 @@
<key>CFBundleExecutable</key>
<string>${PROGNAME}</string>
<key>CFBundleIconFile</key>
<string>keepassxc.icns</string>
<string>${MACOSX_BUNDLE_ICON_NAME}.icns</string>
<key>CFBundleIconName</key>
<string>${MACOSX_BUNDLE_ICON_NAME}</string>
<key>CFBundleIdentifier</key>
<string>org.keepassxc.keepassxc</string>
<string>${MACOSX_BUNDLE_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>

Binary file not shown.

View File

@@ -0,0 +1 @@
<?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>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<?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>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1 @@
<?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>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,99 @@
{
"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

@@ -1799,6 +1799,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>The database file does not exist or is not accessible.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingWidgetMetaData</name>
@@ -2608,10 +2612,6 @@ This is definitely a bug, please report it to the developers.</source>
<source>Open database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to open %1. It either does not exist or is not accessible.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>CSV file</source>
<translation type="unfinished"></translation>
@@ -3735,6 +3735,14 @@ 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>
@@ -10590,11 +10598,7 @@ Example: JBSWY3DPEHPK3PXP</source>
<context>
<name>YubiKey</name>
<message>
<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>
<source>Could not find hardware key with serial number %1. Please connect it to continue.</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -10655,10 +10659,6 @@ 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,6 +121,8 @@
<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

@@ -271,7 +271,7 @@ if(WIN32)
list(APPEND gui_SOURCES
gui/osutils/winutils/ScreenLockListenerWin.cpp
gui/osutils/winutils/WinUtils.cpp)
if (MSVC)
if (WINSDK)
list(APPEND gui_SOURCES quickunlock/WindowsHello.cpp)
endif()
endif()
@@ -415,13 +415,18 @@ if(UNIX AND NOT APPLE)
endif()
if(WIN32)
target_link_libraries(keepassxc_gui Wtsapi32.lib Ws2_32.lib)
if (MSVC)
if (WINSDK)
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
@@ -432,26 +437,43 @@ if(WIN32)
VERSION_PATCH ${KEEPASSXC_VERSION_PATCH}
)
list(APPEND WIN32_ResourceFiles "${CMAKE_SOURCE_DIR}/share/windows/icon.rc")
endif()
target_sources(${PROGNAME} PUBLIC ${WIN32_ResourceFiles})
add_executable(${PROGNAME} WIN32 main.cpp ${WIN32_ResourceFiles})
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)
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"
)
set_target_properties(${PROGNAME} PROPERTIES
MACOSX_BUNDLE ON
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements")
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})
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")
@@ -483,8 +505,9 @@ 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/WindowsPostInstall.cmake.in" "${CMAKE_BINARY_DIR}/WindowsPostInstall.cmake" @ONLY)
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsPostInstall.cmake")
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")
string(REGEX REPLACE "-.*$" "" KEEPASSXC_VERSION_CLEAN ${KEEPASSXC_VERSION})

View File

@@ -175,6 +175,7 @@ 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, false}},
{Config::Security_LockDatabaseIdleSeconds, {QS("Security/LockDatabaseIdleSeconds"), Roaming, 240}},
{Config::Security_LockDatabaseIdle, {QS("Security/LockDatabaseIdle"), Roaming, true}},
{Config::Security_LockDatabaseIdleSeconds, {QS("Security/LockDatabaseIdleSeconds"), Roaming, 900}},
{Config::Security_LockDatabaseMinimize, {QS("Security/LockDatabaseMinimize"), Roaming, false}},
{Config::Security_LockDatabaseScreenLock, {QS("Security/LockDatabaseScreenLock"), Roaming, true}},
{Config::Security_LockDatabaseOnUserSwitch, {QS("Security/LockDatabaseOnUserSwitch"), Roaming, true}},

View File

@@ -1138,6 +1138,15 @@ 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,6 +449,11 @@ namespace Tools
return userName;
}
QString escapeAccelerators(QString string)
{
return string.replace("&", "&&");
}
QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties)
{
QVariantMap result;
@@ -510,7 +515,7 @@ namespace Tools
"application/protobuf",
"application/x-zerosize"};
const static QStringList HtmlFormats = {"text/html"};
const static QStringList MarkdownFormats = {"text/markdown"};
const static QStringList MarkdownFormats = {"text/markdown", "text/x-web-markdown"};
const static QStringList ImageFormats = {"image/"};
static auto isCompatible = [](const QString& format, const QStringList& list) {

View File

@@ -49,6 +49,7 @@ 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

@@ -44,29 +44,43 @@ 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>
@@ -75,30 +89,66 @@ 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>louib (CLI)</li>
<li>varjolintu (Browser Integration)</li>
<li>louib (CLI)</li>
<li>hifi (SSH Agent)</li>
<li>xvallspl (Tags)</li>
<li>Aetf (FdoSecrets Storage Server)</li>
@@ -109,6 +159,8 @@ 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>
@@ -121,179 +173,145 @@ 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 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>Markus</li>
<li>Crimson Idol</li>
<li>Björn König</li>
<li>René Weselowski</li>
<li>gonczor</li>
<li>Steven</li>
<li>Ellie</li>
<li>Anthony Avina</li>
<li>PlushElderGod</li>
<li>gilgwath</li>
<li>Tobias</li>
<li>zapscribe</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>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>Ralph Azucena</li>
<li>Florian</li>
<li>Kristoffer Winther Balling</li>
<li>Peter Link</li>
<li>Vlastimil Vondra</li>
<li>Tony Wang</li>
<li>John Sivak</li>
<li>Nol Aders</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>tolias</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>Tony Wang</li>
<li>Nol Aders</li>
<li>Dirk Bergstrom</li>
<li>proco</li>
<li>Philipp Baderschneider</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>
</ul>
<h3>Translations:</h3>
<ul>
<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>
<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>
</ul>
)";

View File

@@ -100,8 +100,9 @@ void Clipboard::clearCopiedText()
return;
}
if (m_lastCopied == clipboard->text(QClipboard::Clipboard)
|| m_lastCopied == clipboard->text(QClipboard::Selection)) {
if (!m_lastCopied.isEmpty()
&& (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

@@ -38,6 +38,7 @@
namespace
{
constexpr int clearFormsDelay = 30000;
constexpr int fileExistsCheckInterval = 5000;
bool isQuickUnlockAvailable()
{
@@ -68,6 +69,16 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->editPassword->setShowPassword(false);
});
m_fileExistsTimer.setInterval(fileExistsCheckInterval);
m_fileExistsTimer.setSingleShot(false);
connect(&m_fileExistsTimer, &QTimer::timeout, this, [this] {
if (!QFile::exists(m_filename)) {
m_ui->messageWidget->showMessage(tr("The database file does not exist or is not accessible."),
MessageWidget::Warning,
fileExistsCheckInterval + 500);
}
});
QFont font;
font.setPointSize(font.pointSize() + 4);
font.setBold(true);
@@ -215,6 +226,7 @@ bool DatabaseOpenWidget::event(QEvent* event)
}
if (isVisible()) {
m_fileExistsTimer.start();
m_hideTimer.stop();
pollHardwareKey();
}
@@ -226,6 +238,8 @@ bool DatabaseOpenWidget::event(QEvent* event)
m_hideTimer.start();
}
m_fileExistsTimer.stop();
#ifdef WITH_XC_YUBIKEY
if (type == QEvent::Hide) {
m_deviceListener->deregisterAllHotplugCallbacks();

View File

@@ -97,6 +97,7 @@ private:
bool m_triedToQuit = false;
QTimer m_hideTimer;
QTimer m_hideNoHardwareKeysFoundTimer;
QTimer m_fileExistsTimer;
Q_DISABLE_COPY(DatabaseOpenWidget)
};

View File

@@ -163,24 +163,31 @@ void DatabaseTabWidget::addDatabaseTab(const QString& filePath,
QString canonicalFilePath = fileInfo.canonicalFilePath();
if (canonicalFilePath.isEmpty()) {
emit messageGlobal(tr("Failed to open %1. It either does not exist or is not accessible.").arg(cleanFilePath),
MessageWidget::Error);
return;
// The file does not exist, revert back to the cleaned path for comparison
canonicalFilePath = cleanFilePath;
}
// Try to find an existing tab with the same file path
for (int i = 0, c = count(); i < c; ++i) {
auto* dbWidget = databaseWidgetFromIndex(i);
Q_ASSERT(dbWidget);
if (dbWidget
&& dbWidget->database()->canonicalFilePath().compare(canonicalFilePath, FILE_CASE_SENSITIVE) == 0) {
if (dbWidget) {
auto dbFilePath = dbWidget->database()->canonicalFilePath();
if (dbFilePath.isEmpty()) {
// The file does not exist, revert back to the cleaned path for comparison
dbFilePath = QDir::toNativeSeparators(dbWidget->database()->filePath());
}
if (dbFilePath.compare(canonicalFilePath, FILE_CASE_SENSITIVE) == 0) {
// Attempt to unlock the database if password and/or keyfile is provided
dbWidget->performUnlockDatabase(password, keyfile);
if (!inBackground) {
// switch to existing tab if file is already open
setCurrentIndex(indexOf(dbWidget));
}
// Prevent opening a new tab for this file
return;
}
}
}
auto* dbWidget = new DatabaseWidget(QSharedPointer<Database>::create(cleanFilePath), this);
addDatabaseTab(dbWidget, inBackground);
@@ -674,7 +681,7 @@ void DatabaseTabWidget::updateTabName(int index)
return;
}
index = indexOf(dbWidget);
setTabText(index, tabName(index));
setTabText(index, Tools::escapeAccelerators(tabName(index)));
setTabToolTip(index, dbWidget->displayFilePath());
auto iconIndex = dbWidget->database()->publicIcon();
if (iconIndex >= 0 && iconIndex < databaseIcons()->count()) {

View File

@@ -92,12 +92,14 @@ QIcon Icons::trayIcon(bool unlocked)
}
QIcon i;
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
#if 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,6 +38,7 @@
#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"
@@ -193,9 +194,6 @@ 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);
@@ -641,12 +639,13 @@ 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, [=] {
*hidePreRelWarnConn = connect(
m_ui->globalMessageWidget, &KMessageWidget::hideAnimationFinished, [this, hidePreRelWarn, hidePreRelWarnConn] {
m_ui->globalMessageWidget->removeAction(hidePreRelWarn);
disconnect(*hidePreRelWarnConn);
hidePreRelWarn->deleteLater();
});
connect(hidePreRelWarn, &QAction::triggered, [=] {
connect(hidePreRelWarn, &QAction::triggered, [this] {
m_ui->globalMessageWidget->animatedHide();
config()->set(Config::Messages_HidePreReleaseWarning, KEEPASSXC_VERSION);
});
@@ -715,7 +714,7 @@ void MainWindow::restoreConfigState()
if (config()->get(Config::OpenPreviousDatabasesOnStartup).toBool()) {
const QStringList fileNames = config()->get(Config::LastOpenedDatabases).toStringList();
for (const QString& filename : fileNames) {
if (!filename.isEmpty() && QFile::exists(filename)) {
if (!filename.isEmpty()) {
openDatabase(filename);
}
}
@@ -789,7 +788,7 @@ void MainWindow::updateLastDatabasesMenu()
const QStringList lastDatabases = config()->get(Config::LastDatabases).toStringList();
for (const QString& database : lastDatabases) {
QAction* action = m_ui->menuRecentDatabases->addAction(database);
QAction* action = m_ui->menuRecentDatabases->addAction(Tools::escapeAccelerators(database));
action->setData(database);
m_lastDatabasesActions->addAction(action);
}
@@ -1381,6 +1380,12 @@ 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)
@@ -1538,6 +1543,12 @@ void MainWindow::saveWindowInformation()
}
}
void MainWindow::restoreWindowInformation()
{
restoreGeometry(config()->get(Config::GUI_MainWindowGeometry).toByteArray());
restoreState(config()->get(Config::GUI_MainWindowState).toByteArray());
}
bool MainWindow::saveLastDatabases()
{
if (config()->get(Config::OpenPreviousDatabasesOnStartup).toBool()) {

View File

@@ -160,6 +160,7 @@ private:
static const QString BaseWindowTitle;
void saveWindowInformation();
void restoreWindowInformation();
bool saveLastDatabases();
bool isTrayIconEnabled() const;
void customOpenUrl(QString url);
@@ -192,6 +193,7 @@ 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)
Group* createGroupStructure(Database* db, const QString& groupPath, const QString& rootGroupToSkip)
{
auto group = db->rootGroup();
if (!group || groupPath.isEmpty()) {
@@ -42,8 +42,10 @@ namespace
}
auto nameList = groupPath.split("/", Qt::SkipEmptyParts);
// Skip over first group name if root
if (nameList.first().compare("root", Qt::CaseInsensitive) == 0) {
// Skip the identified root group name if present
if (!rootGroupToSkip.isEmpty() && !nameList.isEmpty()
&& nameList.first().compare(rootGroupToSkip, Qt::CaseInsensitive) == 0) {
nameList.removeFirst();
}
@@ -241,8 +243,26 @@ 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 group = createGroupStructure(db.data(), m_parserModel->data(m_parserModel->index(r, 0)).toString());
auto groupPath = m_parserModel->data(m_parserModel->index(r, 0)).toString();
auto groupName = groupPath.mid(0, groupPath.indexOf('/'));
if (!rootGroupName.isNull() && rootGroupName != groupName) {
rootGroupName.clear();
break;
}
rootGroupName = groupName;
}
if (!rootGroupName.isEmpty()) {
db->rootGroup()->setName(rootGroupName);
}
for (int r = 0; r < rows; ++r) {
auto group =
createGroupStructure(db.data(), m_parserModel->data(m_parserModel->index(r, 0)).toString(), rootGroupName);
if (!group) {
continue;
}

View File

@@ -132,6 +132,9 @@ 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

@@ -56,7 +56,6 @@ 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{
@@ -95,7 +94,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, pDevIface->dbcc_size);
const auto name = QString::fromWCharArray(pDevIface->dbcc_name);
if (m_deviceIdMatch.match(name).hasMatch()) {
emit devicePlugged(m->wParam == DBT_DEVICEARRIVAL, nullptr, pDevIface);
return true;

View File

@@ -213,7 +213,7 @@ namespace KeeShareSettings
}
}
} else {
qWarning("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
qDebug("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 {
qWarning("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
qDebug("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
reader.skipCurrentElement();
}
}
@@ -262,8 +262,7 @@ namespace KeeShareSettings
}
Reference::Reference()
: type(Inactive)
, uuid(QUuid::createUuid())
: uuid(QUuid::createUuid())
{
}
@@ -320,12 +319,21 @@ 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") {
@@ -346,8 +354,10 @@ 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 {
qWarning("Unknown Reference element %s", qPrintable(reader.name().toString()));
qDebug("Unknown Reference element %s", qPrintable(reader.name().toString()));
reader.skipCurrentElement();
}
}
@@ -363,7 +373,11 @@ namespace KeeShareSettings
// Extract RSA key data to serialize an ssh-rsa public key.
// ssh-rsa keys are currently not built into Botan
const auto rsaKey = static_cast<Botan::RSA_PrivateKey*>(sign.certificate.key.data());
// 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 {};
}
std::vector<uint8_t> rsaE(rsaKey->get_e().bytes());
rsaKey->get_e().binary_encode(rsaE.data());

View File

@@ -122,10 +122,11 @@ namespace KeeShareSettings
struct Reference
{
Type type;
Type type = Inactive;
QUuid uuid;
QString path;
QString password;
bool keepGroups = true;
Reference();
bool isNull() const;

View File

@@ -62,6 +62,39 @@ 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();
@@ -75,17 +108,10 @@ namespace
targetRoot->setUpdateTimeinfo(false);
KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference());
targetRoot->setUpdateTimeinfo(updateTimeinfo);
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));
}
cloneIcon(targetMetadata, sourceRoot->database(), targetRoot->iconUuid());
cloneEntries(targetMetadata, sourceRoot, targetRoot);
if (reference.keepGroups) {
cloneChildren(targetMetadata, sourceRoot, targetRoot);
}
auto key = QSharedPointer<CompositeKey>::create();

View File

@@ -43,6 +43,7 @@ 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()));
@@ -97,6 +98,7 @@ 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();
@@ -188,6 +190,7 @@ 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();
@@ -291,3 +294,13 @@ 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,6 +48,7 @@ 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="3" column="1">
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
@@ -171,7 +171,7 @@
</item>
</layout>
</item>
<item row="4" column="0">
<item row="5" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -184,6 +184,19 @@
</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,31 +73,29 @@ bool YubiKey::isInitialized()
bool YubiKey::findValidKeys()
{
// Block operations on hardware keys while scanning
QMutexLocker lock(&s_interfaceMutex);
findValidKeys(lock);
m_connectedKeys = 0;
m_findingKeys = true;
m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys(m_connectedKeys);
m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys(m_connectedKeys);
m_findingKeys = false;
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()); });
}
}
YubiKey::KeyMap YubiKey::foundKeys()
{
QMutexLocker lock(&s_interfaceMutex);
KeyMap foundKeys = m_usbKeys;
foundKeys.unite(m_pcscKeys);
@@ -106,38 +104,12 @@ YubiKey::KeyMap YubiKey::foundKeys()
int YubiKey::connectedKeys()
{
QMutexLocker lock(&s_interfaceMutex);
return m_connectedKeys;
}
QString YubiKey::errorMessage()
{
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;
return m_error;
}
/**
@@ -175,25 +147,31 @@ bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
YubiKey::ChallengeResult
YubiKey::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
{
QMutexLocker lock(&s_interfaceMutex);
m_error.clear();
// Make sure we tried to find available keys
if (m_usbKeys.isEmpty() && m_pcscKeys.isEmpty()) {
findValidKeys(lock);
// 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;
}
if (m_usbKeys.contains(slot)) {
return YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
// 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_pcscKeys.contains(slot)) {
return YubiKeyInterfacePCSC::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);
}
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;
return ret;
}

View File

@@ -44,7 +44,8 @@ public:
{
YCR_ERROR = 0,
YCR_SUCCESS = 1,
YCR_WOULDBLOCK = 2
YCR_WOULDBLOCK = 2,
YCR_KEYNOTFOUND = 3,
};
static YubiKey* instance();
@@ -84,14 +85,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_ERROR;
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
}
// Try for a few seconds to find the key
@@ -710,11 +710,8 @@ 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_ERROR;
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
}
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_ERROR;
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
}
auto yk_key = openKeySerial(slot.first);
@@ -245,12 +245,11 @@ 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_ERROR;
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
}
emit challengeStarted();
auto ret = performChallenge(yk_key.get(), slot.second, true, challenge, response);
emit challengeCompleted();
return ret;

View File

@@ -22,6 +22,7 @@
#include <QTest>
#include "core/Group.h"
#include "core/Tools.h"
#include "core/Totp.h"
#include "crypto/Crypto.h"
#include "format/CsvExporter.h"
@@ -110,3 +111,218 @@ 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,6 +39,10 @@ private slots:
void testExport();
void testEmptyDatabase();
void testNestedGroups();
void testRoundTripWithCustomRootName();
void testRoundTripWithDefaultRootName();
void testSingleLevelGroup();
void testAbsolutePaths();
private:
QSharedPointer<Database> m_db;

View File

@@ -626,6 +626,17 @@ 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& makdown : Markdowns) {
QCOMPARE(Tools::getMimeType(QFileInfo(makdown)), Tools::MimeType::Markdown);
for (const auto& markdown : Markdowns) {
QCOMPARE(Tools::getMimeType(QFileInfo(markdown)), Tools::MimeType::Markdown);
}
const QStringList UnknownHeaders = {"test.doc", "test.pdf", "test.docx"};
@@ -451,3 +451,14 @@ 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,6 +43,7 @@ private slots:
void testIsTextMimeType();
void testCleanUsername();
void testCleanUsername_data();
void testEscapeAccelerators();
};
#endif // KEEPASSX_TESTTOOLS_H

View File

@@ -2464,6 +2464,41 @@ void TestGui::testMenuActionStates()
QVERIFY(isActionEnabled("actionPasswordGenerator"));
}
void TestGui::testOpenMissingDatabaseFile()
{
// Test that when trying to open a non-existent database file,
// the unlock dialog is still shown (instead of auto-closing)
// This allows user to retry when the file becomes available (e.g., cloud storage mounting)
const QString nonExistentPath = "/tmp/does_not_exist.kdbx";
// Ensure the file doesn't exist
QFile::remove(nonExistentPath);
QVERIFY(!QFile::exists(nonExistentPath));
// Record initial tab count
int initialTabCount = m_tabWidget->count();
// Try to add database tab with non-existent file
// This should NOT fail but should create a tab and show unlock dialog
m_tabWidget->addDatabaseTab(nonExistentPath);
// Verify that a tab was created (unlock dialog shown)
QCOMPARE(m_tabWidget->count(), initialTabCount + 1);
// Get the database widget for the new tab
auto* dbWidget = m_tabWidget->currentDatabaseWidget();
QVERIFY(dbWidget);
// Verify the database is in a state where it can be unlocked
// (not closed/rejected due to missing file)
QVERIFY(dbWidget->isLocked());
// Close the tab to clean up
m_tabWidget->closeDatabaseTab(m_tabWidget->currentIndex());
QCOMPARE(m_tabWidget->count(), initialTabCount);
}
void TestGui::addCannedEntries()
{
// Find buttons

View File

@@ -71,6 +71,7 @@ private slots:
void testTrayRestoreHide();
void testShortcutConfig();
void testMenuActionStates();
void testOpenMissingDatabaseFile();
private:
void addCannedEntries();

View File

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

View File

@@ -1,67 +1,6 @@
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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=
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=

View File

@@ -1,77 +1,70 @@
#!/usr/bin/env python3
from collections import defaultdict
import json
import os
import sys
from pathlib import Path
from urllib import request
# 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
txrc = Path.home() / '.transifexrc'
if not txrc.exists():
print('No Transifex config found. Run tx init first.')
sys.exit(1)
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))",
}
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']
TEMPLATE = "<li><strong>{0}</strong>: {1}</li>\n"
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)
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)
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)
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
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'!")
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>')

View File

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