Compare commits

..

5 Commits

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

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

View File

@@ -2,10 +2,10 @@ name: "CodeQL"
on:
push:
branches:
- 'develop'
- 'release/**'
branches: [ 'develop', 'release/2.7.x' ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'develop' ]
schedule:
- cron: '5 16 * * 3'

View File

@@ -49,8 +49,6 @@ This section contains full details on advanced features available in KeePassXC.
|{DB_DIR} |Absolute directory path of database file
|===
NOTE: You can insert literal placeholder strings by escaping the beginning and ending curly braces. For example, to insert the string `{USERNAME}`, you would type `++\{USERNAME\}++`.
=== Entry Cross-Reference
A reference to another entry's field is possible using the shorthand syntax:
`{REF:<FIELD>@<SEARCH_IN>:<SEARCH_TEXT>}`

View File

@@ -67,6 +67,10 @@ if(UNIX AND NOT APPLE AND NOT HAIKU)
install(FILES linux/${APP_ID}.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
endif(UNIX AND NOT APPLE AND NOT HAIKU)
if(APPLE)
install(FILES macosx/keepassxc.icns DESTINATION ${DATA_INSTALL_DIR})
endif()
if(WIN32)
install(FILES windows/qt.conf DESTINATION ${BIN_INSTALL_DIR})
endif()
@@ -81,16 +85,7 @@ add_custom_command(TARGET icons
if(APPLE)
add_custom_command(TARGET icons
POST_BUILD
COMMAND xcrun actool share/macosx/keepassxc.icon
--compile share/macosx
--output-partial-info-plist /dev/null
--app-icon keepassxc
--include-all-app-icons
--enable-on-demand-resources NO
--target-device mac
--minimum-deployment-target 11.0
--platform macosx
--output-format human-readable-text
COMMAND png2icns macosx/keepassxc.icns icons/application/256x256/apps/keepassxc.png
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endif()

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

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

Binary file not shown.

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128"><defs><style>.cls-1{fill:none;}.cls-2{fill:url(#radial-gradient-2);mix-blend-mode:lighten;opacity:.7;}.cls-3{fill:url(#radial-gradient);}.cls-4{isolation:isolate;}</style><radialGradient id="radial-gradient" cx="315.9556" cy="395.3416" fx="315.9556" fy="395.3416" r="239.1689" gradientTransform="translate(-46.999 -42.8948) scale(.3539 .2026)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2e6b26"/><stop offset="1" stop-color="#6ab536"/></radialGradient><radialGradient id="radial-gradient-2" cx="314.1662" cy="394.0804" fx="314.1662" fy="394.0804" r="46.7089" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6ab536"/><stop offset="1" stop-color="#2e6b26"/></radialGradient></defs><g class="cls-4"><g id="Layer_2"><g id="Icon_macOS"><rect class="cls-1" width="128" height="128"/><path id="Background" class="cls-3" d="M63.9999,24.1601c-21.9679,0-39.84,17.8721-39.84,39.8399s17.8721,39.8399,39.84,39.8399,39.8399-17.8722,39.8399-39.8399-17.8719-39.8399-39.8399-39.8399Z"/><path id="Lighten_Hole" class="cls-2" d="M63.9998,27.4724c-6.5434,0-11.8668,5.3234-11.8668,11.8668s5.3234,11.8668,11.8668,11.8668,11.8668-5.3235,11.8668-11.8668-5.3234-11.8668-11.8668-11.8668Z"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128"><defs><style>.cls-1{fill:none;}.cls-2{fill:url(#radial-gradient);opacity:.3;}.cls-2,.cls-3{mix-blend-mode:multiply;}.cls-4{fill:url(#radial-gradient-2);}.cls-5{isolation:isolate;}.cls-3{fill:#0f0f0d;opacity:.2;}</style><radialGradient id="radial-gradient" cx="322.2841" cy="405.7418" fx="322.2841" fy="405.7418" r="68.9894" gradientTransform="translate(-44.2199 -179.8556) scale(.3356 .5572)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000"/><stop offset=".7842" stop-color="#4f4f4f" stop-opacity="0"/></radialGradient><radialGradient id="radial-gradient-2" cx="313.1713" cy="380.0413" fx="313.1713" fy="380.0413" r="159.6501" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f5f5f5"/><stop offset=".5036" stop-color="#f2f2f2"/></radialGradient></defs><g class="cls-5"><g id="Layer_2"><g id="Icon_macOS"><rect class="cls-1" width="128" height="128"/><path id="Key_Double_Shadow" class="cls-2" d="M43.0622,29.9746c-.0019.0137-.0038.027-.0058.0406-.0518.0097-.0526-.0026.0058-.0406ZM73.1463,55.5462l-.551,9.8444,6.5017,7.0832-6.5017,7.0832,4.2977,4.6821-4.2977,4.6821.551,9.764h-18.2929v-43.139c-7.2731-3.7217-12.232-11.7653-12.232-21.0094,0-1.5471.2166-2.9765.435-4.5215.789-.1487,13.5481-5.4962,13.5481-5.4962-.1059.578,14.8965.578,14.7906,0,0,0,12.3269,5.3041,13.5637,5.5642.3128,1.5189.4195,2.9293.4195,4.4534,0,9.2442-5.0691,17.2878-12.232,21.0095ZM84.9375,29.9746c.0077.0365.0138.0722.0213.1086.1354.0285.14-.0025-.0213-.1086Z"/><path id="Key_Drop_Shadow" class="cls-3" d="M42.6543,90.0228c-7.0698-6.1486-11.5457-15.2121-11.5457-25.3548,0-11.8019,6.172-22.696,16.3377-28.7786-.1815,1.1802-.3631,2.2696-.3631,3.4498,0,6.9903,4.0844,13.0729,10.0749,15.8872v31.8652l7.5335,7.5351,7.5335-7.5351-.4538-6.6272,3.5398-3.5406-3.5398-3.5406,5.3551-5.3563-5.3551-5.3563.4538-7.4443c5.8997-2.8143,10.0749-8.8968,10.0749-15.8872,0-1.1802-.0908-2.2696-.3631-3.4498,3.6888,2.227,6.8516,5.1011,9.3871,8.4266-6.1315-8.4206-16.0924-13.8991-27.3404-13.8991-18.6365,0-33.7443,15.0357-33.7443,33.5832,0,10.495,4.8383,19.8644,12.4149,26.0227ZM62.241,84.5497h-2.5414v-25.1472h2.5414v25.1472ZM58.6104,31.7133c1.9968-.3631,4.0844-.5447,6.0813-.5447,2.0876-.0908,4.0844.1815,6.0813.5447.0908.4539.1815.9078.1815,1.3617,0,3.4498-2.8137,6.2641-6.2628,6.2641s-6.2628-2.8143-6.2628-6.2641c0-.4539.0908-.9078.1815-1.3617Z"/><path id="Key" class="cls-4" d="M63.9999,24.1601c-21.9582,0-39.8225,17.8721-39.8225,39.8399s17.8643,39.8399,39.8225,39.8399,39.8223-17.8723,39.8223-39.8399-17.8641-39.8399-39.8223-39.8399ZM57.9186,31.0088c1.9968-.3631,4.0844-.5447,6.0813-.5447,2.0876-.0908,4.0844.1815,6.0813.5447.0908.4539.1815.9078.1815,1.3617,0,3.4498-2.8137,6.2641-6.2628,6.2641s-6.2628-2.8143-6.2628-6.2641c0-.4539.0908-.9078.1815-1.3617ZM61.5492,58.6979v25.1472h-2.5414v-25.1472h2.5414ZM63.9999,97.5535c-18.5161,0-33.5831-14.9794-33.5831-33.5901,0-11.8019,6.172-22.696,16.3377-28.7786-.1815,1.1802-.3631,2.2696-.3631,3.4498,0,6.9903,4.0844,13.0729,10.0749,15.8872v31.8652l7.5335,7.5351,7.5335-7.5351-.4538-6.6272,3.5398-3.5406-3.5398-3.5406,5.3551-5.3563-5.3551-5.3563.4538-7.4443c5.8997-2.8143,10.0749-8.8968,10.0749-15.8872,0-1.1802-.0908-2.2696-.3631-3.4498,10.0749,6.0826,16.247,16.9766,16.3377,28.7786,0,18.5199-14.9763,33.5901-33.5831,33.5901Z"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128"><defs><style>.cls-1{fill:none;}.cls-2{fill:url(#radial-gradient-2);opacity:.44;}.cls-3{fill:#0f0f0d;opacity:.35;}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:url(#linear-gradient);}.cls-6{isolation:isolate;}.cls-7{fill:url(#radial-gradient);mix-blend-mode:lighten;opacity:.32;}.cls-8{opacity:.6;}.cls-9{fill:rgba(15,15,13,.35);}</style><linearGradient id="linear-gradient" x1="63.9998" y1="20.4513" x2="63.9998" y2="107.5488" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000"/><stop offset="1" stop-color="#000" stop-opacity=".2"/></linearGradient><linearGradient id="linear-gradient-2" x1="63.9998" y1="15.9186" x2="63.9998" y2="102.9186" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#414141"/><stop offset=".196" stop-color="#3e3e3e"/><stop offset="1" stop-color="#3a3a3a"/></linearGradient><radialGradient id="radial-gradient" cx="305.8904" cy="449.6754" fx="305.8904" fy="449.6754" r="148.0242" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset=".6733" stop-color="#fcfcfc" stop-opacity="0"/><stop offset="1" stop-color="#fcfcfc"/></radialGradient><radialGradient id="radial-gradient-2" cx="311.8517" cy="469.1737" fx="311.8517" fy="469.1737" r="123.1352" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset=".1088" stop-color="#0f0f0d" stop-opacity="0"/><stop offset=".8856" stop-color="#414141" stop-opacity="0"/><stop offset=".9339" stop-color="#1e1e1d" stop-opacity=".6841"/><stop offset=".9874" stop-color="#0f0f0d"/></radialGradient></defs><g class="cls-6"><g id="Layer_2"><g id="Icon_macOS"><rect class="cls-1" width="128" height="128"/><g id="Countour"><g class="cls-8"><path class="cls-5" d="M63.9999,20.8039c23.7037,0,42.9881,19.3777,42.9881,43.1962s-19.2844,43.1962-42.9881,43.1962-42.9882-19.3777-42.9882-43.1962S40.2961,20.8039,63.9999,20.8039M63.9999,20.4513c-23.8983,0-43.3408,19.5358-43.3408,43.5487s19.4425,43.5487,43.3408,43.5487,43.3406-19.536,43.3406-43.5487-19.4423-43.5487-43.3406-43.5487h0Z"/></g></g><path id="Rim" class="cls-4" d="M63.9999,107.5c-23.9861,0-43.5001-19.5141-43.5001-43.5S40.0138,20.5,63.9999,20.5s43.4999,19.5139,43.4999,43.5-19.5138,43.5-43.4999,43.5ZM63.9999,24.1601c-21.9582,0-39.8225,17.8721-39.8225,39.8399s17.8643,39.8399,39.8225,39.8399,39.8223-17.8723,39.8223-39.8399-17.8641-39.8399-39.8223-39.8399Z"/><path id="Bottom_Light" class="cls-7" d="M63.9999,20.517c-23.9767,0-43.4831,19.5063-43.4831,43.483s19.5064,43.483,43.4831,43.483,43.4829-19.5065,43.4829-43.483-19.5062-43.483-43.4829-43.483Z"/><path id="Inner_Shadow" class="cls-2" d="M63.9999,107.5c-23.9861,0-43.5001-19.5141-43.5001-43.5S40.0138,20.5,63.9999,20.5s43.4999,19.5139,43.4999,43.5-19.5138,43.5-43.4999,43.5ZM63.9999,24.1601c-21.9582,0-39.8225,17.8721-39.8225,39.8399s17.8643,39.8399,39.8225,39.8399,39.8223-17.8723,39.8223-39.8399-17.8641-39.8399-39.8223-39.8399Z"/><path id="Bottom_Drop_Shadow" class="cls-9" d="M63.9999,24.3931c-22.1274,0-40.1292,18.0018-40.1292,40.1291s18.0018,40.1291,40.1292,40.1291,40.129-18.002,40.129-40.1291-18.0017-40.1291-40.129-40.1291Z"/><path id="Top_Drop_Shadow" class="cls-3" d="M63.9999,107.5c-23.9861,0-43.5001-19.5141-43.5001-43.5S40.0138,20.5,63.9999,20.5s43.4999,19.5139,43.4999,43.5-19.5138,43.5-43.4999,43.5ZM63.9999,21.431c-23.986,0-43.5001,19.2544-43.5001,42.9213s19.5141,42.9213,43.5001,42.9213,43.4999-19.2546,43.4999-42.9213-19.5138-42.9213-43.4999-42.9213Z"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,99 +0,0 @@
{
"fill-specializations" : [
{
"value" : "automatic"
},
{
"appearance" : "dark",
"value" : "automatic"
}
],
"groups" : [
{
"blur-material" : null,
"layers" : [
{
"glass" : true,
"image-name" : "macos-comp-key.svg",
"name" : "key",
"position" : {
"scale" : 9.5,
"translation-in-points" : [
0,
0
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.3
}
},
{
"blur-material" : null,
"layers" : [
{
"glass" : true,
"image-name" : "macos-comp-bg-green.svg",
"name" : "bg-green",
"position" : {
"scale" : 9.5,
"translation-in-points" : [
0,
0
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.5
}
},
{
"blur-material" : 0.5,
"hidden" : false,
"layers" : [
{
"glass" : true,
"image-name" : "macos-comp-rim.svg",
"name" : "rim",
"position" : {
"scale" : 9.5,
"translation-in-points" : [
0,
0
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.7
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.2
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View File

@@ -657,10 +657,6 @@
<source>Convenience</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enable database quick unlock (Touch ID / Windows Hello)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Lock databases when session is locked or lid is closed</source>
<translation type="unfinished"></translation>
@@ -705,6 +701,14 @@
<source>Hide notes in the entry preview panel</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enable database quick unlock by default</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remember quick unlock after database is closed</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AttachmentWidget</name>
@@ -1654,10 +1658,6 @@ Backup database located at %2</source>
<source>Unlock Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unlock</source>
<translation type="unfinished"></translation>
@@ -1735,10 +1735,6 @@ To prevent this error from appearing, you must go to &quot;Database Settings / S
<source>Cannot use database file as key file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>authenticate to access the database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to authenticate with Quick Unlock: %1</source>
<translation type="unfinished"></translation>
@@ -1791,6 +1787,14 @@ Are you sure you want to continue with this file?.</source>
<source>&lt;a href=&quot;#&quot; style=&quot;text-decoration: underline&quot;&gt;I have a key file&lt;/a&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reset</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware keys found, but no slots are configured.</source>
<translation type="unfinished"></translation>
@@ -1799,6 +1803,10 @@ Are you sure you want to continue with this file?.</source>
<source>Press ESC again to close this database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Quick Unlock</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingWidgetMetaData</name>
@@ -3735,14 +3743,6 @@ Supported extensions are: %1.</source>
<source>Select import/export file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Maintain group structure with shared database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Keep Group Structure</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EditGroupWidgetMain</name>
@@ -9134,46 +9134,10 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Passkeys</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES initialization failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES encrypt failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to store in Linux Keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit returned an error: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not locate key in keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not read key in keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES decrypt failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Polkit authentication agent was available</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit authorization failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Quick Unlock provider is available</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to init KeePassXC crypto.</source>
<translation type="unfinished"></translation>
@@ -9182,10 +9146,6 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Failed to encrypt key data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to get Windows Hello credential.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to decrypt key data.</source>
<translation type="unfinished"></translation>
@@ -9349,7 +9309,35 @@ This option is deprecated, use --set-key-file instead.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Format to use when exporting. Available choices are &apos;xml&apos;, &apos;csv&apos; or &apos;html&apos;. Defaults to &apos;xml&apos;.</source>
<source>Quick Unlock Pin Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pin setup was canceled. Quick unlock has not been enabled.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to get credentials for quick unlock.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter quick unlock pin (%1 of %2 attempts):</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pin entry was canceled.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Polkit authentication agent was available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit authorization failed.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Windows Hello setup was canceled or failed. Quick unlock has not been enabled.</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -9433,6 +9421,34 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Confirm Replace Entry References</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Format to use when exporting. Available choices are &apos;xml&apos;, &apos;csv&apos; or &apos;html&apos;. Defaults to &apos;xml&apos;.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter a %1%2 digit pin to use for quick unlock:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to derive key using Argon2</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Too many pin attempts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No key is stored for this database.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to obtain session key.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to retrieve Windows Hello credential.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtIOCompressor</name>
@@ -10598,7 +10614,11 @@ Example: JBSWY3DPEHPK3PXP</source>
<context>
<name>YubiKey</name>
<message>
<source>Could not find hardware key with serial number %1. Please connect it to continue.</source>
<source>General: </source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not find interface for hardware key with serial number %1. Please connect it to continue.</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -10659,6 +10679,10 @@ Example: JBSWY3DPEHPK3PXP</source>
</context>
<context>
<name>YubiKeyInterfacePCSC</name>
<message>
<source>Could not find or access hardware key with serial number %1. Please present it to continue. </source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware key is locked or timed out. Unlock or re-present it to continue.</source>
<translation type="unfinished"></translation>

View File

@@ -217,6 +217,7 @@ set(gui_SOURCES
gui/wizard/NewDatabaseWizardPageEncryption.cpp
gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp
quickunlock/QuickUnlockInterface.cpp
quickunlock/PinUnlock.cpp
../share/icons/icons.qrc
../share/wizard/wizard.qrc)
@@ -227,40 +228,41 @@ if(APPLE)
gui/osutils/macutils/ScreenLockListenerMac.cpp
gui/osutils/macutils/AppKitImpl.mm
gui/osutils/macutils/AppKit.h
quickunlock/TouchID.mm)
# TODO: Remove -Wno-error once deprecation warnings have been resolved.
set_source_files_properties(quickunlock/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast")
quickunlock/TouchID.cpp)
endif()
if(UNIX AND NOT APPLE)
list(APPEND gui_SOURCES
gui/osutils/nixutils/ScreenLockListenerDBus.cpp
gui/osutils/nixutils/NixUtils.cpp)
if("${CMAKE_SYSTEM}" MATCHES "Linux")
list(APPEND core_SOURCES
quickunlock/Polkit.cpp
quickunlock/PolkitDbusTypes.cpp)
endif()
if(WITH_XC_X11)
list(APPEND gui_SOURCES
gui/osutils/nixutils/X11Funcs.cpp)
endif()
# Polkit is only available on Linux systems
if("${CMAKE_SYSTEM}" MATCHES "Linux")
list(APPEND gui_SOURCES
quickunlock/Polkit.cpp
quickunlock/PolkitDbusTypes.cpp)
set_source_files_properties(
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
PROPERTIES
INCLUDE "quickunlock/PolkitDbusTypes.h"
)
qt5_add_dbus_interface(gui_SOURCES
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
polkit_dbus
)
endif()
# dbus support
qt5_add_dbus_adaptor(gui_SOURCES
gui/org.keepassxc.KeePassXC.MainWindow.xml
gui/MainWindow.h
MainWindow)
set_source_files_properties(
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
PROPERTIES
INCLUDE "quickunlock/PolkitDbusTypes.h"
)
qt5_add_dbus_interface(core_SOURCES
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
polkit_dbus
)
find_library(KEYUTILS_LIBRARIES NAMES keyutils)
if(NOT KEYUTILS_LIBRARIES)
message(FATAL_ERROR "Could not find libkeyutils")
@@ -421,12 +423,7 @@ if(WIN32)
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 ON)
include(GenerateProductVersion)
generate_product_version(
WIN32_ResourceFiles
@@ -437,24 +434,20 @@ if(WIN32)
VERSION_PATCH ${KEEPASSXC_VERSION_PATCH}
)
list(APPEND WIN32_ResourceFiles "${CMAKE_SOURCE_DIR}/share/windows/icon.rc")
target_sources(${PROGNAME} PUBLIC ${WIN32_ResourceFiles})
endif()
elseif(APPLE AND WITH_APP_BUNDLE)
set(MACOSX_BUNDLE_IDENTIFIER org.keepassxc.keepassxc)
set(MACOSX_BUNDLE_ICON_NAME keepassxc)
configure_file("${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake" ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
install(FILES "${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile" DESTINATION ${BUNDLE_INSTALL_DIR})
set(MACOSX_BUNDLE_RESOURCE_FILES
"${CMAKE_SOURCE_DIR}/share/macosx/Assets.car"
"${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.icns"
)
add_executable(${PROGNAME} WIN32 main.cpp ${WIN32_ResourceFiles})
target_link_libraries(${PROGNAME} keepassxc_gui)
set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
# macOS App Bundle
if(APPLE AND WITH_APP_BUNDLE)
install(FILES ${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile DESTINATION ${BUNDLE_INSTALL_DIR})
configure_file(${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
set_target_properties(${PROGNAME} PROPERTIES
MACOSX_BUNDLE ON
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist"
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements"
RESOURCE "${MACOSX_BUNDLE_RESOURCE_FILES}"
)
target_sources(${PROGNAME} PUBLIC ${MACOSX_BUNDLE_RESOURCE_FILES})
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements")
if(QT_MAC_USE_COCOA AND EXISTS "${QT_LIBRARY_DIR}/Resources/qt_menu.nib")
install(DIRECTORY "${QT_LIBRARY_DIR}/Resources/qt_menu.nib"

View File

@@ -139,7 +139,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Security_ClearSearch, {QS("Security/ClearSearch"), Roaming, false}},
{Config::Security_ClearSearchTimeout, {QS("Security/ClearSearchTimeout"), Roaming, 5}},
{Config::Security_HideNotes, {QS("Security/Security_HideNotes"), Roaming, false}},
{Config::Security_LockDatabaseIdle, {QS("Security/LockDatabaseIdle"), Roaming, true}},
{Config::Security_LockDatabaseIdle, {QS("Security/LockDatabaseIdle"), Roaming, false}},
{Config::Security_LockDatabaseIdleSeconds, {QS("Security/LockDatabaseIdleSeconds"), Roaming, 240}},
{Config::Security_LockDatabaseMinimize, {QS("Security/LockDatabaseMinimize"), Roaming, false}},
{Config::Security_LockDatabaseScreenLock, {QS("Security/LockDatabaseScreenLock"), Roaming, true}},
@@ -155,7 +155,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}},
{Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}},
{Config::Security_QuickUnlock, {QS("Security/QuickUnlock"), Local, true}},
{Config::Security_DatabasePasswordMinimumQuality, {QS("Security/DatabasePasswordMinimumQuality"), Local, 0}},
{Config::Security_QuickUnlockRemember, {QS("Security/QuickUnlockRemember"), Local, true}},
// Browser
{Config::Browser_Enabled, {QS("Browser/Enabled"), Roaming, false}},

View File

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

View File

@@ -1138,15 +1138,6 @@ QString Entry::resolveMultiplePlaceholdersRecursive(const QString& str, int maxD
return str;
}
// Short circuit if we have escaped the placeholder brackets
if (str.startsWith("\\{") && str.endsWith("\\}")) {
// Replace the escaped brackets with actuals and move on
auto ret = str;
ret.replace(0, 2, "{");
ret.replace(ret.size() - 2, 2, "}");
return ret;
}
QString result;
auto matches = placeholderRegEx.globalMatch(str);
int capEnd = 0;

View File

@@ -449,11 +449,6 @@ namespace Tools
return userName;
}
QString escapeAccelerators(QString string)
{
return string.replace("&", "&&");
}
QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties)
{
QVariantMap result;

View File

@@ -49,7 +49,6 @@ namespace Tools
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment());
QString cleanFilename(QString filename);
QString cleanUsername();
QString escapeAccelerators(QString string);
template <class T> QSet<T> asSet(const QList<T>& a)
{

View File

@@ -163,6 +163,13 @@ QVariantMap Argon2Kdf::writeParameters()
bool Argon2Kdf::transform(const QByteArray& raw, QByteArray& result) const
{
// This is a programming error and will result in broken encryption
Q_ASSERT(*raw != *result);
if (*raw == *result) {
qWarning("Argon2Kdf: Input and output buffers must not be the same.");
return false;
}
result.clear();
result.resize(32);
// Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length

View File

@@ -44,43 +44,29 @@ static const QString aboutContributors = R"(
<li>Sergey Vilgelm</li>
<li>Victor Engmark</li>
<li>NarwhalOfAges</li>
<li>No Name</li>
<li>SG</li>
<li>Riley Moses</li>
<li>Esteban Martinez</li>
<li>Marc Morocutti</li>
<li>Zivix</li>
<li>Benedikt Heine</li>
<li>Hugo Locurcio</li>
<li>William Petrides</li>
<li>Gunar Gessner</li>
<li>Christian Wittenhorst</li>
<li>Matt Cardarelli</li>
<li>Paul Ammann</li>
<li>Steve Isom</li>
<li>GodSpell</li>
<li>Lionel Laské</li>
<li>Daniel Epp</li>
<li>Oleksii Aleksieiev</li>
<li>Julian Stier</li>
<li>Ruben Schade</li>
<li>Bernhard</li>
<li>Wojciech Kozlowski</li>
<li>Caleb Currie</li>
<li>Morgan Courbet</li>
<li>Kyle Kneitinger</li>
<li>Chris Sohns</li>
<li>Shmavon Gazanchyan</li>
<li>xjdwc</li>
<li>Riley Moses</li>
<li>Igor Zinovik</li>
<li>Jeff</li>
<li>Esteban Martinez</li>
<li>Max Andersen</li>
<li>Zivix</li>
<li>Marc Morocutti</li>
<li>super scampy</li>
<li>Hugo Locurcio</li>
<li>Benedikt Heine</li>
<li>Mischa Peters</li>
<li>Rainer-Maria Fritsch</li>
<li>Micha Ober</li>
<li>Ivan Gromov</li>
<li>William Petrides</li>
<li>Joshua Go</li>
<li>Gunar Gessner</li>
<li>pancakeplant</li>
<li>Hans-Joachim Forker</li>
<li>Nicolas Vandemaele</li>
@@ -89,66 +75,30 @@ static const QString aboutContributors = R"(
<li>Mike</li>
<li>Thomas Renz</li>
<li>Toby Cline</li>
<li>Christian Wittenhorst</li>
<li>Paul Ammann</li>
<li>Matt Cardarelli</li>
<li>Steve Isom</li>
<li>Emre Dessoi</li>
<li>Wojciech Kozlowski</li>
<li>Michael Babnick</li>
<li>kernellinux</li>
<li>Patrick Evans</li>
<li>Marco</li>
<li>GodSpell</li>
<li>Jeremy Rubin</li>
<li>Korbi</li>
<li>andreas</li>
<li>Tyche's tidings</li>
<li>Daniel Kuebler</li>
<li>Brandon Corujo</li>
<li>AheroX</li>
<li>Alexandre G</li>
<li>AshinaGa</li>
<li>BYTEBOLT</li>
<li>CEH</li>
<li>Chris Stone</li>
<li>Christof Böckler</li>
<li>Claude</li>
<li>CzLer</li>
<li>Daniel Burridge</li>
<li>dark</li>
<li>Dave G</li>
<li>David Bowers</li>
<li>dickv</li>
<li>fp4</li>
<li>Huser IT Solutions GmbH</li>
<li>irf</li>
<li>Isaiah Rahmany</li>
<li>JackNYC</li>
<li>Jacob Emmert-Aronson</li>
<li>John Donadeo</li>
<li>Kosta Medinsky</li>
<li>leinad987</li>
<li>Lux</li>
<li>marek</li>
<li>mattlongname</li>
<li>mattock</li>
<li>Max Christian Pohle</li>
<li>nta/norma</li>
<li>picatsv</li>
<li>proto</li>
<li>Raymond Lau</li>
<li>Waido</li>
<li>Weinmann Willy</li>
<li>WildMage</li>
</ul>
<h3>VIP GitHub Sponsors:</h3>
<ul>
<li>mercedes-benz</li>
<li>tiangolo</li>
<li>mrniko</li>
<li>rszamszur</li>
</ul>
<h3>Notable Code Contributions:</h3>
<ul>
<li>droidmonkey</li>
<li>phoerious</li>
<li>varjolintu (Browser Integration)</li>
<li>louib (CLI)</li>
<li>varjolintu (Browser Integration)</li>
<li>hifi (SSH Agent)</li>
<li>xvallspl (Tags)</li>
<li>Aetf (FdoSecrets Storage Server)</li>
@@ -159,8 +109,6 @@ static const QString aboutContributors = R"(
<li>brainplot (many improvements)</li>
<li>kneitinger (many improvements)</li>
<li>frostasm (many improvements)</li>
<li>libklein (many improvements)</li>
<li>w15eacre (many improvements)</li>
<li>fonic (Entry Table View)</li>
<li>kylemanna (YubiKey)</li>
<li>c4rlo (Offline HIBP Checker)</li>
@@ -173,145 +121,179 @@ static const QString aboutContributors = R"(
</ul>
<h3>Patreon Supporters:</h3>
<ul>
<li>Richard Ames</li>
<li>Bernhard</li>
<li>Christian Rasmussen</li>
<li>Nuutti Toivola</li>
<li>Lionel Laské</li>
<li>Tyler Gass</li>
<li>NZSmartie</li>
<li>Darren</li>
<li>Brad</li>
<li>Oleksii Aleksieiev</li>
<li>Julian Stier</li>
<li>Daniel Epp</li>
<li>Ruben Schade</li>
<li>William Komanetsky</li>
<li>Niels Ganser</li>
<li>judd</li>
<li>Tarek Sherif</li>
<li>Eugene</li>
<li>CYB3RL4MBD4</li>
<li>Alexanderjb</li>
<li>Justin Carroll</li>
<li>Bart Libert</li>
<li>Shintaro Matsushima</li>
<li>Thammachart Chinvarapon</li>
<li>Gernot Premper</li>
<li>SLmanDR</li>
<li>Paul Ellenbogen</li>
<li>John C</li>
<li>Markus</li>
<li>Crimson Idol</li>
<li>Steven</li>
<li>Ellie</li>
<li>Anthony Avina</li>
<li>PlushElderGod</li>
<li>Markus Wochnik</li>
<li>Clark Henry</li>
<li>zapscribe</li>
<li>Salt Rock Lamp</li>
<li>Steven Crowley</li>
<li>Ralph Azucena</li>
<li>Guruprasad Kulkarni</li>
<li>jose</li>
<li>Michael Gulick</li>
<li>J Doty</li>
<li>Synchro11</li>
<li>Michael Soares</li>
<li>Johannes Felko</li>
<li>Ellie</li>
<li>David Walluscheck</li>
<li>Anthony Avina</li>
<li>pro</li>
<li>Mark Luxton</li>
<li>Crimson Idol</li>
<li>Björn König</li>
<li>René Weselowski</li>
<li>gonczor</li>
<li>PlushElderGod</li>
<li>gilgwath</li>
<li>Tobias</li>
<li>Christopher Hillenbrand</li>
<li>Daddy's c$sh</li>
<li>Ashura</li>
<li>Florian</li>
<li>Alexandre</li>
<li>Dave Jones</li>
<li>Brett</li>
<li>Ralph Azucena</li>
<li>Florian</li>
<li>Jim Vanderbilt</li>
<li>Brian McGuire</li>
<li>Sid Beske</li>
<li>Dmitrii Galinskii</li>
<li>Johannes Erchen</li>
<li>Brandon Zhang</li>
<li>Maxley Fraser</li>
<li>Nikul Savasadia</li>
<li>Claude</li>
<li>alga</li>
<li>Philipp Jetschina</li>
<li>Kristoffer Winther Balling</li>
<li>Peter Link</li>
<li>David S H Rosenthal</li>
<li>Michael Soares</li>
<li>Vlad Didenko</li>
<li>henloo</li>
<li>Erik Rigtorp</li>
<li>Barry McKenzie</li>
<li>Sebastian van der Est</li>
<li>J.C. Polanycia</li>
<li>Peter Upfold</li>
<li>Josh Yates-Walker</li>
<li>Adam</li>
<li>HJ</li>
<li>bjorndown</li>
<li>Vlastimil Vondra</li>
<li>Tony Wang</li>
<li>John Sivak</li>
<li>Nol Aders</li>
<li>Dirk Bergstrom</li>
<li>proco</li>
<li>Philipp Baderschneider</li>
<li>Charlie Drake</li>
<li>Ryan Goldstein</li>
<li>Doug Witt</li>
<li>David S H Rosenthal</li>
<li>Lance Simmons</li>
<li>Mathew Woodyard</li>
<li>GanderPL</li>
<li>Neša</li>
<li>Dimitris Kogias</li>
<li>Robin Hellsten</li>
<li>Scott Williams</li>
<li>klepto68</li>
<li>Uwe S.</li>
<li>codiflow</li>
<li>eugene</li>
<li>Anton Fisher</li>
<li>David Daly</li>
<li>Crispy_Steak</li>
<li>Cilestin</li>
<li>Benjamin</li>
<li>Daniel Lakeland</li>
<li>erinacio</li>
<li>Leo</li>
<li>Payton</li>
<li>Saicxs</li>
<li>Gorund O</li>
<li>Tony G</li>
<li>Simonas S.</li>
<li>LordKnoppers</li>
<li>Fabien Duchaussois</li>
<li>Tim Bahnes</li>
<li>Aleksei Gusev</li>
<li>J Hanssen</li>
<li>schoepp</li>
<li>Klaus</li>
<li>Eric</li>
<li>Griffondale</li>
<li>Andy D</li>
<li>YAMAMOTO Yuji</li>
<li>elmiko</li>
<li>David</li>
<li>Nate Wynd</li>
<li>Nicolas</li>
<li>magila</li>
<li>Bryan Fisher</li>
<li>Mark Nicholson</li>
<li>Asperatus</li>
<li>Patrick Buchan-Hepburn</li>
<li>Richárd Faragó</li>
<li>David Koch</li>
<li>cheese_cake</li>
<li>duke_money</li>
<li>lund</li>
<li>Ivana Kellyer</li>
<li>Skullzam</li>
<li>Chris Bier</li>
<li>Gustavo</li>
<li>Henning_IdB</li>
<li>Edd</li>
<li>Net</li>
<li>Sergei Slipchenko</li>
<li>Amanita</li>
<li>Gaara</li>
<li>Max</li>
<li>5h4d3</li>
<li>James Taylor</li>
<li>Alexei Bond</li>
<li>cck</li>
<li>David L</li>
<li>devNull</li>
<li>Erica</li>
<li>Matthew O</li>
<li>Druggo Yang</li>
<li>Eric Stokes</li>
</ul>
<h3>GitHub Sponsors:</h3>
<ul>
<li>rszamszur</li>
<li>Sidicas</li>
<li>Mr-NH</li>
<li>tolias</li>
<li>Adam</li>
</ul>
<h3>Translations:</h3>
<ul>
<li><strong>Arabic:</strong> kmutahar</li>
<li><strong>Chinese (China):</strong> Biggulu, Brandon_c, hoilc, ligyxy, Small_Ku, umi_neko, vc5</li>
<li><strong>Chinese (Taiwan):</strong> BestSteve, flachesis, MiauLightouch, Small_Ku, yan12125, ymhuang0808</li>
<li><strong>Czech:</strong> DanielMilde, pavelb, tpavelek</li>
<li><strong>English (United Kingdom):</strong> YCMHARHZ</li>
<li><strong>English (United States):</strong> alexandercrice, DarkHolme, nguyenlekhtn</li>
<li><strong>Finnish:</strong> artnay, hif1, MawKKe, varjolintu</li>
<li><strong>German:</strong> antsas, BasicBaer, Calyrx, codejunky, DavidHamburg, eth0, for1real, jensrutschmann,
joe776, kflesch, marcbone, MarcEdinger, mcliquid, mfernau77, montilo, nursoda, omnisome4, origin_de, pcrcoding,
rgloor, vlenzer, waster, Wyrrrd</li>
<li><strong>Greek:</strong> magkopian, nplatis, tassos.b, xinomilo</li>
<li><strong>Hungarian:</strong> bubu, meskobalazs, urbalazs</li>
<li><strong>Indonesian:</strong> zk</li>
<li><strong>Italian:</strong> amaxis, duncanmid, FranzMari, lucaim, NITAL, Peo, tosky, VosaxAlo</li>
<li><strong>Japanese:</strong> masoo, p2635, Shinichirou_Yamada, vmemjp, yukinakato</li>
<li><strong>Korean:</strong> cancantun, peremen</li>
<li><strong>Lithuanian:</strong> Moo</li>
<li><strong>Norwegian Bokmål:</strong> eothred, haarek, torgeirf</li>
<li><strong>Polish:</strong> keypress, mrerexx, psobczak</li>
<li><strong>Portuguese (Brazil):</strong> danielbibit, fabiom, flaviobn, newmanisaac, vitor895, weslly</li>
<li><strong>Portuguese (Portugal):</strong> a.santos, American_Jesus, hds, lmagomes, smarquespt</li>
<li><strong>Romanian:</strong> alexminza</li>
<li><strong>Russian:</strong> _nomoretears_, agag11507, alexminza, anm, artemkonenko, denoos, KekcuHa, Mogost,
netforhack, NetWormKido, RKuchma, ShareDVI, talvind, VictorR2007, vsvyatski, wkill95</li>
<li><strong>Serbian:</strong> ArtBIT</li>
<li><strong>Swedish:</strong> Anders_Bergqvist, henziger, jpyllman, peron, Thelin</li>
<li><strong>Turkish:</strong> etc, N3pp</li>
<li><strong>Ukrainian:</strong> brisk022, netforhack, ShareDVI, zoresvit</li>
<li><strong>العربية (Arabic)</strong>: 3eani, 3nad, AboShanab, butterflyoffire_temp, jBailony, kmutahar, m.hemoudi,
Marouane87, microtaha, mohame1d, muha_abdulaziz, Night1, omar.nsy, TheAhmed, zer0x</li>
<li><strong>euskara (Basque)</strong>: azken_tximinoa, Galaipa, Hey_neken, porrumentzio</li>
<li><strong> (Bengali)</strong>: codesmite, Foxom, rediancool, RHJihan</li>
<li><strong> (Burmese)</strong>: Christine.Ivy, hafe14, Snooooowwwwwman, tuntunaung</li>
<li><strong>català (Catalan)</strong>: antoniopolonio, benLabcat, capitantrueno, dsoms, Ecron, jamalinu, jmaribau,
MarcRiera, mcus, raulua, zeehio, ZJaume</li>
<li><strong> (Chinese (Simplified))</strong>: Biacke, Biggulu, Brandon_c, carp0129, Clafiok, deluxghost, Dy64,
ef6, Felix2yu, hoilc, jy06308127, kikyous, kofzhanganguo, ligyxy, lxx4380, oksjd, remonli, ShuiHuo, sjdhome,
slgray, Small_Ku, snhun, umi_neko, vc5, Wylmer_Wang, Z4HD</li>
<li><strong> () (Chinese (Traditional))</strong>: BestSteve, Biacke, flachesis, gojpdchx, ligyxy, MiauLightouch,
plesry, priv, raymondtau, Siriusmart, Small_Ku, ssuhung, Superbil, th3lusive, yan12125, ymhuang0808</li>
<li><strong>hrvatski jezik (Croatian)</strong>: krekrekre, mladenuzelac</li>
<li><strong>čeština (Czech)</strong>: DanielMilde, jiri.jagos, pavelb, pavelz, S474N, stps, tpavelek, vojtechjurcik</li>
<li><strong>dansk (Danish)</strong>: alfabetacain, dovecode, ebbe, ERYpTION, GimliDk, Grooty12, JakobPP, KalleDK,
MannVera, nlkl, Saustrup, thniels</li>
<li><strong>Nederlands (Dutch)</strong>: apie, bartlibert, Bubbel, bython, Dr.Default, e2jk, evanoosten, fourwood,
fvw, glotzbach, JCKalman, keunes, KnooL, ms.vd.linden, ovisicnarf, pietermj, pvdl, rigrig, srgvg, Stephan_P,
stijndubrul, theniels17, ThomasChurchman, timschreinemachers, Vistaus, wanderingidea, Zombaya1</li>
<li><strong>Esperanto (Esperanto)</strong>: batisteo</li>
<li><strong>eesti (Estonian)</strong>: Hermanio, okul, sarnane, tlend, V6lur</li>
<li><strong>suomi (Finnish)</strong>: artnay, hif1, MawKKe, petri, tomisalmi, uusijani, varjolintu</li>
<li><strong>français (French)</strong>: ayiniho, Beatussum, butterflyoffire_temp, Cabirto, francoisa, iannick,
jean_pierre_raumer, John.Mickael, Jojo6375, lacnic, Marouane87, mohame1d, orion78fr, stephanecodes, swarmpan,
t0mmy742, Takeçi, tenzap, webafrancois, x0rld</li>
<li><strong>Galego (Galician)</strong>: damufo, enfeitizador, mustek</li>
<li><strong>Deutsch (German)</strong>: andreas.maier, antsas, archer_321, ASDFGamer, Atalanttore, BasicBaer, blacksn0w,
bwolkchen, Calyrx, clonejo, codejunky, DavidHamburg, eth0, fahstat, FlotterCodername, for1real, frlan, funny0facer,
Gyges, h_h, Hativ, heynemax, hjonas, HoferJulian, hueku, janis91, jensrutschmann, jhit, joe776, kflesch, man_at_home,
marcbone, MarcEdinger, markusd112, Marouane87, maxwxyz, mcliquid, mfernau77, mircsicz, montilo, MuehlburgPhoenix,
muellerma, nautilusx, neon64, Nerzahd, Nightwriter, noodles101, NotAName, nursoda, OLLI_S, omnisome4, origin_de,
pcrcoding, PFischbeck, phallobst, philje, pqtjhhBzDd5NuJ9, r3drock, rakekniven, revoltek, rgloor, Rheggie, RogueThorn,
rugk, ScholliYT, scotwee, Silas_229, spacemanspiff, SteffoSpieler, testarossa47, TheForcer, thillux, transi_222, traschke,
Unkn0wnCat, vlenzer, vpav, waster, wolfram.roesler, Wyrrrd, xf</li>
<li><strong>ελληνικά (Greek)</strong>: anvo, arttor, Dkafetzis, giwrgosmant, GorianM, Jason_M, magkopian, nplatis, saglogog,
tassos.b, xinomilo</li>
<li><strong>עברית (Hebrew)</strong>: avimar, ronyala, shemeshg, shmag18, ThunderB0lt, tryandtry, ztwersky</li>
<li><strong>magyar (Hungarian)</strong>: andras_tim, bubu, entaevau, kempelen, meskobalazs, spammy, typingseashell, urbalazs</li>
<li><strong>Íslenska (Icelandic)</strong>: MannVera</li>
<li><strong>Bahasa Indonesia (Indonesian)</strong>: achmad, algustionesa, bora_ach, racrbmr, zk</li>
<li><strong>Italiano (Italian)</strong>: aleb2000, amaxis, bovirus, duncanmid, FranzMari, Gringoarg, idetao, lucaim, NITAL, Peo,
Pietrog, salvatorecordiano, seatedscribe, Stemby, the.sailor, tosky, VosaxAlo</li>
<li><strong> (Japanese)</strong>: AlCooo, gojpdchx, helloguys, masoo, p2635, Shinichirou_Yamada, shortarrow, ssuhung, tadasu,
take100yen, Umoxfo, vargas.peniel, vmemjp, WatanabeShint, yukinakato</li>
<li><strong>қазақ тілі (Kazakh)</strong>: sotrud_nik</li>
<li><strong> (Korean)</strong>: BraINstinct0, cancantun, peremen</li>
<li><strong>latine (Latin)</strong>: alexandercrice</li>
<li><strong>latviešu valoda (Latvian)</strong>: andis.luksho, victormeirans, wakeeshi</li>
<li><strong>lietuvių kalba (Lithuanian)</strong>: Kornelijus, Moo, pauliusbaulius, rookwood101, wakeeshi</li>
<li><strong>Norsk Bokmål (Norwegian Bokmål)</strong>: bkvamme, eirikl, eothred, haarek, JardarBolin, jumpingmushroom, sattor,
torgeirf, ysteinalver</li>
<li><strong> (Punjabi)</strong>: aalam</li>
<li><strong>فارسی (Farsi)</strong>: gnulover, siamax</li>
<li><strong>فارسی (Farsi (Iran))</strong>: magnifico</li>
<li><strong>język polski (Polish)</strong>: AreYouLoco, dedal123, EsEnZeT, hoek, keypress, konradmb, mrerexx, pabli, ply,
psobczak, SebJez, verahawk</li>
<li><strong>Português (Portuguese)</strong>: diraol, hugok, pfialho, rudahximenes, weslly, xendez</li>
<li><strong>Português (Portuguese (Brazil))</strong>: alinda, amalvarenga, andersoniop, danielbibit, diraol, fabiom, flaviobn,
fmilagres, furious_, gabrieljcs, Guilherme.Peev, guilherme__sr, Havokdan, igorruckert, josephelias94, keeBR, kiskadee, lecalam,
lucasjsoliveira, mauri.andres, newmanisaac, rafaelnp, ruanmed, rudahximenes, ul1sses, vitor895, weslly, wtuemura, xendez,
zodSilence</li>
<li><strong>Português (Portuguese (Portugal))</strong>: a.santos, American_Jesus, arainho, hds, hugok, lecalam, lmagomes, pfialho,
smarquespt, smiguel, xendez, xnenjm</li>
<li><strong>Română (Romanian)</strong>: _parasite_, aduzsardi, alexminza, polearnik</li>
<li><strong>русский (Russian)</strong>: 3nad, _nomoretears_, agag11507, alexandersokol, alexminza, anm, artemkonenko, ashed,
BANOnotIT, burningalchemist, cl0ne, cnide, denoos, DG, DmitriyMaksimov, egorrabota, injseon, Japet, JayDi85, KekcuHa, kerastinell,
laborxcom, leo9uinuo98, Mogost, Mr.GreyWolf, MustangDSG, netforhack, NetWormKido, nibir, Olesya_Gerasimenko, onix, Orianti,
RKuchma, ruslan.denisenko, ShareDVI, Shevchuk, solodyagin, talvind, treylav, upupa, VictorR2007, vsvyatski, wakeeshi, Walter.S,
wkill95, wtigga, zOrg1331</li>
<li><strong>српски језик (Serbian)</strong>: ArtBIT, ozzii</li>
<li><strong>Slovenčina (Slovak)</strong>: Asprotes, crazko, jose1711, l.martinicky, pecer, reisuya, Slavko</li>
<li><strong>Slovenščina (Slovenian)</strong>: asasdasd, samodekleva</li>
<li><strong>Español (Spanish)</strong>: adolfogc, antifaz, capitantrueno, cquike, cyphra, DarkHolme, doubleshuffle, e2jk,
EdwardNavarro, fserrador, gabeweb, gonrial, jjtp, jorpilo, LeoBeltran, mauri.andres, piegope, pquin, puchrojo, rodolfo.guagnini,
tierracomun, vsvyatski</li>
<li><strong>Svenska (Swedish)</strong>: 0x9fff00, aiix, Anders_Bergqvist, ArmanB, Autom, baxtex, eson, henziger, jpyllman, malkus,
merikan, peron, peterkz, Thelin, theschitz, victorhggqvst</li>
<li><strong> (Thai)</strong>: arthit, ben_cm, chumaporn.t, darika, digitalthailandproject, GitJirasamatakij, ll3an, minoplhy,
muhammadmumean, nimid, nipattra, ordinaryjane, rayg, sirawat, Socialister, Wipanee</li>
<li><strong>Türkçe (Turkish)</strong>: abcmen, ahmed.ulusoy, cagries, denizoglu, desc4rtes, etc, ethem578, kayazeren, mcveri, N3pp,
rgucluer, SeLeNLeR, sprlptr48, TeknoMobil, Ven_Zallow, veysiertekin</li>
<li><strong>Українська (Ukrainian)</strong>: brisk022, chulivska, cl0ne, exlevan, m0stik, moudrick, netforhack, olko, onix, paul_sm,
ShareDVI, upupa, zoresvit</li>
</ul>
)";

View File

@@ -348,8 +348,15 @@ void ApplicationSettingsWidget::loadSettings()
m_secUi->hideTotpCheckBox->setChecked(config()->get(Config::Security_HideTotpPreviewPanel).toBool());
m_secUi->hideNotesCheckBox->setChecked(config()->get(Config::Security_HideNotes).toBool());
m_secUi->quickUnlockCheckBox->setEnabled(getQuickUnlock()->isAvailable());
m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
m_secUi->quickUnlockRememberCheckBox->setChecked(config()->get(Config::Security_QuickUnlockRemember).toBool());
#ifdef Q_OS_LINUX
// Remembering quick unlock is not supported on Linux
m_secUi->quickUnlockRememberCheckBox->setVisible(false);
#else
// Only show this option if Touch ID or Windows Hello are available for use
m_secUi->quickUnlockRememberCheckBox->setVisible(getQuickUnlock()->isNativeAvailable());
#endif
for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings();
@@ -469,9 +476,8 @@ void ApplicationSettingsWidget::saveSettings()
config()->set(Config::Security_HideTotpPreviewPanel, m_secUi->hideTotpCheckBox->isChecked());
config()->set(Config::Security_HideNotes, m_secUi->hideNotesCheckBox->isChecked());
if (m_secUi->quickUnlockCheckBox->isEnabled()) {
config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked());
}
config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked());
config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked());
// Security: clear storage if related settings are disabled
if (!config()->get(Config::RememberLastDatabases).toBool()) {

View File

@@ -148,7 +148,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -174,7 +174,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -210,7 +210,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -250,7 +250,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -315,7 +315,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -483,7 +483,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -516,7 +516,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -648,7 +648,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -681,7 +681,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -743,7 +743,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -971,7 +971,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -1019,7 +1019,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -1076,7 +1076,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>364</width>
<height>505</height>
<width>437</width>
<height>529</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -138,7 +138,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -168,7 +168,14 @@
<item>
<widget class="QCheckBox" name="quickUnlockCheckBox">
<property name="text">
<string>Enable database quick unlock (Touch ID / Windows Hello)</string>
<string>Enable database quick unlock by default</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="quickUnlockRememberCheckBox">
<property name="text">
<string>Remember quick unlock after database is closed</string>
</property>
</widget>
</item>

View File

@@ -100,9 +100,8 @@ void Clipboard::clearCopiedText()
return;
}
if (!m_lastCopied.isEmpty()
&& (m_lastCopied == clipboard->text(QClipboard::Clipboard)
|| m_lastCopied == clipboard->text(QClipboard::Selection))) {
if (m_lastCopied == clipboard->text(QClipboard::Clipboard)
|| m_lastCopied == clipboard->text(QClipboard::Selection)) {
clipboard->clear(QClipboard::Clipboard);
clipboard->clear(QClipboard::Selection);
#ifdef Q_OS_UNIX

View File

@@ -84,9 +84,8 @@ void DatabaseOpenDialog::showEvent(QShowEvent* event)
{
QDialog::showEvent(event);
QTimer::singleShot(100, this, [this] {
if (m_view->isOnQuickUnlockScreen() && !m_view->unlockingDatabase()) {
m_view->triggerQuickUnlock();
}
// Automatically trigger quick unlock if it's available
m_view->triggerQuickUnlock();
});
}

View File

@@ -38,14 +38,6 @@
namespace
{
constexpr int clearFormsDelay = 30000;
bool isQuickUnlockAvailable()
{
if (config()->get(Config::Security_QuickUnlock).toBool()) {
return getQuickUnlock()->isAvailable();
}
return false;
}
} // namespace
DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
@@ -68,17 +60,10 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->editPassword->setShowPassword(false);
});
QFont font;
font.setPointSize(font.pointSize() + 4);
font.setBold(true);
m_ui->labelHeadline->setFont(font);
m_ui->quickUnlockButton->setFont(font);
m_ui->quickUnlockButton->setIcon(
icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText)));
m_ui->quickUnlockButton->setIconSize({32, 32});
connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile()));
QFont largeFont;
largeFont.setPointSize(largeFont.pointSize() + 4);
largeFont.setBold(true);
m_ui->labelHeadline->setFont(largeFont);
auto okBtn = m_ui->buttonBox->button(QDialogButtonBox::Ok);
okBtn->setText(tr("Unlock"));
@@ -86,16 +71,19 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase()));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
// Key file components
m_ui->selectKeyFileComponent->setVisible(false);
connect(m_ui->addKeyFileLinkLabel, &QLabel::linkActivated, this, &DatabaseOpenWidget::browseKeyFile);
connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile()));
connect(m_ui->keyFileLineEdit, &PasswordWidget::textChanged, this, [&](const QString& text) {
bool state = !text.isEmpty();
m_ui->addKeyFileLinkLabel->setVisible(!state);
m_ui->selectKeyFileComponent->setVisible(state);
});
connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled);
m_ui->selectKeyFileComponent->setVisible(false);
// Hardware key components
toggleHardwareKeyComponent(false);
connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled);
QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy();
sp.setRetainSizeWhenHidden(true);
@@ -127,13 +115,24 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->refreshHardwareKeys->setVisible(false);
#endif
// QuickUnlock actions
// QuickUnlock components
m_ui->quickUnlockButton->setFont(largeFont);
m_ui->quickUnlockButton->setIcon(
icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText)));
connect(m_ui->quickUnlockButton, &QPushButton::pressed, this, [this] { openDatabase(); });
connect(m_ui->resetQuickUnlockButton, &QPushButton::pressed, this, [this] { resetQuickUnlock(); });
connect(m_ui->closeQuickUnlockButton, &QPushButton::pressed, this, [this] { reject(); });
m_ui->resetQuickUnlockButton->setShortcut(Qt::Key_Escape);
}
DatabaseOpenWidget::~DatabaseOpenWidget() = default;
DatabaseOpenWidget::~DatabaseOpenWidget()
{
// Reset quick unlock if we are not remembering it
if (!config()->get(Config::Security_QuickUnlockRemember).toBool()) {
resetQuickUnlock();
}
}
void DatabaseOpenWidget::toggleHardwareKeyComponent(bool state)
{
@@ -189,7 +188,7 @@ bool DatabaseOpenWidget::event(QEvent* event)
auto type = event->type();
if (type == QEvent::Show || type == QEvent::WindowActivate) {
if (isOnQuickUnlockScreen() && (m_db.isNull() || !canPerformQuickUnlock())) {
if (isOnQuickUnlockScreen() && !canPerformQuickUnlock()) {
resetQuickUnlock();
}
toggleQuickUnlockScreen();
@@ -294,6 +293,7 @@ void DatabaseOpenWidget::load(const QString& filename)
}
toggleQuickUnlockScreen();
m_ui->enableQuickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
#ifdef WITH_XC_YUBIKEY
// Do initial auto-poll
@@ -335,16 +335,12 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
m_ui->editPassword->setText(pw);
m_ui->keyFileLineEdit->setText(keyFile);
m_blockQuickUnlock = true;
m_ui->enableQuickUnlockCheckBox->setChecked(false);
openDatabase();
}
void DatabaseOpenWidget::openDatabase()
{
// Cache this variable for future use then reset
bool blockQuickUnlock = m_blockQuickUnlock || isOnQuickUnlockScreen();
m_blockQuickUnlock = false;
setUserInteractionLock(true);
m_ui->editPassword->setShowPassword(false);
m_ui->messageWidget->hide();
@@ -386,10 +382,13 @@ void DatabaseOpenWidget::openDatabase()
}
}
// Save Quick Unlock credentials if available
if (!blockQuickUnlock && isQuickUnlockAvailable()) {
// Save Quick Unlock credentials if available and enabled
if (!isOnQuickUnlockScreen() && isQuickUnlockAvailable() && m_ui->enableQuickUnlockCheckBox->isChecked()) {
auto keyData = databaseKey->serialize();
getQuickUnlock()->setKey(m_db->publicUuid(), keyData);
auto qu = getQuickUnlock()->interface();
if (!qu->setKey(m_db->publicUuid(), keyData) && !qu->errorString().isEmpty()) {
getMainWindow()->displayTabMessage(qu->errorString(), MessageWidget::MessageType::Warning);
}
m_ui->messageWidget->hideMessage();
}
@@ -434,13 +433,16 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
{
auto databaseKey = QSharedPointer<CompositeKey>::create();
if (!m_db.isNull() && canPerformQuickUnlock()) {
// try to retrieve the stored password using Windows Hello
if (canPerformQuickUnlock()) {
// try to retrieve the stored password using quick unlock
QByteArray keyData;
if (!getQuickUnlock()->getKey(m_db->publicUuid(), keyData)) {
m_ui->messageWidget->showMessage(
tr("Failed to authenticate with Quick Unlock: %1").arg(getQuickUnlock()->errorString()),
MessageWidget::Error);
auto qu = getQuickUnlock()->interface();
if (!qu->getKey(m_db->publicUuid(), keyData)) {
m_ui->messageWidget->showMessage(tr("Failed to authenticate with Quick Unlock: %1").arg(qu->errorString()),
MessageWidget::Error);
if (!qu->hasKey(m_db->publicUuid())) {
resetQuickUnlock();
}
return {};
}
databaseKey->setRawKey(keyData);
@@ -627,9 +629,15 @@ void DatabaseOpenWidget::setUserInteractionLock(bool state)
m_unlockingDatabase = state;
}
bool DatabaseOpenWidget::isQuickUnlockAvailable() const
{
auto qu = getQuickUnlock()->interface();
return qu && qu->isAvailable();
}
bool DatabaseOpenWidget::canPerformQuickUnlock() const
{
return !m_db.isNull() && isQuickUnlockAvailable() && getQuickUnlock()->hasKey(m_db->publicUuid());
return m_db && isQuickUnlockAvailable() && getQuickUnlock()->interface()->hasKey(m_db->publicUuid());
}
bool DatabaseOpenWidget::isOnQuickUnlockScreen() const
@@ -656,7 +664,7 @@ void DatabaseOpenWidget::toggleQuickUnlockScreen()
void DatabaseOpenWidget::triggerQuickUnlock()
{
if (isOnQuickUnlockScreen()) {
if (isOnQuickUnlockScreen() && !unlockingDatabase()) {
m_ui->quickUnlockButton->click();
}
}
@@ -668,11 +676,9 @@ void DatabaseOpenWidget::triggerQuickUnlock()
*/
void DatabaseOpenWidget::resetQuickUnlock()
{
if (!isQuickUnlockAvailable()) {
return;
}
if (!m_db.isNull()) {
getQuickUnlock()->reset(m_db->publicUuid());
auto qu = getQuickUnlock()->interface();
if (m_db && qu) {
qu->reset(m_db->publicUuid());
}
load(m_filename);
}

View File

@@ -19,7 +19,6 @@
#ifndef KEEPASSX_DATABASEOPENWIDGET_H
#define KEEPASSX_DATABASEOPENWIDGET_H
#include <QPointer>
#include <QScopedPointer>
#include <QTimer>
@@ -46,21 +45,17 @@ class DatabaseOpenWidget : public DialogyWidget
public:
explicit DatabaseOpenWidget(QWidget* parent = nullptr);
~DatabaseOpenWidget() override;
void load(const QString& filename);
QString filename();
QSharedPointer<Database> database();
void clearForms();
void enterKey(const QString& pw, const QString& keyFile);
QSharedPointer<Database> database();
void triggerQuickUnlock();
bool unlockingDatabase();
void showMessage(const QString& text, MessageWidget::MessageType type, int autoHideTimeout);
// Quick Unlock helper functions
bool canPerformQuickUnlock() const;
bool isOnQuickUnlockScreen() const;
void toggleQuickUnlockScreen();
void triggerQuickUnlock();
void resetQuickUnlock();
signals:
void dialogFinished(bool accepted);
@@ -85,14 +80,20 @@ private slots:
void closeDatabase();
void pollHardwareKey(bool manualTrigger = false, int delay = 0);
void hardwareKeyResponse(bool found);
void resetQuickUnlock();
private:
// Quick Unlock helper functions
bool isQuickUnlockAvailable() const;
bool canPerformQuickUnlock() const;
bool isOnQuickUnlockScreen() const;
void toggleQuickUnlockScreen();
#ifdef WITH_XC_YUBIKEY
QPointer<DeviceListener> m_deviceListener;
#endif
bool m_pollingHardwareKey = false;
bool m_manualHardwareKeyRefresh = false;
bool m_blockQuickUnlock = false;
bool m_unlockingDatabase = false;
bool m_triedToQuit = false;
QTimer m_hideTimer;

View File

@@ -180,7 +180,7 @@
<number>0</number>
</property>
<property name="bottomMargin">
<number>10</number>
<number>0</number>
</property>
<item>
<widget class="QLabel" name="passwordLabel">
@@ -192,7 +192,7 @@
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>2</number>
<number>0</number>
</property>
<item>
<widget class="PasswordWidget" name="editPassword" native="true">
@@ -250,13 +250,13 @@
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
<number>10</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>10</number>
<number>15</number>
</property>
<item>
<widget class="QLabel" name="selectKeyFileLabel">
@@ -399,7 +399,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -465,6 +465,48 @@
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="enableQuickUnlockCheckBox">
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Quick Unlock</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>8</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item alignment="Qt::AlignRight">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="focusPolicy">
@@ -511,6 +553,9 @@
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="spacing">
<number>0</number>
</property>
<item>
<spacer name="verticalSpacer_7">
<property name="orientation">
@@ -542,17 +587,81 @@
<property name="text">
<string>Unlock Database</string>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="resetQuickUnlockButton">
<property name="text">
<string>Cancel</string>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>4</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="resetQuickUnlockButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_8">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>4</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="closeQuickUnlockButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
<property name="text">
<string>Close Database</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_8">
@@ -645,8 +754,6 @@
</customwidget>
</customwidgets>
<tabstops>
<tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
<tabstop>editPassword</tabstop>
<tabstop>keyFileLineEdit</tabstop>
<tabstop>buttonBrowseFile</tabstop>
@@ -654,7 +761,11 @@
<tabstop>hardwareKeyCombo</tabstop>
<tabstop>refreshHardwareKeys</tabstop>
<tabstop>addKeyFileLinkLabel</tabstop>
<tabstop>enableQuickUnlockCheckBox</tabstop>
<tabstop>buttonBox</tabstop>
<tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
<tabstop>closeQuickUnlockButton</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@@ -674,7 +674,7 @@ void DatabaseTabWidget::updateTabName(int index)
return;
}
index = indexOf(dbWidget);
setTabText(index, Tools::escapeAccelerators(tabName(index)));
setTabText(index, tabName(index));
setTabToolTip(index, dbWidget->displayFilePath());
auto iconIndex = dbWidget->database()->publicIcon();
if (iconIndex >= 0 && iconIndex < databaseIcons()->count()) {

View File

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

View File

@@ -92,14 +92,12 @@ QIcon Icons::trayIcon(bool unlocked)
}
QIcon i;
#if defined(Q_OS_WIN)
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
if (osUtils->isStatusBarDark()) {
i = icon(QString("keepassxc-monochrome-light%1").arg(suffix), false);
} else {
i = icon(QString("keepassxc-monochrome-dark%1").arg(suffix), false);
}
#elif defined(Q_OS_MACOS)
i = icon(QString("keepassxc-monochrome-light%1").arg(suffix), false);
#else
i = icon(QString("%1-%2%3").arg(applicationIconName(), iconAppearance, suffix), false);
#endif

View File

@@ -38,7 +38,6 @@
#include "autotype/AutoType.h"
#include "core/InactivityTimer.h"
#include "core/Resources.h"
#include "core/Tools.h"
#include "gui/AboutDialog.h"
#include "gui/ActionCollection.h"
#include "gui/Icons.h"
@@ -790,7 +789,7 @@ void MainWindow::updateLastDatabasesMenu()
const QStringList lastDatabases = config()->get(Config::LastDatabases).toStringList();
for (const QString& database : lastDatabases) {
QAction* action = m_ui->menuRecentDatabases->addAction(Tools::escapeAccelerators(database));
QAction* action = m_ui->menuRecentDatabases->addAction(database);
action->setData(database);
m_lastDatabasesActions->addAction(action);
}

View File

@@ -132,9 +132,6 @@ void KeyFileEditWidget::browseKeyFile()
QString filters = QString("%1 (*.keyx *.key);;%2 (*)").arg(tr("Key files"), tr("All files"));
QString fileName = fileDialog()->getOpenFileName(this, tr("Select a key file"), QString(), filters);
if (fileName.isEmpty()) { // user clicked on cancel
return;
}
if (QFileInfo(fileName).canonicalFilePath() == m_parent->getDatabase()->canonicalFilePath()) {
MessageBox::critical(getMainWindow(),
tr("Invalid Key File"),

View File

@@ -229,7 +229,7 @@ bool DatabaseSettingsWidgetDatabaseKey::saveSettings()
m_db->setKey(newKey, true, false, false);
getQuickUnlock()->reset(m_db->publicUuid());
getQuickUnlock()->interface()->reset(m_db->publicUuid());
emit editFinished(true);
if (m_isDirty) {

View File

@@ -72,6 +72,14 @@ public:
virtual bool canPreventScreenCapture() const = 0;
virtual bool setPreventScreenCapture(QWindow* window, bool allow) const;
/**
* Platform specific secrets storage/handling
*/
virtual bool saveSecret(const QString& key, const QByteArray& secretData) const = 0;
virtual bool getSecret(const QString& key, QByteArray& secretData) const = 0;
virtual bool removeSecret(const QString& key) const = 0;
virtual bool removeAllSecrets() const = 0;
signals:
void globalShortcutTriggered(const QString& name, const QString& search = {});

View File

@@ -17,14 +17,22 @@
*/
#import "AppKitImpl.h"
#import "MacUtils.h"
#import <QWindow>
#import <QMenu>
#import <QMenuBar>
#import <Cocoa/Cocoa.h>
#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <LocalAuthentication/LocalAuthentication.h>
#import <Security/Security.h>
#if __clang_major__ >= 13 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_12_3
#import <ScreenCaptureKit/ScreenCaptureKit.h>
#endif
#include "config-keepassx.h"
@implementation AppKitImpl
- (id) initWithObject:(AppKit*)appkit
@@ -340,3 +348,222 @@ void AppKit::configureWindowAndHelpMenus(QMainWindow* window, QMenu* helpMenu)
{
[static_cast<id>(self) configureWindowAndHelpMenus:window helpMenu:helpMenu];
}
// Common prefix for saved secrets
static const auto s_touchIdKeyPrefix = QStringLiteral("KeepassXC_Keys_");
// Convert macOS error codes to strings
inline std::string StatusToErrorMessage(OSStatus status)
{
CFStringRef text = SecCopyErrorMessageString(status, NULL);
if (!text) {
return std::to_string(status);
}
auto msg = CFStringGetCStringPtr(text, kCFStringEncodingUTF8);
std::string result;
if (msg) {
result = msg;
}
CFRelease(text);
return result;
}
// Report status errors if not successful
inline void LogStatusError(const char *message, OSStatus status)
{
if (status) {
std::string msg = StatusToErrorMessage(status);
qWarning("%s: %s", message, msg.c_str());
}
}
// Create an access control object to govern use of the saved secret
SecAccessControlRef createAccessControl(bool useTouchId)
{
// We need both runtime and compile time checks here to solve the following problems:
// - Not all flags are available in all OS versions, so we have to check it at compile time
// - Requesting Biometry/TouchID/DevicePassword when no fingerprint sensor is available will result in runtime error
SecAccessControlCreateFlags accessControlFlags = 0;
// When TouchID is not enrolled and the flag is set, the method call fails with an error.
// We still want to set this flag if TouchID is enrolled but temporarily unavailable due to closed lid
//
// Sometimes, the enrolled-check does not work, LAErrorBiometryNotAvailable is returned instead of LAErrorBiometryNotEnrolled.
// To fallback gracefully, we have to try to save the key a second time without this flag.
if (useTouchId) {
#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
// This is the non-deprecated and preferred flag
accessControlFlags = kSecAccessControlBiometryCurrentSet;
#elif XC_COMPILER_SUPPORT(TOUCH_ID)
accessControlFlags = kSecAccessControlTouchIDCurrentSet;
#endif
}
// Add support for watch authentication if available
#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch;
#endif
// Check if password fallback is possible and add that as an option
#if XC_COMPILER_SUPPORT(TOUCH_ID)
if (macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::PasswordFallback)) {
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlDevicePasscode;
}
#endif
CFErrorRef error = nullptr;
auto sacObject = SecAccessControlCreateWithFlags(
kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
if (!sacObject || error) {
auto e = static_cast<NSError*>(error);
qWarning("MacUtils::saveSecret - Error creating security flags: %s", e.localizedDescription.UTF8String);
return nullptr;
}
return sacObject;
}
bool MacUtils::saveSecret(const QString& key, const QByteArray& secretData) const
{
const auto keyName = s_touchIdKeyPrefix + key;
// Delete any existing entry since macOS does not allow overwrite
if (!removeSecret(key)) {
qWarning("MacUtils::saveSecret - Failed to remove existing secret for key '%s'", qPrintable(key));
}
// Add new entry
auto keyBase64 = secretData.toBase64();
auto keyValueData = CFDataCreateWithBytesNoCopy(
kCFAllocatorDefault, reinterpret_cast<const UInt8*>(keyBase64.data()),
keyBase64.length(), kCFAllocatorDefault);
auto attributes = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(attributes, kSecAttrAccount, static_cast<CFStringRef>(keyName.toNSString()));
CFDictionarySetValue(attributes, kSecValueData, keyValueData);
CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
// First, attempt with TouchID enabled
CFDictionarySetValue(attributes, kSecAttrAccessControl, createAccessControl(true));
auto status = SecItemAdd(attributes, nullptr);
if (status != errSecSuccess) {
qDebug("MacUtils::saveSecret - Failed to save secret with TouchID enabled");
// Try again without TouchID enabled
CFDictionarySetValue(attributes, kSecAttrAccessControl, createAccessControl(false));
status = SecItemAdd(attributes, nullptr);
if (status != errSecSuccess) {
qWarning("MacUtils::saveSecret - Failed to save secret to keystore");
}
}
CFRelease(keyValueData);
CFRelease(attributes);
return status == errSecSuccess;
}
bool MacUtils::getSecret(const QString& key, QByteArray& secretData) const
{
const auto keyName = s_touchIdKeyPrefix + key;
secretData.clear();
auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(query, kSecAttrAccount, static_cast<CFStringRef>(keyName.toNSString()));
CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
CFTypeRef dataTypeRef = nullptr;
auto status = SecItemCopyMatching(query, &dataTypeRef);
CFRelease(query);
if (status == errSecUserCanceled) {
// user canceled the authentication, return true with empty key
return true;
} else if (status != errSecSuccess || !dataTypeRef) {
// TODO: Log failure
return false;
}
auto valueData = static_cast<CFDataRef>(dataTypeRef);
secretData = QByteArray::fromBase64(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)),
CFDataGetLength(valueData)));
CFRelease(dataTypeRef);
return !secretData.isEmpty();
}
bool MacUtils::removeSecret(const QString& key) const
{
const auto keyName = s_touchIdKeyPrefix + key;
auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(query, kSecAttrAccount, static_cast<CFStringRef>(keyName.toNSString()));
CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
// TODO: Log failure to delete?
SecItemDelete(query);
CFRelease(query);
return true;
}
bool MacUtils::removeAllSecrets() const
{
auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(query, kSecReturnAttributes, kCFBooleanTrue);
CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitAll);
CFTypeRef result = nullptr;
auto status = SecItemCopyMatching(query, &result);
if (status == errSecSuccess && result) {
for (NSDictionary* item in static_cast<NSArray*>(result)) {
NSString* account = item[static_cast<id>(kSecAttrAccount)];
if (account && [account hasPrefix:s_touchIdKeyPrefix.toNSString()]) {
auto delQuery = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(delQuery, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(delQuery, kSecAttrAccount, static_cast<CFStringRef>(account));
// TODO: Log failure to delete?
SecItemDelete(delQuery);
CFRelease(delQuery);
}
}
CFRelease(result);
}
CFRelease(query);
return true;
}
bool MacUtils::isAuthPolicyAvailable(AuthPolicy policy) const
{
LAPolicy policyCode;
switch (policy) {
case AuthPolicy::TouchId:
policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
break;
case AuthPolicy::Watch:
policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch;
break;
case AuthPolicy::PasswordFallback:
policyCode = LAPolicyDeviceOwnerAuthentication;
break;
default:
return false;
}
@try {
LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
bool available = [context canEvaluatePolicy:policyCode error:&error];
[context release];
if (error) {
qDebug("MacUtils::isPolicyAvailable - Policy not available: %s", error.localizedDescription.UTF8String);
}
return available;
} @catch (NSException *exception) {
qWarning("MacUtils::isPolicyAvailable - Exception occurred: %s", exception.reason.UTF8String);
return false;
}
}

View File

@@ -68,6 +68,21 @@ public:
bool canPreventScreenCapture() const override;
bool setPreventScreenCapture(QWindow* window, bool prevent) const override;
// Key management API (TouchID)
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
bool getSecret(const QString& key, QByteArray& secretData) const override;
bool removeSecret(const QString& key) const override;
bool removeAllSecrets() const override;
enum class AuthPolicy
{
TouchId,
Watch,
PasswordFallback
};
bool isAuthPolicyAvailable(AuthPolicy policy) const;
signals:
void userSwitched();

View File

@@ -30,6 +30,11 @@
#include <QStandardPaths>
#include <QStyle>
#include <QTextStream>
extern "C" {
#include <keyutils.h>
}
#ifdef WITH_XC_X11
#include <QX11Info>
@@ -411,3 +416,74 @@ quint64 NixUtils::getProcessStartTime() const
qDebug() << "nixutils: failed to find ')' in " << processStatPath;
return 0;
}
namespace
{
key_serial_t getKeyring()
{
auto keyring = keyctl_get_persistent(-1, KEY_SPEC_PROCESS_KEYRING);
if (keyring == -1) {
// Return the non-persistent keyring as a fallback
qWarning("nixutils: failed to get persistent keyring: %s", strerror(errno));
keyring = KEY_SPEC_PROCESS_KEYRING;
}
return keyring;
}
} // namespace
bool NixUtils::saveSecret(const QString& key, const QByteArray& secretData) const
{
auto keyserial =
add_key("user", key.toStdString().c_str(), secretData.constData(), secretData.size(), getKeyring());
if (keyserial < 0) {
qWarning("nixutils: failed to save secret: %s", strerror(errno));
return false;
}
// Only allow this process to read/write this key
keyctl_setperm(keyserial, KEY_POS_ALL);
return true;
}
bool NixUtils::getSecret(const QString& key, QByteArray& secretData) const
{
secretData.clear();
auto keyserial = request_key("user", key.toStdString().c_str(), nullptr, getKeyring());
if (keyserial < 0) {
qWarning("nixutils: failed to find secret: %s", strerror(errno));
return false;
}
secretData.resize(512);
auto size = keyctl_read(keyserial, secretData.data(), secretData.size());
if (size == -1) {
qWarning("nixutils: failed to read secret: %s", strerror(errno));
return false;
}
secretData.resize(size);
return true;
}
bool NixUtils::removeSecret(const QString& key) const
{
auto keyserial = request_key("user", key.toStdString().c_str(), nullptr, getKeyring());
if (keyserial < 0) {
qWarning("nixutils: failed to find secret: %s", strerror(errno));
return false;
}
if (keyctl_unlink(keyserial, getKeyring()) < 0) {
qWarning("nixutils: failed to remove secret: %s", strerror(errno));
return false;
}
return true;
}
bool NixUtils::removeAllSecrets() const
{
// NixUtils does not support clearing all keys
return false;
}

View File

@@ -52,6 +52,11 @@ public:
quint64 getProcessStartTime() const;
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
bool getSecret(const QString& key, QByteArray& secretData) const override;
bool removeSecret(const QString& key) const override;
bool removeAllSecrets() const override;
private slots:
void handleColorSchemeRead(QDBusVariant value);
void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value);

View File

@@ -56,6 +56,7 @@ void DeviceListenerWin::registerHotplugCallback(bool arrived,
regex += QString("PID_%1&").arg(productId, 0, 16).toUpper();
}
}
regex += QString(".*$"); // Qt won't match otherwise
m_deviceIdMatch = QRegularExpression(regex);
DEV_BROADCAST_DEVICEINTERFACE_W notificationFilter{
@@ -94,7 +95,7 @@ bool DeviceListenerWin::nativeEventFilter(const QByteArray& eventType, void* mes
|| (m_handleRemoval && m->wParam == DBT_DEVICEREMOVECOMPLETE)) {
const auto pBrHdr = reinterpret_cast<PDEV_BROADCAST_HDR>(m->lParam);
const auto pDevIface = reinterpret_cast<PDEV_BROADCAST_DEVICEINTERFACE_W>(pBrHdr);
const auto name = QString::fromWCharArray(pDevIface->dbcc_name);
const auto name = QString::fromWCharArray(pDevIface->dbcc_name, pDevIface->dbcc_size);
if (m_deviceIdMatch.match(name).hasMatch()) {
emit devicePlugged(m->wParam == DBT_DEVICEARRIVAL, nullptr, pDevIface);
return true;

View File

@@ -20,11 +20,24 @@
#include <QApplication>
#include <QDir>
#include <QSettings>
#include <QUuid>
#include <QWindow>
#include <Windows.h>
#include <winrt/base.h>
#include <winrt/windows.foundation.collections.h>
#include <winrt/windows.security.credentials.h>
#undef MessageBox
using namespace winrt;
using namespace Windows::Foundation::Collections;
using namespace Windows::Security::Credentials;
namespace
{
const std::wstring s_winKeyStoreName{L"keepassxc"};
}
QPointer<WinUtils> WinUtils::m_instance = nullptr;
WinUtils* WinUtils::instance()
@@ -361,3 +374,59 @@ DWORD WinUtils::qtToNativeModifiers(Qt::KeyboardModifiers modifiers)
return nativeModifiers;
}
bool WinUtils::saveSecret(const QString& key, const QByteArray& secretData) const
{
try {
auto vault = PasswordVault();
vault.Add({s_winKeyStoreName,
winrt::hstring(key.toStdWString()),
winrt::to_hstring(secretData.toBase64().toStdString())});
return true;
} catch (winrt::hresult_error const&) {
qWarning("WinUtils - Failed to add key to password vault");
return false;
}
}
bool WinUtils::getSecret(const QString& key, QByteArray& secretData) const
{
secretData.clear();
try {
auto vault = PasswordVault();
auto credential = vault.Retrieve(s_winKeyStoreName, winrt::hstring(key.toStdWString()));
secretData = QByteArray::fromBase64(QByteArray::fromStdString(winrt::to_string(credential.Password())));
} catch (winrt::hresult_error const&) {
qWarning("WinUtils - Failed to retrieve key from password vault");
return false;
}
return !secretData.isEmpty();
}
bool WinUtils::removeSecret(const QString& key) const
{
try {
auto vault = PasswordVault();
vault.Remove({s_winKeyStoreName, winrt::hstring(key.toStdWString()), L"nodata"});
return true;
} catch (winrt::hresult_error const&) {
qWarning("WinUtils - Failed to clear key from password vault");
return false;
}
}
bool WinUtils::removeAllSecrets() const
{
auto vault = PasswordVault();
auto credentials = vault.FindAllByResource(s_winKeyStoreName);
bool allSuccess = true;
for (const auto& credential : credentials) {
try {
vault.Remove(credential);
} catch (winrt::hresult_error const&) {
qWarning("WinUtils - Failed to clear key from password vault");
allSuccess = false;
}
}
return allSuccess;
}

View File

@@ -61,6 +61,11 @@ public:
bool canPreventScreenCapture() const override;
bool setPreventScreenCapture(QWindow* window, bool prevent) const override;
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
bool getSecret(const QString& key, QByteArray& secretData) const override;
bool removeSecret(const QString& key) const override;
bool removeAllSecrets() const override;
protected:
explicit WinUtils(QObject* parent = nullptr);
~WinUtils() override = default;

View File

@@ -213,7 +213,7 @@ namespace KeeShareSettings
}
}
} else {
qDebug("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
qWarning("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
reader.skipCurrentElement();
}
}
@@ -253,7 +253,7 @@ namespace KeeShareSettings
} else if (reader.name() == "PublicKey") {
own.certificate = Certificate::deserialize(reader);
} else {
qDebug("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
qWarning("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
reader.skipCurrentElement();
}
}
@@ -262,7 +262,8 @@ namespace KeeShareSettings
}
Reference::Reference()
: uuid(QUuid::createUuid())
: type(Inactive)
, uuid(QUuid::createUuid())
{
}
@@ -319,21 +320,12 @@ namespace KeeShareSettings
writer.writeStartElement("Password");
writer.writeCharacters(reference.password.toUtf8().toBase64());
writer.writeEndElement();
writer.writeStartElement("KeepGroups");
writer.writeCharacters(reference.keepGroups ? "True" : "False");
writer.writeEndElement();
});
}
Reference Reference::deserialize(const QString& raw)
{
if (raw.isEmpty()) {
return {};
}
Reference reference;
// If KeepGroups is not present, default to false for backward compatibility
reference.keepGroups = false;
xmlDeserialize(raw, [&](QXmlStreamReader& reader) {
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "Type") {
@@ -354,10 +346,8 @@ namespace KeeShareSettings
reference.path = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1()));
} else if (reader.name() == "Password") {
reference.password = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1()));
} else if (reader.name() == "KeepGroups") {
reference.keepGroups = reader.readElementText().compare("True") == 0;
} else {
qDebug("Unknown Reference element %s", qPrintable(reader.name().toString()));
qWarning("Unknown Reference element %s", qPrintable(reader.name().toString()));
reader.skipCurrentElement();
}
}
@@ -373,11 +363,7 @@ namespace KeeShareSettings
// Extract RSA key data to serialize an ssh-rsa public key.
// ssh-rsa keys are currently not built into Botan
// need a dynamic_cast here, because the base class is virtual
const auto rsaKey = dynamic_cast<Botan::RSA_PrivateKey*>(sign.certificate.key.data());
if (!rsaKey) {
return {};
}
const auto rsaKey = static_cast<Botan::RSA_PrivateKey*>(sign.certificate.key.data());
std::vector<uint8_t> rsaE(rsaKey->get_e().bytes());
rsaKey->get_e().binary_encode(rsaE.data());

View File

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

View File

@@ -62,39 +62,6 @@ namespace
}
}
void cloneIcon(Metadata* targetMetadata, const Database* sourceDb, const QUuid& iconUuid)
{
if (!iconUuid.isNull() && !targetMetadata->hasCustomIcon(iconUuid)) {
targetMetadata->addCustomIcon(iconUuid, sourceDb->metadata()->customIcon(iconUuid));
}
}
void cloneEntries(Metadata* targetMetadata, const Group* sourceGroup, Group* targetGroup)
{
for (const Entry* sourceEntry : sourceGroup->entries()) {
auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
const bool updateTimeinfoEntry = targetEntry->canUpdateTimeinfo();
targetEntry->setUpdateTimeinfo(false);
targetEntry->setGroup(targetGroup);
targetEntry->setUpdateTimeinfo(updateTimeinfoEntry);
cloneIcon(targetMetadata, sourceEntry->database(), targetEntry->iconUuid());
}
}
void cloneChildren(Metadata* targetMetadata, const Group* sourceRoot, Group* targetRoot)
{
for (const Group* sourceGroup : sourceRoot->children()) {
auto* targetGroup = sourceGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
const bool updateTimeinfo = targetGroup->canUpdateTimeinfo();
targetGroup->setUpdateTimeinfo(false);
targetGroup->setParent(targetRoot);
targetGroup->setUpdateTimeinfo(updateTimeinfo);
cloneIcon(targetMetadata, sourceRoot->database(), targetGroup->iconUuid());
cloneEntries(targetMetadata, sourceGroup, targetGroup);
cloneChildren(targetMetadata, sourceGroup, targetGroup);
}
}
Database* extractIntoDatabase(const KeeShareSettings::Reference& reference, const Group* sourceRoot)
{
const auto* sourceDb = sourceRoot->database();
@@ -108,10 +75,17 @@ namespace
targetRoot->setUpdateTimeinfo(false);
KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference());
targetRoot->setUpdateTimeinfo(updateTimeinfo);
cloneIcon(targetMetadata, sourceRoot->database(), targetRoot->iconUuid());
cloneEntries(targetMetadata, sourceRoot, targetRoot);
if (reference.keepGroups) {
cloneChildren(targetMetadata, sourceRoot, targetRoot);
const auto sourceEntries = sourceRoot->entriesRecursive(false);
for (const Entry* sourceEntry : sourceEntries) {
auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
const bool updateTimeinfoEntry = targetEntry->canUpdateTimeinfo();
targetEntry->setUpdateTimeinfo(false);
targetEntry->setGroup(targetRoot);
targetEntry->setUpdateTimeinfo(updateTimeinfoEntry);
const auto iconUuid = targetEntry->iconUuid();
if (!iconUuid.isNull() && !targetMetadata->hasCustomIcon(iconUuid)) {
targetMetadata->addCustomIcon(iconUuid, sourceEntry->database()->metadata()->customIcon(iconUuid));
}
}
auto key = QSharedPointer<CompositeKey>::create();

View File

@@ -43,7 +43,6 @@ EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent)
connect(m_ui->pathEdit, SIGNAL(editingFinished()), SLOT(selectPath()));
connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog()));
connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType()));
connect(m_ui->keepGroupsCheckbox, SIGNAL(toggled(bool)), SLOT(keepGroupsToggled(bool)));
connect(m_ui->clearButton, SIGNAL(clicked(bool)), SLOT(clearInputs()));
connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(updateSharingState()));
@@ -98,7 +97,6 @@ void EditGroupWidgetKeeShare::updateSharingState()
m_ui->pathEdit->setEnabled(isEnabled);
m_ui->pathSelectionButton->setEnabled(isEnabled);
m_ui->passwordEdit->setEnabled(isEnabled);
m_ui->keepGroupsCheckbox->setEnabled(isEnabled);
if (!m_temporaryGroup || !isEnabled) {
m_ui->messageWidget->hideMessage();
@@ -190,7 +188,6 @@ void EditGroupWidgetKeeShare::update()
m_ui->typeComboBox->setCurrentIndex(reference.type);
m_ui->passwordEdit->setText(reference.password);
m_ui->pathEdit->setText(reference.path);
m_ui->keepGroupsCheckbox->setChecked(reference.keepGroups);
}
updateSharingState();
@@ -294,13 +291,3 @@ void EditGroupWidgetKeeShare::selectType()
updateSharingState();
}
void EditGroupWidgetKeeShare::keepGroupsToggled(bool toggled)
{
if (!m_temporaryGroup) {
return;
}
auto reference = KeeShare::referenceOf(m_temporaryGroup);
reference.keepGroups = toggled;
KeeShare::setReferenceTo(m_temporaryGroup, reference);
}

View File

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

View File

@@ -138,7 +138,7 @@
</item>
</layout>
</item>
<item row="4" column="1">
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
@@ -171,7 +171,7 @@
</item>
</layout>
</item>
<item row="5" column="0">
<item row="4" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -184,19 +184,6 @@
</property>
</spacer>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="keepGroupsCheckbox">
<property name="toolTip">
<string>Maintain group structure with shared database</string>
</property>
<property name="text">
<string>Keep Group Structure</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View File

@@ -73,29 +73,31 @@ bool YubiKey::isInitialized()
bool YubiKey::findValidKeys()
{
// Block operations on hardware keys while scanning
QMutexLocker lock(&s_interfaceMutex);
m_connectedKeys = 0;
m_findingKeys = true;
m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys(m_connectedKeys);
m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys(m_connectedKeys);
m_findingKeys = false;
findValidKeys(lock);
return !m_usbKeys.isEmpty() || !m_pcscKeys.isEmpty();
}
void YubiKey::findValidKeys(const QMutexLocker& locker)
{
// Check QMutexLocker since version 6.4
Q_UNUSED(locker);
m_connectedKeys = 0;
m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys(m_connectedKeys);
m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys(m_connectedKeys);
}
void YubiKey::findValidKeysAsync()
{
// Don't start another scan if we are already doing one
if (!m_findingKeys) {
m_findingKeys = true;
QtConcurrent::run([this] { emit detectComplete(findValidKeys()); });
}
QtConcurrent::run([this] { emit detectComplete(findValidKeys()); });
}
YubiKey::KeyMap YubiKey::foundKeys()
{
QMutexLocker lock(&s_interfaceMutex);
KeyMap foundKeys = m_usbKeys;
foundKeys.unite(m_pcscKeys);
@@ -104,12 +106,38 @@ YubiKey::KeyMap YubiKey::foundKeys()
int YubiKey::connectedKeys()
{
QMutexLocker lock(&s_interfaceMutex);
return m_connectedKeys;
}
QString YubiKey::errorMessage()
{
return m_error;
QMutexLocker lock(&s_interfaceMutex);
QString error;
error.clear();
if (!m_error.isNull()) {
error += tr("General: ") + m_error;
}
QString usb_error = YubiKeyInterfaceUSB::instance()->errorMessage();
if (!usb_error.isNull()) {
if (!error.isNull()) {
error += " | ";
}
error += "USB: " + usb_error;
}
QString pcsc_error = YubiKeyInterfacePCSC::instance()->errorMessage();
if (!pcsc_error.isNull()) {
if (!error.isNull()) {
error += " | ";
}
error += "PCSC: " + pcsc_error;
}
return error;
}
/**
@@ -147,31 +175,25 @@ bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
YubiKey::ChallengeResult
YubiKey::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
{
m_error.clear();
// Prevent re-entrant access to hardware keys
QMutexLocker lock(&s_interfaceMutex);
// Try finding key on the USB interface first
auto ret = YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
if (ret == ChallengeResult::YCR_ERROR) {
m_error = YubiKeyInterfaceUSB::instance()->errorMessage();
return ret;
m_error.clear();
// Make sure we tried to find available keys
if (m_usbKeys.isEmpty() && m_pcscKeys.isEmpty()) {
findValidKeys(lock);
}
// If a USB key was not found, try PC/SC interface
if (ret == ChallengeResult::YCR_KEYNOTFOUND) {
ret = YubiKeyInterfacePCSC::instance()->challenge(slot, challenge, response);
if (ret == ChallengeResult::YCR_ERROR) {
m_error = YubiKeyInterfacePCSC::instance()->errorMessage();
return ret;
}
if (m_usbKeys.contains(slot)) {
return YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
}
if (ret == ChallengeResult::YCR_KEYNOTFOUND) {
m_error =
tr("Could not find hardware key with serial number %1. Please connect it to continue.").arg(slot.first);
if (m_pcscKeys.contains(slot)) {
return YubiKeyInterfacePCSC::instance()->challenge(slot, challenge, response);
}
return ret;
m_error = tr("Could not find interface for hardware key with serial number %1. Please connect it to continue.")
.arg(slot.first);
return ChallengeResult::YCR_ERROR;
}

View File

@@ -44,8 +44,7 @@ public:
{
YCR_ERROR = 0,
YCR_SUCCESS = 1,
YCR_WOULDBLOCK = 2,
YCR_KEYNOTFOUND = 3,
YCR_WOULDBLOCK = 2
};
static YubiKey* instance();
@@ -85,14 +84,14 @@ signals:
private:
explicit YubiKey();
void findValidKeys(const QMutexLocker& locker);
static YubiKey* m_instance;
QTimer m_interactionTimer;
bool m_initialized = false;
bool m_findingKeys = false;
QString m_error;
// Prevents multiple simultaneous operations on hardware keys
static QMutex s_interfaceMutex;
KeyMap m_usbKeys;

View File

@@ -679,7 +679,7 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
m_error.clear();
if (!m_initialized) {
m_error = tr("The YubiKey PC/SC interface has not been initialized.");
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
return YubiKey::ChallengeResult::YCR_ERROR;
}
// Try for a few seconds to find the key
@@ -710,8 +710,11 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
}
}
m_error = tr("Could not find or access hardware key with serial number %1. Please present it to continue. ")
.arg(slot.first)
+ m_error;
emit challengeCompleted();
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
return YubiKey::ChallengeResult::YCR_ERROR;
}
YubiKey::ChallengeResult YubiKeyInterfacePCSC::performChallenge(void* key,

View File

@@ -237,7 +237,7 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
m_error.clear();
if (!m_initialized) {
m_error = tr("The YubiKey USB interface has not been initialized.");
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
return YubiKey::ChallengeResult::YCR_ERROR;
}
auto yk_key = openKeySerial(slot.first);
@@ -245,11 +245,12 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
// Key with specified serial number is not connected
m_error =
tr("Could not find hardware key with serial number %1. Please plug it in to continue.").arg(slot.first);
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
return YubiKey::ChallengeResult::YCR_ERROR;
}
emit challengeStarted();
auto ret = performChallenge(yk_key.get(), slot.second, true, challenge, response);
emit challengeCompleted();
return ret;

View File

@@ -0,0 +1,207 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PinUnlock.h"
#include "crypto/CryptoHash.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
#include "crypto/kdf/Argon2Kdf.h"
#include "gui/osutils/OSUtils.h"
#include <QInputDialog>
#include <QRegularExpression>
const int PinUnlock::MIN_PIN_LENGTH = 6;
const int PinUnlock::MAX_PIN_LENGTH = 10;
const int PinUnlock::MAX_PIN_ATTEMPTS = 3;
bool PinUnlock::isAvailable() const
{
return true;
}
bool PinUnlock::promptPin(int attempt, QByteArray& sessionKey)
{
QString pin;
if (attempt == 0) {
// Loop until a valid pin has been entered or canceled
QRegularExpression pinRegex("^\\d+$");
while (true) {
bool ok = false;
pin = QInputDialog::getText(
nullptr,
QObject::tr("Quick Unlock Pin Entry"),
QObject::tr("Enter a %1%2 digit pin to use for quick unlock:").arg(MIN_PIN_LENGTH).arg(MAX_PIN_LENGTH),
QLineEdit::Password,
{},
&ok);
if (!ok) {
m_error = QObject::tr("Pin setup was canceled. Quick unlock has not been enabled.");
return false;
}
// Validate pin criteria
if (pin.length() >= MIN_PIN_LENGTH && pin.length() <= MAX_PIN_LENGTH && pinRegex.match(pin).hasMatch()) {
// Pin is valid, move to hashing
break;
}
}
} else {
bool ok = false;
pin = QInputDialog::getText(
nullptr,
QObject::tr("Quick Unlock Pin Entry"),
QObject::tr("Enter quick unlock pin (%1 of %2 attempts):").arg(attempt).arg(MAX_PIN_ATTEMPTS),
QLineEdit::Password,
{},
&ok);
if (!ok) {
// User canceled the pin entry dialog, record pin attempts
m_error = QObject::tr("Pin entry was canceled.");
return false;
}
}
// Hash the pin then run it through Argon2 to derive the encryption key
sessionKey.fill('\0', 32);
Argon2Kdf kdf(Argon2Kdf::Type::Argon2id);
CryptoHash hash(CryptoHash::Sha256);
hash.addData(pin.toLatin1());
if (!kdf.transform(hash.result(), sessionKey)) {
m_error = QObject::tr("Failed to derive key using Argon2");
return false;
}
return true;
}
bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data)
{
QByteArray key;
if (!promptPin(0, key)) {
// Pin entry was canceled or failed, error set by promptPin
return false;
}
// Generate a random IV
const auto iv = Random::instance()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
// Encrypt the data using AES-256-GCM
SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) {
m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false;
}
QByteArray encrypted = data;
if (!cipher.finish(encrypted)) {
m_error = QObject::tr("Failed to encrypt key data.");
return false;
}
// Store the encrypted data
saveKey(dbUuid, encrypted.prepend(iv));
return true;
}
bool PinUnlock::getKey(const QUuid& dbUuid, QByteArray& data)
{
data.clear();
bool hasSecret = m_encryptedKeys.contains(dbUuid);
if (!hasSecret) {
// Check if the OS has a secret stored for this database UUID
QByteArray tmp;
if (osUtils->getSecret(dbUuid.toString(), tmp)) {
// Cache the secret in memory
m_encryptedKeys.insert(dbUuid, qMakePair(1, tmp));
} else {
m_error = QObject::tr("Failed to get credentials for quick unlock.");
return false;
}
}
// Restrict pin attempts per database
const auto& pairData = m_encryptedKeys.value(dbUuid);
for (int pinAttempts = pairData.first; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) {
QByteArray key;
if (!promptPin(pinAttempts, key)) {
// Pin entry was canceled or failed, error set by promptPin
m_encryptedKeys.insert(dbUuid, qMakePair(pinAttempts, pairData.second));
return false;
}
// Read the previously used challenge and encrypted data
const auto& ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
const auto& iv = pairData.second.left(ivSize);
// Decrypt the data using the generated key and IV from above
SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false;
}
// Attempt to decrypt the key data
data = pairData.second.mid(ivSize);
if (cipher.finish(data)) {
// Decryption succeeded, reset the pin attempts
m_encryptedKeys.insert(dbUuid, qMakePair(1, pairData.second));
return true;
}
}
data.clear();
m_error = QObject::tr("Too many pin attempts.");
reset(dbUuid);
return false;
}
void PinUnlock::saveKey(const QUuid& dbUuid, const QByteArray& data)
{
// Save the key to the OS secret store
if (!osUtils->saveSecret(dbUuid.toString(), data)) {
qWarning("PinUnlock - Failed to save quick unlock credentials.");
}
// Store the encrypted key in memory
m_encryptedKeys.insert(dbUuid, qMakePair(1, data));
}
bool PinUnlock::hasKey(const QUuid& dbUuid) const
{
bool hasSecret = m_encryptedKeys.contains(dbUuid);
if (!hasSecret) {
// Check if the OS has a secret stored for this database UUID
QByteArray tmp;
hasSecret = osUtils->getSecret(dbUuid.toString(), tmp);
}
return hasSecret;
}
void PinUnlock::reset(const QUuid& dbUuid)
{
m_encryptedKeys.remove(dbUuid);
osUtils->removeSecret(dbUuid.toString());
}
void PinUnlock::reset()
{
m_encryptedKeys.clear();
osUtils->removeAllSecrets();
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_PINUNLOCK_H
#define KEEPASSXC_PINUNLOCK_H
#include "QuickUnlockInterface.h"
#include <QHash>
class PinUnlock : public QuickUnlockInterface
{
public:
PinUnlock() = default;
bool isAvailable() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid) override;
void reset() override;
static const int MIN_PIN_LENGTH;
static const int MAX_PIN_LENGTH;
static const int MAX_PIN_ATTEMPTS;
protected:
bool promptPin(int attempt, QByteArray& sessionKey);
private:
void saveKey(const QUuid& dbUuid, const QByteArray& key);
QHash<QUuid, QPair<int, QByteArray>> m_encryptedKeys;
Q_DISABLE_COPY(PinUnlock)
};
#endif // KEEPASSXC_PINUNLOCK_H

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,8 +23,8 @@
#include "gui/osutils/nixutils/NixUtils.h"
#include <QDebug>
#include <QFile>
#include <QtDBus>
#include <botan/mem_ops.h>
#include <cerrno>
@@ -35,19 +35,11 @@ extern "C" {
const QString polkit_service = "org.freedesktop.PolicyKit1";
const QString polkit_object = "/org/freedesktop/PolicyKit1/Authority";
namespace
{
QString getKeyName(const QUuid& dbUuid)
{
static const QString keyPrefix = "keepassxc_polkit_keys_";
return keyPrefix + dbUuid.toString();
}
} // namespace
Polkit::Polkit()
{
PolkitSubject::registerMetaType();
PolkitAuthorizationResults::registerMetaType();
PolkitActionDescription::registerMetaType();
/* Note we explicitly use our own dbus path here, as the ::systemBus() method could be overridden
through an environment variable to return an alternative bus path. This bus could have an application
@@ -61,18 +53,34 @@ Polkit::Polkit()
m_available = bus.isConnected();
if (!m_available) {
qDebug() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)";
qWarning() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)";
return;
}
m_available = bus.interface()->isServiceRegistered(polkit_service);
if (!m_available) {
qDebug() << "polkit: Polkit is not registered on dbus";
qWarning() << "polkit: Polkit is not registered on dbus";
return;
}
// Initiate the Polkit dbus interface
m_polkit.reset(new org::freedesktop::PolicyKit1::Authority(polkit_service, polkit_object, bus));
// Reset available state and check Polkit registered actions for KeePassXC
m_available = false;
auto kpxcAction = QStringLiteral("org.keepassxc.KeePassXC.unlockDatabase");
auto actions = m_polkit->EnumerateActions("");
for (const auto& action : actions.value()) {
if (action.actionId == kpxcAction) {
m_available = true;
break;
}
}
if (!m_available) {
qWarning() << "polkit: KeePassXC Polkit action is not installed";
}
}
Polkit::~Polkit()
@@ -81,7 +89,8 @@ Polkit::~Polkit()
void Polkit::reset(const QUuid& dbUuid)
{
m_encryptedMasterKeys.remove(dbUuid);
m_sessionKeys.remove(dbUuid);
nixUtils()->removeSecret(dbUuid.toString());
}
bool Polkit::isAvailable() const
@@ -89,67 +98,100 @@ bool Polkit::isAvailable() const
return m_available;
}
QString Polkit::errorString() const
{
return m_error;
}
void Polkit::reset()
{
m_encryptedMasterKeys.clear();
m_sessionKeys.clear();
nixUtils()->removeAllSecrets();
}
bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key)
bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& data)
{
reset(dbUuid);
// Generate a random iv/key pair to encrypt the master password with
QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
QByteArray keychainKeyValue = randomKey + randomIV;
// Prompt for a pin to use as session key
QByteArray key;
if (!promptPin(0, key)) {
return false;
}
auto iv = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
SymmetricCipher aes256Encrypt;
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
m_error = QObject::tr("AES initialization failed");
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) {
m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false;
}
// Encrypt the master password
QByteArray encryptedMasterKey = key;
if (!aes256Encrypt.finish(encryptedMasterKey)) {
m_error = QObject::tr("AES encrypt failed");
qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString();
// Encrypt the database key
QByteArray encrypted = data;
if (!aes256Encrypt.finish(encrypted)) {
m_error = QObject::tr("Failed to encrypt key data.");
return false;
}
// Add the iv/key pair into the linux keyring
key_serial_t key_serial = add_key("user",
getKeyName(dbUuid).toStdString().c_str(),
keychainKeyValue.constData(),
keychainKeyValue.size(),
KEY_SPEC_PROCESS_KEYRING);
if (key_serial < 0) {
m_error = QObject::tr("Failed to store in Linux Keyring");
qDebug() << "polkit keyring failed to store: " << errno;
return false;
}
// Store the session key and save the encrypted master key to the keyring
m_sessionKeys.insert(dbUuid, key);
nixUtils()->saveSecret(dbUuid.toString(), encrypted.prepend(iv));
// Scrub the keys from ram
Botan::secure_scrub_memory(randomKey.data(), randomKey.size());
Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
Botan::secure_scrub_memory(keychainKeyValue.data(), keychainKeyValue.size());
// Store encrypted master password and return
m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
return true;
}
bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
bool Polkit::getKey(const QUuid& dbUuid, QByteArray& data)
{
if (!m_polkit || !hasKey(dbUuid)) {
if (!m_available || !hasKey(dbUuid)) {
m_error = QObject::tr("No key is stored for this database.");
return false;
}
QByteArray key;
for (int pinAttempts = 1; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) {
if (!m_sessionKeys.contains(dbUuid)) {
// Request pin to obtain a session key
if (!promptPin(pinAttempts, key)) {
m_error = QObject::tr("Failed to obtain session key.");
return false;
}
} else {
// We already have the session key, prompt using polkit to authorize use
if (!promptPolkit()) {
// Error set in promptPolkit call
return false;
}
key = m_sessionKeys.value(dbUuid);
}
// Retrieve the encrypted master key from the OS secret store
QByteArray encData;
if (!nixUtils()->getSecret(dbUuid.toString(), encData)) {
m_error = QObject::tr("Failed to get credentials for quick unlock.");
return false;
}
const auto& ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
const auto& iv = encData.left(ivSize);
// Decrypt the data using the generated key and IV from above
SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false;
}
// Attempt to decrypt the key data
data = encData.mid(ivSize);
if (cipher.finish(data)) {
// Decryption succeeded, store the session key used
m_sessionKeys.insert(dbUuid, key);
return true;
}
}
m_error = QObject::tr("Too many pin attempts.");
return false;
}
bool Polkit::promptPolkit()
{
PolkitSubject subject;
subject.kind = "unix-process";
subject.details.insert("pid", static_cast<uint>(QCoreApplication::applicationPid()));
@@ -170,78 +212,26 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
if (result.isError()) {
auto msg = result.error().message();
m_error = QObject::tr("Polkit returned an error: %1").arg(msg);
qDebug() << "polkit returned an error: " << msg;
return false;
}
PolkitAuthorizationResults authResult = result.value();
if (authResult.is_authorized) {
QByteArray encryptedMasterKey = m_encryptedMasterKeys.value(dbUuid);
key_serial_t keySerial =
find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING);
if (keySerial == -1) {
m_error = QObject::tr("Could not locate key in keyring");
qDebug() << "polkit keyring failed to find: " << errno;
return false;
}
void* keychainBuffer;
long keychainDataSize = keyctl_read_alloc(keySerial, &keychainBuffer);
if (keychainDataSize == -1) {
m_error = QObject::tr("Could not read key in keyring");
qDebug() << "polkit keyring failed to read: " << errno;
return false;
}
QByteArray keychainBytes(static_cast<const char*>(keychainBuffer), keychainDataSize);
Botan::secure_scrub_memory(keychainBuffer, keychainDataSize);
free(keychainBuffer);
QByteArray keychainKey = keychainBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
QByteArray keychainIv = keychainBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
SymmetricCipher aes256Decrypt;
if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, keychainKey, keychainIv)) {
m_error = QObject::tr("AES initialization failed");
qDebug() << "polkit aes init failed";
return false;
}
key = encryptedMasterKey;
if (!aes256Decrypt.finish(key)) {
key.clear();
m_error = QObject::tr("AES decrypt failed");
qDebug() << "polkit aes decrypt failed: " << aes256Decrypt.errorString();
return false;
}
// Scrub the keys from ram
Botan::secure_scrub_memory(keychainKey.data(), keychainKey.size());
Botan::secure_scrub_memory(keychainIv.data(), keychainIv.size());
Botan::secure_scrub_memory(keychainBytes.data(), keychainBytes.size());
Botan::secure_scrub_memory(encryptedMasterKey.data(), encryptedMasterKey.size());
return true;
}
// Failed to authenticate
if (authResult.is_challenge) {
m_error = QObject::tr("No Polkit authentication agent was available");
m_error = QObject::tr("No Polkit authentication agent was available.");
} else {
m_error = QObject::tr("Polkit authorization failed");
m_error = QObject::tr("Polkit authorization failed.");
}
return false;
}
bool Polkit::hasKey(const QUuid& dbUuid) const
{
if (!m_encryptedMasterKeys.contains(dbUuid)) {
return false;
}
return find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING) != -1;
// Check if the OS has a secret stored for this database UUID
QByteArray tmp;
return nixUtils()->getSecret(dbUuid.toString(), tmp);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,36 +15,34 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_POLKIT_H
#define KEEPASSX_POLKIT_H
#pragma once
#include "QuickUnlockInterface.h"
#include "PinUnlock.h"
#include "polkit_dbus.h"
#include <QHash>
#include <QScopedPointer>
class Polkit : public QuickUnlockInterface
class Polkit : public PinUnlock
{
public:
Polkit();
~Polkit() override;
bool isAvailable() const override;
QString errorString() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool setKey(const QUuid& dbUuid, const QByteArray& data) override;
bool getKey(const QUuid& dbUuid, QByteArray& data) override;
bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid) override;
void reset() override;
private:
bool promptPolkit();
bool m_available;
QString m_error;
QHash<QUuid, QByteArray> m_encryptedMasterKeys;
QHash<QUuid, QByteArray> m_sessionKeys;
QScopedPointer<org::freedesktop::PolicyKit1::Authority> m_polkit;
};
#endif // KEEPASSX_POLKIT_H

View File

@@ -1,3 +1,20 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PolkitDbusTypes.h"
void PolkitSubject::registerMetaType()
@@ -43,3 +60,32 @@ const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizati
argument.endStructure();
return argument;
}
void PolkitActionDescription::registerMetaType()
{
qRegisterMetaType<PolkitActionDescription>("PolkitActionDescription");
qDBusRegisterMetaType<PolkitActionDescription>();
qRegisterMetaType<PolkitActionDescriptionList>("PolkitActionDescriptionList");
qDBusRegisterMetaType<PolkitActionDescriptionList>();
}
QDBusArgument& operator<<(QDBusArgument& argument, const PolkitActionDescription& action)
{
argument.beginStructure();
argument << action.actionId << action.description << action.message << action.vendorName << action.vendorUrl
<< action.iconName << action.implicitAny << action.implicitInactive << action.implicitActive
<< action.annotations;
argument.endStructure();
return argument;
}
const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitActionDescription& action)
{
argument.beginStructure();
argument >> action.actionId >> action.description >> action.message >> action.vendorName >> action.vendorUrl
>> action.iconName >> action.implicitAny >> action.implicitInactive >> action.implicitActive
>> action.annotations;
argument.endStructure();
return argument;
}

View File

@@ -1,5 +1,21 @@
#ifndef KEEPASSX_POLKITDBUSTYPES_H
#define KEEPASSX_POLKITDBUSTYPES_H
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QtDBus>
@@ -30,7 +46,30 @@ public:
friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizationResults& subject);
};
class PolkitActionDescription
{
public:
QString actionId;
QString description;
QString message;
QString vendorName;
QString vendorUrl;
QString iconName;
uint implicitAny;
uint implicitInactive;
uint implicitActive;
QMap<QString, QString> annotations;
static void registerMetaType();
friend QDBusArgument& operator<<(QDBusArgument& argument, const PolkitActionDescription& action);
friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitActionDescription& action);
};
typedef QList<PolkitActionDescription> PolkitActionDescriptionList;
Q_DECLARE_METATYPE(PolkitSubject);
Q_DECLARE_METATYPE(PolkitAuthorizationResults);
#endif // KEEPASSX_POLKITDBUSTYPES_H
Q_DECLARE_METATYPE(PolkitActionDescription);
Q_DECLARE_METATYPE(PolkitActionDescriptionList);

View File

@@ -16,66 +16,55 @@
*/
#include "QuickUnlockInterface.h"
#include "PinUnlock.h"
#include <QObject>
#if defined(Q_OS_MACOS)
#include "TouchID.h"
#define QUICKUNLOCK_IMPLEMENTATION TouchID
#elif defined(Q_CC_MSVC)
#include "WindowsHello.h"
#define QUICKUNLOCK_IMPLEMENTATION WindowsHello
#elif defined(Q_OS_LINUX)
#include "Polkit.h"
#define QUICKUNLOCK_IMPLEMENTATION Polkit
#else
#define QUICKUNLOCK_IMPLEMENTATION NoQuickUnlock
#endif
QUICKUNLOCK_IMPLEMENTATION* quickUnlockInstance = {nullptr};
QuickUnlockManager* g_quickUnlockManager = nullptr;
QuickUnlockInterface* getQuickUnlock()
QuickUnlockManager* getQuickUnlock()
{
if (!quickUnlockInstance) {
quickUnlockInstance = new QUICKUNLOCK_IMPLEMENTATION();
if (!g_quickUnlockManager) {
g_quickUnlockManager = new QuickUnlockManager();
}
return quickUnlockInstance;
return g_quickUnlockManager;
}
bool NoQuickUnlock::isAvailable() const
QuickUnlockManager::QuickUnlockManager()
{
return false;
// Create the native interface based on the platform
#if defined(Q_OS_MACOS)
m_nativeInterface.reset(new TouchID());
#elif defined(Q_CC_MSVC)
m_nativeInterface.reset(new WindowsHello());
#elif defined(Q_OS_LINUX)
m_nativeInterface.reset(new Polkit());
#endif
// Always create the fallback interface
m_fallbackInterface.reset(new PinUnlock());
}
QString NoQuickUnlock::errorString() const
{
return QObject::tr("No Quick Unlock provider is available");
}
void NoQuickUnlock::reset()
QuickUnlockManager::~QuickUnlockManager()
{
}
bool NoQuickUnlock::setKey(const QUuid& dbUuid, const QByteArray& key)
QSharedPointer<QuickUnlockInterface> QuickUnlockManager::interface() const
{
Q_UNUSED(dbUuid)
Q_UNUSED(key)
return false;
if (isNativeAvailable()) {
return m_nativeInterface;
}
return m_fallbackInterface;
}
bool NoQuickUnlock::getKey(const QUuid& dbUuid, QByteArray& key)
bool QuickUnlockManager::isNativeAvailable() const
{
Q_UNUSED(dbUuid)
Q_UNUSED(key)
return false;
}
bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const
{
Q_UNUSED(dbUuid)
return false;
}
void NoQuickUnlock::reset(const QUuid& dbUuid)
{
Q_UNUSED(dbUuid)
return m_nativeInterface && m_nativeInterface->isAvailable();
}

View File

@@ -18,6 +18,7 @@
#ifndef KEEPASSXC_QUICKUNLOCKINTERFACE_H
#define KEEPASSXC_QUICKUNLOCKINTERFACE_H
#include <QSharedPointer>
#include <QUuid>
class QuickUnlockInterface
@@ -29,7 +30,6 @@ public:
virtual ~QuickUnlockInterface() = default;
virtual bool isAvailable() const = 0;
virtual QString errorString() const = 0;
virtual bool setKey(const QUuid& dbUuid, const QByteArray& key) = 0;
virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0;
@@ -37,22 +37,32 @@ public:
virtual void reset(const QUuid& dbUuid) = 0;
virtual void reset() = 0;
virtual QString errorString() const
{
return m_error;
}
protected:
QString m_error;
};
class NoQuickUnlock : public QuickUnlockInterface
class QuickUnlockManager final
{
Q_DISABLE_COPY(QuickUnlockManager)
public:
bool isAvailable() const override;
QString errorString() const override;
QuickUnlockManager();
~QuickUnlockManager();
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool hasKey(const QUuid& dbUuid) const override;
QSharedPointer<QuickUnlockInterface> interface() const;
bool isNativeAvailable() const;
void reset(const QUuid& dbUuid) override;
void reset() override;
private:
QSharedPointer<QuickUnlockInterface> m_nativeInterface;
QSharedPointer<QuickUnlockInterface> m_fallbackInterface;
};
QuickUnlockInterface* getQuickUnlock();
QuickUnlockManager* getQuickUnlock();
#endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "quickunlock/TouchID.h"
#include "gui/osutils/OSUtils.h"
/**
* Store the serialized database key into the macOS key store. The OS handles encrypt/decrypt operations.
* https://developer.apple.com/documentation/security/keychain_services/keychain_items
*/
bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& key)
{
if (key.isEmpty()) {
qWarning("TouchID::setKey - provided key is empty");
return false;
}
return osUtils->saveSecret(dbUuid.toString(), key);
}
/**
* Retrieve serialized key data from the macOS Keychain after successful authentication
* with TouchID or Watch interface.
*/
bool TouchID::getKey(const QUuid& dbUuid, QByteArray& key)
{
key.clear();
if (!hasKey(dbUuid)) {
qWarning("TouchID::getKey - No stored key found");
return false;
}
return osUtils->getSecret(dbUuid.toString(), key);
}
bool TouchID::hasKey(const QUuid& dbUuid) const
{
QByteArray tmp;
return osUtils->getSecret(dbUuid.toString(), tmp);
}
bool TouchID::isAvailable() const
{
return macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::TouchId)
|| macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::Watch)
|| macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::PasswordFallback);
}
void TouchID::reset(const QUuid& dbUuid)
{
osUtils->removeSecret(dbUuid.toString());
}
void TouchID::reset()
{
osUtils->removeAllSecrets();
}

View File

@@ -15,17 +15,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_TOUCHID_H
#define KEEPASSX_TOUCHID_H
#pragma once
#include "QuickUnlockInterface.h"
#include <QHash>
class TouchID : public QuickUnlockInterface
{
public:
bool isAvailable() const override;
QString errorString() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey) override;
bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override;
@@ -33,17 +30,4 @@ public:
void reset(const QUuid& dbUuid = "") override;
void reset() override;
private:
static bool isWatchAvailable();
static bool isTouchIdAvailable();
static bool isPasswordFallbackPossible();
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID);
static void deleteKeyEntry(const QString& accountName);
static QString databaseKeyName(const QUuid& dbUuid);
QHash<QUuid, QByteArray> m_encryptedMasterKeys;
};
#endif // KEEPASSX_TOUCHID_H

View File

@@ -1,408 +0,0 @@
#include "quickunlock/TouchID.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
#include "crypto/CryptoHash.h"
#include "config-keepassx.h"
#include <botan/mem_ops.h>
#include <Foundation/Foundation.h>
#include <CoreFoundation/CoreFoundation.h>
#include <LocalAuthentication/LocalAuthentication.h>
#include <Security/Security.h>
#include <QCoreApplication>
#include <QString>
#define TOUCH_ID_ENABLE_DEBUG_LOGS() 0
#if TOUCH_ID_ENABLE_DEBUG_LOGS()
#define debug(...) qWarning(__VA_ARGS__)
#else
inline void debug(const char *message, ...)
{
Q_UNUSED(message);
}
#endif
inline std::string StatusToErrorMessage(OSStatus status)
{
CFStringRef text = SecCopyErrorMessageString(status, NULL);
if (!text) {
return std::to_string(status);
}
auto msg = CFStringGetCStringPtr(text, kCFStringEncodingUTF8);
std::string result;
if (msg) {
result = msg;
}
CFRelease(text);
return result;
}
inline void LogStatusError(const char *message, OSStatus status)
{
if (!status) {
return;
}
std::string msg = StatusToErrorMessage(status);
debug("%s: %s", message, msg.c_str());
}
inline CFMutableDictionaryRef makeDictionary() {
return CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
}
//! Try to delete an existing keychain entry
void TouchID::deleteKeyEntry(const QString& accountName)
{
NSString* nsAccountName = accountName.toNSString(); // The NSString is released by Qt
// try to delete an existing entry
CFMutableDictionaryRef query = makeDictionary();
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) nsAccountName);
CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
// get data from the KeyChain
OSStatus status = SecItemDelete(query);
LogStatusError("TouchID::deleteKeyEntry - Status deleting existing entry", status);
}
QString TouchID::databaseKeyName(const QUuid& dbUuid)
{
static const QString keyPrefix = "KeepassXC_TouchID_Keys_";
return keyPrefix + dbUuid.toString();
}
QString TouchID::errorString() const
{
// TODO
return "";
}
void TouchID::reset()
{
m_encryptedMasterKeys.clear();
}
bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID)
{
if (passwordKey.isEmpty()) {
debug("TouchID::setKey - illegal arguments");
return false;
}
if (m_encryptedMasterKeys.contains(dbUuid)) {
debug("TouchID::setKey - Already stored key for this database");
return true;
}
// generate random AES 256bit key and IV
QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
SymmetricCipher aes256Encrypt;
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
debug("TouchID::setKey - AES initialisation failed");
return false;
}
// encrypt and keep result in memory
QByteArray encryptedMasterKey = passwordKey;
if (!aes256Encrypt.finish(encryptedMasterKey)) {
debug("TouchID::getKey - AES encrypt failed: %s", aes256Encrypt.errorString().toUtf8().constData());
return false;
}
const QString keyName = databaseKeyName(dbUuid);
deleteKeyEntry(keyName); // Try to delete the existing key entry
// prepare adding secure entry to the macOS KeyChain
CFErrorRef error = NULL;
// We need both runtime and compile time checks here to solve the following problems:
// - Not all flags are available in all OS versions, so we have to check it at compile time
// - Requesting Biometry/TouchID/DevicePassword when to fingerprint sensor is available will result in runtime error
SecAccessControlCreateFlags accessControlFlags = 0;
#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
// Needs a special check to work with SecItemAdd, when TouchID is not enrolled and the flag
// is set, the method call fails with an error. But we want to still set this flag if TouchID is
// enrolled but temporarily unavailable due to closed lid
//
// At least on a Hackintosh the enrolled-check does not work, there LAErrorBiometryNotAvailable gets returned instead of
// LAErrorBiometryNotEnrolled.
//
// That's kinda unfortunate, because now you cannot know for sure if TouchID hardware is either temporarily unavailable or not present
// at all, because LAErrorBiometryNotAvailable is used for both cases.
//
// So to make quick unlock fallbacks possible on these machines you have to try to save the key a second time without this flag, if the
// first try fails with an error.
if (!ignoreTouchID) {
// Prefer the non-deprecated flag when available
accessControlFlags = kSecAccessControlBiometryCurrentSet;
}
#elif XC_COMPILER_SUPPORT(TOUCH_ID)
if (!ignoreTouchID) {
accessControlFlags = kSecAccessControlTouchIDCurrentSet;
}
#endif
#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch;
#endif
#if XC_COMPILER_SUPPORT(TOUCH_ID)
if (isPasswordFallbackPossible()) {
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlDevicePasscode;
}
#endif
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(
kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
if (sacObject == NULL || error != NULL) {
NSError* e = (__bridge NSError*) error;
debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String);
return false;
}
NSString *accountName = keyName.toNSString(); // The NSString is released by Qt
// prepare data (key) to be stored
QByteArray keychainKeyValue = (randomKey + randomIV).toHex();
CFDataRef keychainValueData =
CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, reinterpret_cast<UInt8 *>(keychainKeyValue.data()),
keychainKeyValue.length(), kCFAllocatorDefault);
CFMutableDictionaryRef attributes = makeDictionary();
CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName);
CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keychainValueData);
CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject);
// add to KeyChain
OSStatus status = SecItemAdd(attributes, NULL);
LogStatusError("TouchID::setKey - Status adding new entry", status);
CFRelease(sacObject);
CFRelease(attributes);
// Cleanse the key information from the memory
Botan::secure_scrub_memory(randomKey.data(), randomKey.size());
Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
if (status != errSecSuccess) {
return false;
}
// memorize which database the stored key is for
m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
debug("TouchID::setKey - Success!");
return true;
}
/**
* Generates a random AES 256bit key and uses it to encrypt the PasswordKey that
* protects the database. The encrypted PasswordKey is kept in memory while the
* AES key is stored in the macOS KeyChain protected by either TouchID or Apple Watch.
*/
bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey)
{
if (!setKey(dbUuid,passwordKey, false)) {
debug("TouchID::setKey failed with error trying fallback method without TouchID flag");
return setKey(dbUuid, passwordKey, true);
} else {
return true;
}
}
/**
* Checks if an encrypted PasswordKey is available for the given database, tries to
* decrypt it using the KeyChain and if successful, returns it.
*/
bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey)
{
passwordKey.clear();
if (!hasKey(dbUuid)) {
debug("TouchID::getKey - No stored key found");
return false;
}
// query the KeyChain for the AES key
CFMutableDictionaryRef query = makeDictionary();
const QString keyName = databaseKeyName(dbUuid);
NSString* accountName = keyName.toNSString(); // The NSString is released by Qt
NSString* touchPromptMessage =
QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database")
.toNSString(); // The NSString is released by Qt
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName);
CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
CFDictionarySetValue(query, kSecUseOperationPrompt, (__bridge CFStringRef) touchPromptMessage);
// get data from the KeyChain
CFTypeRef dataTypeRef = NULL;
OSStatus status = SecItemCopyMatching(query, &dataTypeRef);
CFRelease(query);
if (status == errSecUserCanceled) {
// user canceled the authentication, return true with empty key
debug("TouchID::getKey - User canceled authentication");
return true;
} else if (status != errSecSuccess || dataTypeRef == NULL) {
LogStatusError("TouchID::getKey - key query error", status);
return false;
}
CFDataRef valueData = static_cast<CFDataRef>(dataTypeRef);
QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)),
CFDataGetLength(valueData)));
CFRelease(dataTypeRef);
// extract AES key and IV from data bytes
QByteArray key = dataBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
QByteArray iv = dataBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
SymmetricCipher aes256Decrypt;
if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
debug("TouchID::getKey - AES initialization failed");
return false;
}
// decrypt PasswordKey from memory using AES
passwordKey = m_encryptedMasterKeys[dbUuid];
if (!aes256Decrypt.finish(passwordKey)) {
passwordKey.clear();
debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData());
return false;
}
// Cleanse the key information from the memory
Botan::secure_scrub_memory(key.data(), key.size());
Botan::secure_scrub_memory(iv.data(), iv.size());
return true;
}
bool TouchID::hasKey(const QUuid& dbUuid) const
{
return m_encryptedMasterKeys.contains(dbUuid);
}
// TODO: Both functions below should probably handle the returned errors to
// provide more information on availability. E.g.: the closed laptop lid results
// in an error (because touch id is not unavailable). That error could be
// displayed to the user when we first check for availability instead of just
// hiding the checkbox.
//! @return true if Apple Watch is available for authentication.
bool TouchID::isWatchAvailable()
{
#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
@try {
LAContext *context = [[LAContext alloc] init];
LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch;
NSError *error;
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
[context release];
if (error) {
debug("Apple Wach available: %d (%ld / %s / %s)", canAuthenticate,
(long)error.code, error.description.UTF8String,
error.localizedDescription.UTF8String);
} else {
debug("Apple Wach available: %d", canAuthenticate);
}
return canAuthenticate;
} @catch (NSException *) {
return false;
}
#else
return false;
#endif
}
//! @return true if Touch ID is available for authentication.
bool TouchID::isTouchIdAvailable()
{
#if XC_COMPILER_SUPPORT(TOUCH_ID)
@try {
LAContext *context = [[LAContext alloc] init];
LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
NSError *error;
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
[context release];
if (error) {
debug("Touch ID available: %d (%ld / %s / %s)", canAuthenticate,
(long)error.code, error.description.UTF8String,
error.localizedDescription.UTF8String);
} else {
debug("Touch ID available: %d", canAuthenticate);
}
return canAuthenticate;
} @catch (NSException *) {
return false;
}
#else
return false;
#endif
}
bool TouchID::isPasswordFallbackPossible()
{
#if XC_COMPILER_SUPPORT(TOUCH_ID)
@try {
LAContext *context = [[LAContext alloc] init];
LAPolicy policyCode = LAPolicyDeviceOwnerAuthentication;
NSError *error;
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
[context release];
if (error) {
debug("Password fallback available: %d (%ld / %s / %s)", canAuthenticate,
(long)error.code, error.description.UTF8String,
error.localizedDescription.UTF8String);
} else {
debug("Password fallback available: %d", canAuthenticate);
}
return canAuthenticate;
} @catch (NSException *) {
return false;
}
#else
return false;
#endif
}
//! @return true if either TouchID or Apple Watch is available at the moment.
bool TouchID::isAvailable() const
{
// note: we cannot cache the check results because the configuration
// is dynamic in its nature. User can close the laptop lid or take off
// the watch, thus making one (or both) of the authentication types unavailable.
return isWatchAvailable() || isTouchIdAvailable() || isPasswordFallbackPossible();
}
/**
* Resets the inner state either for all or for the given database
*/
void TouchID::reset(const QUuid& dbUuid)
{
m_encryptedMasterKeys.remove(dbUuid);
}

View File

@@ -17,8 +17,9 @@
#include "WindowsHello.h"
#include <Userconsentverifierinterop.h>
#include <Windows.h>
#include <winrt/base.h>
#include <winrt/windows.foundation.collections.h>
#include <winrt/windows.foundation.h>
#include <winrt/windows.security.credentials.h>
#include <winrt/windows.security.cryptography.h>
@@ -28,12 +29,14 @@
#include "crypto/CryptoHash.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
#include "gui/osutils/OSUtils.h"
#include <QTimer>
#include <QWindow>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Security::Credentials;
using namespace Windows::Security::Cryptography;
using namespace Windows::Storage::Streams;
@@ -43,17 +46,20 @@ namespace
const std::wstring s_winHelloKeyName{L"keepassxc_winhello"};
int g_promptFocusCount = 0;
void queueSecurityPromptFocus(int delay = 500)
void queueSecurityPromptFocus(bool initial, int delay = 500)
{
if (initial) {
g_promptFocusCount = 0;
}
QTimer::singleShot(delay, [] {
auto hWnd = ::FindWindowA("Credential Dialog Xaml Host", nullptr);
if (hWnd) {
::SetForegroundWindow(hWnd);
} else if (++g_promptFocusCount <= 3) {
queueSecurityPromptFocus();
return;
qDebug("WindowsHello - Could not find security prompt window");
queueSecurityPromptFocus(false);
}
g_promptFocusCount = 0;
});
}
@@ -105,14 +111,9 @@ bool WindowsHello::isAvailable() const
return task.get();
}
QString WindowsHello::errorString() const
{
return m_error;
}
bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
{
queueSecurityPromptFocus();
queueSecurityPromptFocus(true);
// Generate a random challenge that will be signed by Windows Hello
// to create the key. The challenge is also used as the IV.
@@ -120,6 +121,7 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
auto challenge = Random::instance()->randomArray(ivSize);
QByteArray key;
if (!deriveEncryptionKey(challenge, key, m_error)) {
m_error = QObject::tr("Windows Hello setup was canceled or failed. Quick unlock has not been enabled.");
return false;
}
@@ -137,28 +139,28 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
// Prepend the challenge/IV to the encrypted data
encrypted.prepend(challenge);
m_encryptedKeys.insert(dbUuid, encrypted);
return true;
return osUtils->saveSecret(dbUuid.toString(), encrypted);
}
bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
{
data.clear();
if (!hasKey(dbUuid)) {
m_error = QObject::tr("Failed to get Windows Hello credential.");
QByteArray keydata;
if (!osUtils->getSecret(dbUuid.toString(), keydata)) {
m_error = QObject::tr("Failed to retrieve Windows Hello credential.");
return false;
}
queueSecurityPromptFocus();
queueSecurityPromptFocus(true);
// Read the previously used challenge and encrypted data
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
const auto& keydata = m_encryptedKeys.value(dbUuid);
auto challenge = keydata.left(ivSize);
auto encrypted = keydata.mid(ivSize);
QByteArray key;
QByteArray key;
if (!deriveEncryptionKey(challenge, key, m_error)) {
// Error is set in deriveEncryptionKey
return false;
}
@@ -182,15 +184,16 @@ bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
void WindowsHello::reset(const QUuid& dbUuid)
{
m_encryptedKeys.remove(dbUuid);
osUtils->removeSecret(dbUuid.toString());
}
bool WindowsHello::hasKey(const QUuid& dbUuid) const
{
return m_encryptedKeys.contains(dbUuid);
QByteArray tmp;
return osUtils->getSecret(dbUuid.toString(), tmp);
}
void WindowsHello::reset()
{
m_encryptedKeys.clear();
osUtils->removeAllSecrets();
}

View File

@@ -20,26 +20,22 @@
#include "QuickUnlockInterface.h"
#include <QHash>
#include <QObject>
class WindowsHello : public QuickUnlockInterface
{
public:
WindowsHello() = default;
bool isAvailable() const override;
QString errorString() const override;
void reset() override;
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid) override;
void reset() override;
private:
QString m_error;
QHash<QUuid, QByteArray> m_encryptedKeys;
Q_DISABLE_COPY(WindowsHello);
Q_DISABLE_COPY(WindowsHello)
};
#endif // KEEPASSXC_WINDOWSHELLO_H

View File

@@ -12,5 +12,10 @@
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="PolkitSubject"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QMap&lt;QString, QString&gt;"/>
</method>
<method name="EnumerateActions">
<arg type="s" name="locale" direction="in" />
<arg type="a(ssssssuuua{ss})" name="action_descriptions" direction="out" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="PolkitActionDescriptionList"/>
</method>
</interface>
</node>

View File

@@ -626,17 +626,6 @@ void TestEntry::testResolveReplacePlaceholders()
// Test complicated and nested replacements
QCOMPARE(entry2->resolveMultiplePlaceholders(entry2->url()),
QString("cmd://sap.exe -system=server1 -client=12345 -user=Username2 -pw=Password1"));
auto* entry3 = new Entry();
entry3->setGroup(root);
entry3->setUuid(QUuid::createUuid());
entry3->setTitle("Entry 3");
entry3->setUsername("HMAC-SHA-256");
entry3->setUrl("{T-REPLACE-RX:!{USERNAME}!\\{USERNAME\\}!!}");
// Test escaped enclosures
QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->url()), entry3->username());
// Test invalid syntax
QString error;
entry1->resolveRegexPlaceholder("{T-REPLACE-RX:/{USERNAME}/.*+?/test/}", &error); // invalid regex

View File

@@ -451,14 +451,3 @@ void TestTools::testCleanUsername_data()
QTest::newRow("Trailing dots and spaces") << "username... " << "username";
QTest::newRow("Combination of issues") << R"( user<>:"/\|?*name... )" << "user_________name";
}
void TestTools::testEscapeAccelerators()
{
QCOMPARE(Tools::escapeAccelerators(""), "");
QCOMPARE(Tools::escapeAccelerators("NoAccelerator"), "NoAccelerator");
QCOMPARE(Tools::escapeAccelerators("&Accelerator"), "&&Accelerator");
QCOMPARE(Tools::escapeAccelerators("Accelerator&"), "Accelerator&&");
QCOMPARE(Tools::escapeAccelerators("Accel&erator&"), "Accel&&erator&&");
QCOMPARE(Tools::escapeAccelerators("Accel&&erator"), "Accel&&&&erator");
QCOMPARE(Tools::escapeAccelerators("Some & text"), "Some && text");
}

View File

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

View File

@@ -1,70 +1,77 @@
#!/usr/bin/env python3
from collections import defaultdict
import json
import sys
from pathlib import Path
from urllib import request
import os
txrc = Path.home() / '.transifexrc'
if not txrc.exists():
print('No Transifex config found. Run tx init first.')
sys.exit(1)
# Download Transifex languages dump at: https://www.transifex.com/api/2/project/keepassxc/languages
# Language information from https://www.wikiwand.com/en/List_of_ISO_639-1_codes and http://www.lingoes.net/en/translator/langcode.htm
org = 'o:keepassxc'
proj = f'{org}:p:keepassxc'
resource = f'{proj}:r:share-translations-keepassxc-en-ts--master'
token = [l for l in open(txrc, 'r') if l.startswith('token')][0].split('=', 1)[1].strip()
member_blacklist = ['u:droidmonkey', 'u:phoerious']
LANGS = {
"ar" : "العربية (Arabic)",
"bn" : "বাংলা (Bengali)",
"ca" : "català (Catalan)",
"cs" : "čeština (Czech)",
"da" : "dansk (Danish)",
"de" : "Deutsch (German)",
"el" : "ελληνικά (Greek)",
"eo" : "Esperanto (Esperanto)",
"es" : "Español (Spanish)",
"et" : "eesti (Estonian)",
"eu" : "euskara (Basque)",
"fa" : "فارسی (Farsi)",
"fa_IR" : "فارسی (Farsi (Iran))",
"fi" : "suomi (Finnish)",
"fr" : "français (French)",
"gl" : "Galego (Galician)",
"he" : "עברית (Hebrew)",
"hr_HR" : "hrvatski jezik (Croatian)",
"hu" : "magyar (Hungarian)",
"id" : "Bahasa Indonesia (Indonesian)",
"is_IS" : "Íslenska (Icelandic)",
"it" : "Italiano (Italian)",
"ja" : "日本語 (Japanese)",
"kk" : "қазақ тілі (Kazakh)",
"ko" : "한국어 (Korean)",
"la" : "latine (Latin)",
"lt" : "lietuvių kalba (Lithuanian)",
"lv" : "latviešu valoda (Latvian)",
"nb" : "Norsk Bokmål (Norwegian Bokmål)",
"nl_NL" : "Nederlands (Dutch)",
"my" : "ဗမာစာ (Burmese)",
"pa" : "ਪੰਜਾਬੀ (Punjabi)",
"pa_IN" : "ਪੰਜਾਬੀ (Punjabi (India))",
"pl" : "język polski (Polish)",
"pt" : "Português (Portuguese)",
"pt_BR" : "Português (Portuguese (Brazil))",
"pt_PT" : "Português (Portuguese (Portugal))",
"ro" : "Română (Romanian)",
"ru" : "русский (Russian)",
"sk" : "Slovenčina (Slovak)",
"sl_SI" : "Slovenščina (Slovenian)",
"sr" : "српски језик (Serbian)",
"sv" : "Svenska (Swedish)",
"th" : "ไทย (Thai)",
"tr" : "Türkçe (Turkish)",
"uk" : "Українська (Ukrainian)",
"zh_CN" : "中文 (Chinese (Simplified))",
"zh_TW" : "中文 (台灣) (Chinese (Traditional))",
}
TEMPLATE = "<li><strong>{0}</strong>: {1}</li>\n"
def get_url(url):
req = request.Request(url)
req.add_header('Content-Type', 'application/vnd.api+json')
req.add_header('Authorization', f'Bearer {token}')
with request.urlopen(req) as resp:
return json.load(resp)
if not os.path.exists("languages.json"):
print("Could not find 'languages.json' in current directory!")
print("Save the output from https://www.transifex.com/api/2/project/keepassxc/languages")
exit(0)
print('Fetching languages...', file=sys.stderr)
languages_json = get_url(f'https://rest.api.transifex.com/projects/{proj}/languages')
languages = {}
for lang in languages_json['data']:
languages[lang['id']] = lang['attributes']['name']
print('Fetching language stats...', file=sys.stderr)
language_stats_json = get_url('https://rest.api.transifex.com/resource_language_stats?'
f'filter[project]={proj}&filter[resource]={resource}')
completion = {}
for stat in language_stats_json['data']:
completion = stat['attributes']['translated_strings'] / stat['attributes']['total_strings']
if completion < .6:
languages.pop(stat['relationships']['language']['data']['id'])
print('Fetching language members...', end='', file=sys.stderr)
members_json = get_url(f'https://rest.api.transifex.com/team_memberships?filter[organization]={org}')
members = defaultdict(set)
for member in members_json['data']:
print('.', end='', file=sys.stderr)
if member['relationships']['user']['data']['id'] in member_blacklist:
continue
lid = member['relationships']['language']['data']['id']
if lid not in languages:
continue
user = get_url(member['relationships']['user']['links']['related'])['data']['attributes']['username']
members[lid].add(user)
print(file=sys.stderr)
print('<ul>')
for lang in sorted(languages, key=lambda x: languages[x]):
if not members[lang]:
continue
lines = [f' <li><strong>{languages[lang]}:</strong> ']
for i, m in enumerate(sorted(members[lang], key=lambda x: x.lower())):
if len(lines[-1]) + len(m) >= 120:
lines.append(' ')
lines[-1] += m
if i < len(members[lang]) - 1:
lines[-1] += ', '
lines[-1] += '</li>'
print('\n'.join(lines))
print('</ul>')
with open("languages.json") as json_file:
output = open("translators.html", "w", encoding="utf-8")
languages = json.load(json_file)
for lang in languages:
code = lang["language_code"]
if code not in LANGS:
print("WARNING: Could not find language code:", code)
continue
translators = ", ".join(sorted(lang["reviewers"] + lang["translators"], key=str.casefold))
output.write(TEMPLATE.format(LANGS[code], translators))
output.close()
print("Language translators written to 'translators.html'!")