From 5b000cec769b5163285f728553c2fa6d1225639d Mon Sep 17 00:00:00 2001 From: Ellet Date: Sun, 13 Sep 2026 02:51:22 +0300 Subject: [PATCH] feat(linux): add simple_secure_storage_linux_portal package for XDG Desktop Secret Portal support Fixes https://github.com/Skyost/SimpleSecureStorage/issues/16 --- .../CHANGELOG.md | 3 + .../LICENSE | 21 +++ .../README.md | 62 +++++++++ .../simple_secure_storage_linux_portal.dart | 127 ++++++++++++++++++ .../pubspec.yaml | 20 +++ pubspec.yaml | 1 + 6 files changed, 234 insertions(+) create mode 100644 packages/simple_secure_storage_linux_portal/CHANGELOG.md create mode 100644 packages/simple_secure_storage_linux_portal/LICENSE create mode 100644 packages/simple_secure_storage_linux_portal/README.md create mode 100644 packages/simple_secure_storage_linux_portal/lib/simple_secure_storage_linux_portal.dart create mode 100644 packages/simple_secure_storage_linux_portal/pubspec.yaml diff --git a/packages/simple_secure_storage_linux_portal/CHANGELOG.md b/packages/simple_secure_storage_linux_portal/CHANGELOG.md new file mode 100644 index 0000000..d3a5c15 --- /dev/null +++ b/packages/simple_secure_storage_linux_portal/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +- **FIRST PUBLIC RELEASE**. diff --git a/packages/simple_secure_storage_linux_portal/LICENSE b/packages/simple_secure_storage_linux_portal/LICENSE new file mode 100644 index 0000000..82c3640 --- /dev/null +++ b/packages/simple_secure_storage_linux_portal/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Hugo DELAUNAY "Skyost" + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/simple_secure_storage_linux_portal/README.md b/packages/simple_secure_storage_linux_portal/README.md new file mode 100644 index 0000000..54b34fb --- /dev/null +++ b/packages/simple_secure_storage_linux_portal/README.md @@ -0,0 +1,62 @@ +A Linux implementation of [`simple_secure_storage`](https://pub.dev/packages/simple_secure_storage) using the XDG Desktop [Secret Portal API](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html) (`org.freedesktop.portal.Secret`). + +## Usage + +Register this implementation when the application should use the XDG Desktop +Secret Portal API (`org.freedesktop.portal.Secret`), such as when running in a +sandboxed environment (e.g., Flatpak or Snap). + +```dart +import 'package:simple_secure_storage_linux_portal/simple_secure_storage_linux_portal.dart'; + +SimpleSecureStorageLinuxPortal.registerWith(); +``` + +> [!IMPORTANT] +> `SimpleSecureStorageLinuxPortal.registerWith()` must be explicitly called. Simply adding the package as a dependency is not sufficient. + +## Example + +The following example registers this implementation when running in Flatpak. + +```dart +import 'dart:io'; + +import 'package:simple_secure_storage_linux_portal/simple_secure_storage_linux_portal.dart'; + +if (Platform.isLinux) { + final isFlatpak = + Platform.environment.containsKey('FLATPAK_ID') || + Platform.environment['container'] == 'flatpak'; + + if (isFlatpak) { + SimpleSecureStorageLinuxPortal.registerWith(); + } +} +``` + +> [!TIP] +> This is only an example of when to register the implementation. Applications +may choose different conditions based on their environment or requirements. + +## Implementation Details + +The Secret Portal API [provides a unique master secret](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html#org-freedesktop-portal-secret-retrievesecret) for a sandboxed application. + +Unlike the Secret Service API, it does not provide secure storage itself. Instead, applications can use the master secret to encrypt secrets and store them in a file, for example. + +### File Path + +The secrets are stored encrypted in [a file](https://pub.dev/packages/xdg_secret_portal_store#storage-format): + +`$XDG_DATA_HOME/$APPLICATION_ID/secure_storage/secrets.json`. + +### Cryptography + +For [security details](https://pub.dev/packages/xdg_secret_portal_store_default#cryptography). + +### Not interoperable with GNOME libsecret + +This implementation cannot retrieve secrets stored by [GNOME libsecret](https://gitlab.gnome.org/GNOME/libsecret). + +For more details, refer to [this section](https://pub.dev/packages/xdg_secret_portal_store#not-interoperable-with-gnome-libsecret). diff --git a/packages/simple_secure_storage_linux_portal/lib/simple_secure_storage_linux_portal.dart b/packages/simple_secure_storage_linux_portal/lib/simple_secure_storage_linux_portal.dart new file mode 100644 index 0000000..13826a3 --- /dev/null +++ b/packages/simple_secure_storage_linux_portal/lib/simple_secure_storage_linux_portal.dart @@ -0,0 +1,127 @@ +import 'dart:io'; + +import 'package:simple_secure_storage_platform_interface/simple_secure_storage_platform_interface.dart'; +import 'package:linux_application_id/linux_application_id.dart'; +import 'package:xdg_desktop_portal/xdg_desktop_portal.dart'; +import 'package:xdg_directories/xdg_directories.dart' as xdg_directories; +import 'package:xdg_secret_portal_store/xdg_secret_portal_store.dart'; +import 'package:xdg_secret_portal_store_default/xdg_secret_portal_store_default.dart'; + +typedef _StorageMap = Map; + +/// A Linux implementation of [SimpleSecureStoragePlatform] using the +/// XDG Desktop Portal Secret API ([`org.freedesktop.portal.Secret`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html)). +/// +/// [SimpleSecureStorageLinuxPortal.registerWith] must be explicitly called. +/// Simply adding the package as a dependency is not sufficient. +class SimpleSecureStorageLinuxPortal extends SimpleSecureStoragePlatform { + /// Registers this class as the default instance of [SimpleSecureStoragePlatform]. + static void registerWith() => + SimpleSecureStoragePlatform.instance = SimpleSecureStorageLinuxPortal(); + + /// A helper for storing application secrets in an encrypted file using the + /// master secret provided by the XDG Desktop Portal Secret API. + XdgSecretPortalStore? _store; + XdgSecretPortalStore get _requireStore => + _store ?? + (throw StateError( + 'SimpleSecureStorage must be initialized before accessing storage.', + )); + + /// Overrides the Linux application ID. + /// + /// If null, the application ID of the running GLib `GApplication` is used. + /// + /// The application ID is used to determine the directory where the encrypted + /// secret store is persisted. + String? applicationIdOverride; + String get _applicationId => + applicationIdOverride ?? + linuxApplicationId() ?? + (throw UnsupportedError( + 'No Linux application ID is available. This must be called from a running Flutter Linux application.', + )); + + /// Optional prefix applied to all storage keys. + /// + /// Loaded from [InitializationOptions.prefix] during [initialize] and used to + /// namespace keys within the underlying storage map. + String? _prefix; + String _applyPrefix(String key) => '${_prefix ?? ''}$key'; + + Future<_StorageMap> _readStorageMap() => _requireStore.read(); + Future _writeStorageMap(_StorageMap map) => _requireStore.write(map); + + @override + Future initialize(InitializationOptions options) async { + _prefix = options.prefix; + + final client = XdgDesktopPortalClient(); + + final store = XdgSecretPortalStore( + masterSecretRetriever: client.secret.retrieveSecret, + persistence: SecretStorePersistenceFile( + File( + '${xdg_directories.dataHome.path}/$_applicationId/secure_storage/secrets.json', + ), + ), + crypto: SecretStoreCryptoDefault(), + ); + + await store.loadMasterSecret(); + await client.close(); + + _store = store; + } + + /// Clears all values. + @override + Future clear() => _writeStorageMap({}); + + /// Deletes the value associated to the given [key]. + @override + Future delete(String key) async { + final map = await _readStorageMap(); + map.remove(_applyPrefix(key)); + + await _writeStorageMap(map); + } + + /// Returns whether the secure storage has the given [key]. + @override + Future has(String key) async { + final map = await _readStorageMap(); + return map.containsKey(_applyPrefix(key)); + } + + /// Lists all key/value pairs. + @override + Future> list() async { + final map = await _readStorageMap(); + final prefix = _prefix; + + if (prefix == null || prefix.isEmpty) { + return map; + } + + return map.map( + (key, value) => MapEntry(key.substring(prefix.length), value), + ); + } + + /// Returns the value of the given [key]. + @override + Future read(String key) async { + final map = await _readStorageMap(); + return map[_applyPrefix(key)]; + } + + /// Writes the [value] so that it corresponds to the [key]. + @override + Future write(String key, String value) async { + final map = await _readStorageMap(); + map[_applyPrefix(key)] = value; + + await _writeStorageMap(map); + } +} diff --git a/packages/simple_secure_storage_linux_portal/pubspec.yaml b/packages/simple_secure_storage_linux_portal/pubspec.yaml new file mode 100644 index 0000000..0ac4c3c --- /dev/null +++ b/packages/simple_secure_storage_linux_portal/pubspec.yaml @@ -0,0 +1,20 @@ +name: simple_secure_storage_linux_portal +description: A Linux implementation of the simple_secure_storage plugin using the XDG Desktop Portal Secret API (org.freedesktop.portal.Secret) +homepage: https://github.com/Skyost/SimpleSecureStorage +repository: https://github.com/Skyost/SimpleSecureStorage/tree/main/packages/simple_secure_storage_linux_portal +issue_tracker: https://github.com/Skyost/SimpleSecureStorage/issues +version: 0.1.0 +resolution: workspace +topics: [os-integration, xdg-portal, secure-storage, simple-secure-storage] + +environment: + sdk: '>=3.12.0 <4.0.0' + +dependencies: + simple_secure_storage_platform_interface: ^0.2.3 + xdg_secret_portal_store: ^0.2.0 + # TODO: Remove xdg_secret_portal_store_default and implement xdg_secret_portal_store using package:cipherlib (https://github.com/Skyost/SimpleSecureStorage/issues/16) + xdg_secret_portal_store_default: ^0.1.1 + xdg_desktop_portal: ^0.1.13 + xdg_directories: ^1.1.0 + linux_application_id: ^1.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 2fe2113..fa90046 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ workspace: - packages/simple_secure_storage_android - packages/simple_secure_storage_darwin - packages/simple_secure_storage_linux + - packages/simple_secure_storage_linux_portal - packages/simple_secure_storage_platform_interface - packages/simple_secure_storage_web - packages/simple_secure_storage_windows