Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/simple_secure_storage_linux_portal/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.1.0

- **FIRST PUBLIC RELEASE**.
21 changes: 21 additions & 0 deletions packages/simple_secure_storage_linux_portal/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 62 additions & 0 deletions packages/simple_secure_storage_linux_portal/README.md
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -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<String, String>;

/// 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<void> _writeStorageMap(_StorageMap map) => _requireStore.write(map);

@override
Future<void> 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<void> clear() => _writeStorageMap({});

/// Deletes the value associated to the given [key].
@override
Future<void> 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<bool> has(String key) async {
final map = await _readStorageMap();
return map.containsKey(_applyPrefix(key));
}

/// Lists all key/value pairs.
@override
Future<Map<String, String>> 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<String?> read(String key) async {
final map = await _readStorageMap();
return map[_applyPrefix(key)];
}

/// Writes the [value] so that it corresponds to the [key].
@override
Future<void> write(String key, String value) async {
final map = await _readStorageMap();
map[_applyPrefix(key)] = value;

await _writeStorageMap(map);
}
}
20 changes: 20 additions & 0 deletions packages/simple_secure_storage_linux_portal/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down