From 5e2916aa6cdd1b7bc466ba11a2fb1779d6a80f9d Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sat, 17 Oct 2026 05:50:26 +0300 Subject: [PATCH] fix(macos): store secrets in the login keychain (#1586) flutter_secure_storage defaults to the data protection keychain on macOS (usesDataProtectionKeychain: true), which needs keychain-access-groups. No entitlements file declares it, and it cannot simply be added: it is restricted, and a Developer ID build carrying it without a provisioning profile does not launch. Reads and writes failed with errSecMissingEntitlement, shown as SecretsStoreUnavailableException. Pass MacOsOptions(usesDataProtectionKeychain: false) so the file-based login keychain is used. Nothing could have been stored in the data protection keychain without the entitlement, so there is nothing to migrate. --- CHANGELOG.md | 1 + lib/core/storage/connection_secrets_store.dart | 15 +++++++++++++-- .../connection_secrets_store_macos_test.dart | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 test/core/storage/connection_secrets_store_macos_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index e8f44a50..15425cd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **macOS: saved passwords use the login keychain** — the secure storage plugin defaults to the data protection keychain, which needs an entitlement no Querya build has, so saving or reading a connection password failed with "Could not read the saved credentials from the system keyring". macOS may ask once to allow Querya Desktop to use its keychain items, and again after an update of an unsigned build (#1586). - **`querya-ext-tester` sends a `--connect` password with `system.injectCredentials`, like the app** — a driver that reads its password only from the injected secret (the ClickHouse driver) failed authentication under the tester because the password was in the `db.connect` payload only. - **Demo Marketplace themes were named `${manifest.name}`** — the generated theme.json now carries the real name (the demo catalog is only used with `--dart-define=QUERYA_MARKETPLACE_MOCK=true`). - **In-app update of a package install (AUR, `.deb`, `.rpm`)** — the updater no longer downloads a zip and quits when the app lives in `/opt/querya-desktop` (owned by root): the update dialog says how to update it (`yay -Syu querya-desktop`, `apt`, `dnf`) with a *Copy update command* button, or offers the release page for any other folder the user cannot write. If an in-place replace fails anyway, the app is started again and the reason is logged to `~/.local/state/querya-update.log`, instead of staying closed. diff --git a/lib/core/storage/connection_secrets_store.dart b/lib/core/storage/connection_secrets_store.dart index 363eb254..7fa31211 100644 --- a/lib/core/storage/connection_secrets_store.dart +++ b/lib/core/storage/connection_secrets_store.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; /// Backend for reading/writing per-connection secrets (password, connection string). @@ -9,8 +10,9 @@ abstract class SecretsStorageBackend { } class _FlutterSecureStorageBackend implements SecretsStorageBackend { - /// Desktop targets use the default platform options from the plugin. - final FlutterSecureStorage _storage = const FlutterSecureStorage(); + final FlutterSecureStorage _storage = const FlutterSecureStorage( + mOptions: ConnectionSecretsStore.macOsOptions, + ); @override Future read(String key) => _storage.read(key: key); @@ -60,6 +62,15 @@ class ConnectionSecretsStore { /// Production uses OS-backed storage; tests override [backend] (see `test/flutter_test_config.dart`). static SecretsStorageBackend backend = _FlutterSecureStorageBackend(); + /// macOS: the file-based login keychain. The plugin's default, the data + /// protection keychain, needs the `keychain-access-groups` entitlement, + /// which none of the builds carry (a restricted entitlement: a Developer ID + /// build with it needs a provisioning profile or does not launch), so every + /// read and write failed with errSecMissingEntitlement (#1586). + @visibleForTesting + static const MacOsOptions macOsOptions = + MacOsOptions(usesDataProtectionKeychain: false); + /// Identifies the current profile's local database (installed / portable / /// migrated-legacy — see `AppDataRoot`), so that two independent profile /// databases never collide in the shared OS keyring even when they each diff --git a/test/core/storage/connection_secrets_store_macos_test.dart b/test/core/storage/connection_secrets_store_macos_test.dart new file mode 100644 index 00000000..e8f88b55 --- /dev/null +++ b/test/core/storage/connection_secrets_store_macos_test.dart @@ -0,0 +1,14 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/storage/connection_secrets_store.dart'; + +void main() { + test('macOS secrets go to the login keychain, not the data protection one ' + '(#1586)', () { + // What the native plugin reads: "true" (its default) needs the + // keychain-access-groups entitlement, which the app does not have. + expect( + ConnectionSecretsStore.macOsOptions.toMap()['usesDataProtectionKeychain'], + 'false', + ); + }); +}