Fix repeating Touch ID prompt on MacOS

Also adds the APPLE_TEAM_ID and APPLE_APP_IDENTIFIER variables to CMake for using the keychain with a custom provisioning profile.
This commit is contained in:
Sebastian Livoni 2025-11-06 13:04:01 +01:00
parent f15ba49fc6
commit 065c144633
No known key found for this signature in database
GPG key ID: 827A963A22F2E6B4
10 changed files with 43 additions and 7 deletions

2
release-tool.py Executable file → Normal file
View file

@ -1010,7 +1010,7 @@ class AppSign(Command):
# (Re-)Sign main executable with --entitlements
logger.debug('Signing main executable...')
_run(['xcrun', 'codesign', f'--sign={identity}', '--force', '--options=runtime',
'--entitlements', (src_dir / 'share/macosx/keepassxc.entitlements').as_posix(),
'--entitlements', (src_dir / 'build/src/keepassxc.entitlements').as_posix(),
(app_dir_app / 'Contents/MacOS/KeePassXC').as_posix()], cwd=None)
tmp_out = out_file.with_suffix(f'.{"".join(random.choices(string.ascii_letters, k=8))}{file.suffix}')

View file

@ -15,7 +15,7 @@
<key>CFBundleIconFile</key>
<string>keepassxc.icns</string>
<key>CFBundleIdentifier</key>
<string>org.keepassxc.keepassxc</string>
<string>${APPLE_APP_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>

View file

@ -3,10 +3,10 @@
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>G2S7P7J672.org.keepassxc.keepassxc</string>
<string>${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER}</string>
<key>keychain-access-groups</key>
<array>
<string>G2S7P7J672.org.keepassxc.keepassxc</string>
<string>${APPLE_TEAM_ID}.${APPLE_APP_IDENTIFIER}</string>
</array>
</dict>
</plist>

View file

@ -442,12 +442,16 @@ set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
# macOS App Bundle
if(APPLE AND WITH_APP_BUNDLE)
set(APPLE_TEAM_ID "G2S7P7J672" CACHE STRING "Apple Developer Team ID")
set(APPLE_APP_IDENTIFIER "org.keepassxc.keepassxc" CACHE STRING "App Bundle Identifier")
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)
configure_file(${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements.cmake ${CMAKE_CURRENT_BINARY_DIR}/keepassxc.entitlements)
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")
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_CURRENT_BINARY_DIR}/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

@ -77,6 +77,7 @@ public:
*/
virtual bool saveSecret(const QString& key, const QByteArray& secretData) const = 0;
virtual bool getSecret(const QString& key, QByteArray& secretData) const = 0;
virtual bool hasSecret(const QString& key) const = 0;
virtual bool removeSecret(const QString& key) const = 0;
virtual bool removeAllSecrets() const = 0;

View file

@ -445,6 +445,7 @@ bool MacUtils::saveSecret(const QString& key, const QByteArray& secretData) cons
CFDictionarySetValue(attributes, kSecAttrAccount, static_cast<CFStringRef>(keyName.toNSString()));
CFDictionarySetValue(attributes, kSecValueData, keyValueData);
CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
CFDictionarySetValue(attributes, kSecUseDataProtectionKeychain, kCFBooleanTrue);
CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
// First, attempt with TouchID enabled
CFDictionarySetValue(attributes, kSecAttrAccessControl, createAccessControl(true));
@ -466,6 +467,28 @@ bool MacUtils::saveSecret(const QString& key, const QByteArray& secretData) cons
return status == errSecSuccess;
}
bool MacUtils::hasSecret(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, kCFBooleanTrue);
CFDictionarySetValue(query, kSecUseDataProtectionKeychain, kCFBooleanTrue);
CFDictionarySetValue(query, kSecUseAuthenticationUI, kSecUseAuthenticationUIFail);
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching(query, &result);
if (result) {
CFRelease(result);
}
CFRelease(query);
return status == errSecInteractionNotAllowed;
}
bool MacUtils::getSecret(const QString& key, QByteArray& secretData) const
{
const auto keyName = s_touchIdKeyPrefix + key;
@ -503,6 +526,7 @@ bool MacUtils::removeSecret(const QString& key) const
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(query, kSecAttrAccount, static_cast<CFStringRef>(keyName.toNSString()));
CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
CFDictionarySetValue(query, kSecUseDataProtectionKeychain, kCFBooleanTrue);
// TODO: Log failure to delete?
SecItemDelete(query);
CFRelease(query);

View file

@ -71,6 +71,7 @@ public:
// Key management API (TouchID)
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
bool getSecret(const QString& key, QByteArray& secretData) const override;
bool hasSecret(const QString& key) const override;
bool removeSecret(const QString& key) const override;
bool removeAllSecrets() const override;

View file

@ -403,6 +403,12 @@ bool WinUtils::getSecret(const QString& key, QByteArray& secretData) const
return !secretData.isEmpty();
}
bool WinUtils::hasSecret(const QString& key) const
{
QByteArray tmp;
return osUtils->getSecret(dbUuid.toString(), tmp);
}
bool WinUtils::removeSecret(const QString& key) const
{
try {

View file

@ -63,6 +63,7 @@ public:
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
bool getSecret(const QString& key, QByteArray& secretData) const override;
bool hasSecret(const QString& key) const override;
bool removeSecret(const QString& key) const override;
bool removeAllSecrets() const override;

View file

@ -50,8 +50,7 @@ bool TouchID::getKey(const QUuid& dbUuid, QByteArray& key)
bool TouchID::hasKey(const QUuid& dbUuid) const
{
QByteArray tmp;
return osUtils->getSecret(dbUuid.toString(), tmp);
return osUtils->hasSecret(dbUuid.toString());
}
bool TouchID::isAvailable() const