Dart Client for Linux Secret Service
A Dart client implementation for the freedesktop.org Secret Service API
(org.freedesktop.secrets)
for storing and retrieving secrets through a Secret Service provider.
Context
On Linux, XDG is a set of specifications developed by freedesktop.org to improve interoperability and provide shared technologies across desktop environments.
This package communicates with the Secret Service API over D-Bus, making it compatible with any compliant Secret Service implementation (e.g., GNOME Keyring, KDE Wallet).
Tip
This package is focused on the Secret Service API (org.freedesktop.secrets).
The Secret Portal (org.freedesktop.portal.Secret) is a separate API with different semantics and requirements, including client-side encryption. For applications targeting the Secret Portal, see package:xdg_secret_portal_store.
Features
- Pure Dart implementation using D-Bus directly.
- Does not require additional system packages to build or run the application (e.g.,
libsecret-1-0orlibsecret-1-devon Ubuntu). - Uses the standard Secret Service API, which is the primary API used by GNOME Libsecret, making it possible to retain compatibility without data migration (more details).
- Handles prompts, unlocking the default collection (also known as a keyring or wallet) and items when needed.
- Automatically creates the default collection when it does not exist (e.g., on fresh Linux installations).
- Provides convenience strategies for handling duplicate matches during lookup and deletion.
Requirements
These requirements are typically already satisfied by default on most Linux desktop environments.
- A Linux operating system with D-Bus support.
- A running Secret Service implementation (e.g., GNOME Keyring, KDE Wallet or another implementation of the
org.freedesktop.secretsD-Bus service).
Usage
Example:
import 'package:freedesktop_secret/freedesktop_secret.dart';
final client = FreeDesktopSecret();
await client.initialize();
// Replace '<APPLICATION_ID>' with the Linux application ID.
const lookupAttributes = {
'xdg:schema': '<APPLICATION_ID>',
'key': 'refresh_token_example_key',
};
await client.storeSecretText(
attributes: lookupAttributes,
secret: '123',
label: 'User Refresh Token',
replace: true,
);
print('Stored secret');
final secret = await client.lookupSecret(
attributes: lookupAttributes,
duplicateStrategy: LookupSecretDuplicateStrategy.newestCreated,
);
print('Looked up secret: ${secret?.secretAsText()}');
await client.deleteSecret(
attributes: lookupAttributes,
duplicateStrategy: DeleteSecretDuplicateStrategy.deleteAll,
);
print('Deleted the secret');
final int secretCount = await client.countSecrets(
attributes: lookupAttributes,
);
print('Matching secrets: $secretCount');
Tip
Calling client.close() is typically not necessary.
The client can remain open for the application's lifetime and
will be cleaned up automatically when the application exits.
Verified Secret Service implementations
This library is intended to work with Secret Service implementations that comply with the Freedesktop Secret Service specification. The following implementations have been verified through integration testing:
-
GNOME Keyring
- Fedora 44 (GNOME)
- Linux Mint 22 (Cinnamon)
- Pop!_OS 24.04 LTS (COSMIC)
-
KWallet
- CachyOS (KDE Plasma)
Status
This package is feature-complete for its intended scope and has been verified through integration testing against multiple Secret Service implementations.
However, the public API is not yet considered stable. Minor breaking API changes may still occur before the first stable (1.0.0) release as the API is refined based on real-world usage and feedback.
All breaking changes will be documented in CHANGELOG.md.
Lookup attributes
Lookup attributes identify a stored secret and are used to retrieve or delete it later. Services implementing this API will probably store attributes in an unencrypted manner in order to support simple and efficient lookups.
Changing lookup attributes after storing a secret is a breaking change, as the same secret can no longer be retrieved using the new attributes.
xdg:schema attribute (optional)
The xdg:schema attribute is optional and is not part of the Secret Service specification (GNOME Libsecret sets it internally). However, we recommend setting it to the application ID (or another unique, stable identifier) to avoid collisions with other applications and for compatibility with GNOME Libsecret (more details).
const lookupAttributes = {
'xdg:schema': '<APPLICATION_ID>',
// Other attributes here...
};
// Important: Use the same lookup attributes for all operations to access the same secret item.
await client.storeSecretText(attributes: lookupAttributes);
await client.lookupSecret(attributes: lookupAttributes);
await client.deleteSecret(attributes: lookupAttributes);
Lookup duplication strategy
Lookup attributes are not unique constraints. A lookup may return multiple matching items if multiple secrets have the same attributes.
To control how duplicate matches are handled:
await client.lookupSecret(
attributes: ...,
duplicateStrategy: LookupSecretDuplicateStrategy.first,
);
await client.deleteSecret(
attributes: ...,
duplicateStrategy: DeleteSecretDuplicateStrategy.deleteAll,
);
The default is .throwException to avoid silently returning or deleting an unexpected secret.
Tip
According to the GNOME Libsecret documentation, GNOME Libsecret handles duplicate matches by returning the most recently stored item.
Pass .newestCreated to the duplicateStrategy parameter to obtain behavior similar to GNOME Libsecret, rather than relying on the service-defined ordering of .first.
Tip
When storing secrets, use replace: true to update an existing item with the same lookup attributes instead of creating duplicate items. However, duplicates may still occur, so lookup duplication handling is still required.
Prompting
The Secret Service may require user interaction to complete an operation, such as unlocking an item or collection. The specification explicitly recommends that clients do not assume objects are already unlocked (source):
A client application should always be ready to unlock the items for the secrets it needs, or objects it must modify. It must not assume that an item is already unlocked for whatever reason.
This library automatically handles lock checks, unlocking, prompting, waiting for the prompt's Completed signal, checking whether the prompt was dismissed, and processing the operation-specific result.
If a prompt is required, the method awaits until the prompt completes. This happens when the user either dismisses the prompt or completes the required action (for example, entering their password). During this time, a loading or progress indicator can be displayed in the application's UI.
Providing Window ID (optional)
By default, this library passes an empty window ID ("") to the Secret Service. If the application can provide a platform-specific window handle, you may supply it to allow the desktop environment to associate prompts with the application's window (for example, presenting them as a transient dialog).
final client = FreeDesktopSecret(
windowIdProvider: () async => ...,
);
D-Bus session
By default, this library lazily creates a new D-Bus client session (DBusClient.session()) for each FreeDesktopSecret instance.
This behavior can be overridden:
final dbusClient = DBusClient.session();
final client = FreeDesktopSecret(
dbusClientProvider: () => dbusClient,
);
Note
When a non-null dbusClientProvider is provided, FreeDesktopSecret.close() does not call DBusClient.close(). In that case, the caller retains ownership of the DBusClient instance and is responsible for closing it.
This ownership model is consistent with other Linux D-Bus packages, such as avahi and xdg_desktop_portal.
Migration from GNOME Libsecret
Since both Libsecret and this package communicate with the same Secret Service API over D-Bus, compatibility can be retained (i.e., without removing existing user data).
For example, the following schema definition in GNOME Libsecret:
#include <libsecret/secret.h>
static const SecretSchema my_schema = {
"org.example.Example",
SECRET_SCHEMA_NONE,
{
{ "service", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "account", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ NULL, 0 },
}
};
gchar *password = secret_password_lookup_sync(
&my_schema,
NULL,
NULL,
"service", ...,
"account", ...,
NULL
);
/* Full example: https://gnome.pages.gitlab.gnome.org/libsecret/libsecret-c-examples.html#lookup-a-password */
Uses the schema name org.example.Example and defines two string attributes: service and account.
The equivalent lookup attributes in Dart:
import 'package:freedesktop_secret/freedesktop_secret.dart';
final FreeDesktopSecret client = ...;
final secret = await client.lookupSecret(
attributes: {
'xdg:schema': 'org.example.Example',
'service': ...,
'account': ...,
},
);
The xdg:schema attribute corresponds to the Libsecret schema name (Libsecret sets it internally). The other keys correspond to the schema attributes, and their values should match the attributes used when storing the item.
Compatibility with existing packages
Tip
The lookup attributes below reflect internal implementation details of the respective plugins and may change over time.
flutter_secure_storage
Note
There is a pull request that updates flutter_secure_storage_linux to use freedesktop_secret for early testing.
flutter_secure_storage_linux stores all key-value pairs in a single item as JSON.
For example:
final storage = FlutterSecureStorage();
await storage.write(key: 'username', value: 'flutter_user');
In the current flutter_secure_storage_linux implementation, the item can be looked up using:
// Replace <APPLICATION_ID> with the Flutter Linux application ID,
// for example: 'org.example.Example'
final applicationId = '<APPLICATION_ID>';
final lookupAttributes = {
'account': '$applicationId.secureStorage',
// Normally, 'xdg:schema' should be explicitly defined with a
// unique value. However, the current flutter_secure_storage_linux
// implementation stores an incorrect value due to a historical bug.
// It is omitted here to match existing data regardless of the stored
// value. This is not expected to be an issue since 'account' is
// already unique. Changing this would require data migration or a
// breaking change:
// https://github.com/juliansteenbakker/flutter_secure_storage/issues/1181
// 'xdg:schema': ...,
};
final secret = await client.lookupSecret(attributes: lookupAttributes);
if (secret == null) {
print('Secret not found');
return;
}
final secrets = jsonDecode(secret.secretAsText());
print('Found secret: ${secrets['username']}');
print('Deleting all flutter_secure_storage secrets of "$applicationId"');
await client.deleteSecret(attributes: lookupAttributes);
The application ID can be found in linux/CMakeLists.txt:
set(APPLICATION_ID "...")
simple_secure_storage
Note
See also this pull request that updates simple_secure_storage_linux to use freedesktop_secret.
simple_secure_storage_linux uses the schema fr.skyost.SimpleSecureStorage with data attribute:
const SecretSchema* get_schema (void){
static const SecretSchema schema = {
"fr.skyost.SimpleSecureStorage",
SECRET_SCHEMA_NONE,
{
{"data", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"NULL", SECRET_SCHEMA_ATTRIBUTE_STRING},
}
};
return &schema;
}
The equivalent lookup attributes in Dart:
final lookupAttributes = {
'xdg:schema': 'fr.skyost.SimpleSecureStorage',
'data': ...,
};
biometric_storage
biometric_storage uses the schema design.codeux.BiometricStorage with name attribute:
const SecretSchema *
biometric_get_schema (void)
{
static const SecretSchema the_schema = {
"design.codeux.BiometricStorage", SECRET_SCHEMA_NONE,
{
{ "name", SECRET_SCHEMA_ATTRIBUTE_STRING },
// { "NULL", 0 },
}
};
return &the_schema;
}
The equivalent lookup attributes in Dart:
final lookupAttributes = {
'xdg:schema': 'design.codeux.BiometricStorage',
'name': ...,
};
dbus_secrets
dbus_secrets uses a simple key-value lookup and does not use GNOME Libsecret.
The lookup attributes:
final applicationId = '<APPLICATION_ID>';
final lookupAttributes = {
'Application': applicationId,
'Id': '<KEY>',
};
Recommendations
Do not assume previously stored secrets exist
Applications should not assume that a previously stored secret will always exist, even if the application itself is responsible for creating and deleting it.
Instead, they should always be prepared to handle a missing secret. For example, if a refresh token is unavailable, the application should reauthenticate the user, invalidate the session, or log the user out.
Secrets may be removed by the user, another application, the Secret Service implementation, or become inaccessible if the user switches Secret Service implementations (e.g., KDE Wallet to KeePassXC).
Include at least one application-unique attribute
For example:
{
'xdg:schema': '<APPLICATION_ID>',
}
More details in this section.
There are exceptional cases, such as migration from another implementation. In those cases, use the lookup attributes expected by that implementation.
For library authors: Allow overriding FreeDesktopSecret
Prefer explicitly requiring a FreeDesktopSecret instance:
void example({required FreeDesktopSecret freeDesktopSecretClient}) {
// ...
}
Or allow one to be provided while creating a default instance when omitted:
void example({FreeDesktopSecret? freeDesktopSecretClient}) {
final client = freeDesktopSecretClient ?? FreeDesktopSecret();
}
For library authors: do not hardcode the application ID
Hardcoding an application ID or xdg:schema causes all applications using the same library to share the same namespace. This can lead to collisions, unintended access to another application's secrets, or force applications to choose a duplication strategy (see Lookup duplication strategy).
In fact, GNOME libsecret always uses a schema in normal usage.
Consider this schema:
{
'xdg:schema': linuxApplicationId(), // e.g., via FFI or method channel
'package': 'flutter_secure_storage', // Example package name
}
This approach allows a library to identify its own secrets within each application, while keeping different applications isolated from one another. It also makes it straightforward for application developers to migrate or access the library's secrets when needed, without risking collisions between unrelated applications or packages.
Later, package authors can use this lookup to retrieve only the secrets created by their package:
{
'xdg:schema': linuxApplicationId(),
'package': 'flutter_secure_storage',
}
Application developers can use this lookup to retrieve all secrets belonging to their application, regardless of the package:
{
'xdg:schema': linuxApplicationId(),
}
Tip
The package linux_application_id provides synchronous access to the Linux application ID using Dart FFI.
Handle thrown Exceptions
When performing operations such as storing or retrieving secrets, prefer catching Exception.
Although many failures seem uncommon in practice, catching Exception ensures that both D-Bus exceptions (such as DBusErrorException and DBusMethodResponseException) and library-specific exceptions derived from SecretServiceException are handled.
try {
// Operations here...
} on Exception catch (e) {
...
}
When writing a library, consider avoiding patterns like this:
// Avoid this pattern when possible.
try {
// Operations here...
print('Succeeds');
return true;
} catch (e) {
print('An error occurs: $e');
return false;
}
This pattern has several limitations:
- Catches programming errors that should typically be fixed rather than handled at runtime (for example,
ArgumentError,StateError). There is a Dart lint to avoid this. - Hardcodes a logging strategy, making it inconsistent with the application's chosen logging framework or telemetry.
- Prevents applications from handling exceptions explicitly. For example, an application may want to display a specific message when a
SecretServicePromptDismissedExceptionis thrown, or recover when no Secret Service implementation or D-Bus session is available. - Silently converts failures into a return value that hides the original exception, stack trace, and other diagnostic information. This may prevent them from being reported by crash reporting tools such as Sentry if the exception is not handled.
- Applications can still map exceptions to
Resultfailures according to their own error-handling requirements.
- Applications can still map exceptions to
Tip
This recommendation is not specific to freedesktop_secret. The same guidance generally applies when using libraries such as dart:io, package:flutter_secure_storage, and many other Dart and Flutter packages.
Secret Service security model
The Secret Service specification does not define per-application access control. In practice, Secret Service implementations typically do not isolate applications running as the same user. This applies to GNOME Keyring and KWallet regardless of the client library used (such as GNOME libsecret or freedesktop_secret).
See also: this discussion.
Provides:
- Encrypted storage of secrets at rest.
- Protection against other Linux users accessing the user's secrets.
- Protection against offline filesystem access (for example, copied disks or Windows dual-boot access).
- Protection against physical device theft, provided the keyring remains protected.
Does not provide:
- Per-application access control defined by the Secret Service specification.
- Protection against malicious software running as the same user.
Portal security model
The portal org.freedesktop.portal.Secret is designed to provide a degree of isolation between sandboxed applications.
It provides a master secret for a sandboxed application, which the application can use to encrypt and decrypt its own secrets stored on disk.
In a typical backend implementation, the master secret is stored in the user's keyring under the application ID (reference).
- Unsandboxed applications can still retrieve the master secret from the user's keyring and decrypt the secrets of sandboxed applications.
- Sandboxed applications cannot retrieve the master secret of other applications unless they have D-Bus access to
org.freedesktop.secrets. Even then, they would also need filesystem access to other Flatpak applications' data.- Older Flathub submissions commonly requested
org.freedesktop.secretsD-Bus access (example). Newer submissions are generally encouraged to use the portal instead.
- Older Flathub submissions commonly requested
Inform users about malicious or untrusted software
Applications that store sensitive data should consider informing users about the risks of installing malicious or untrusted software. This recommendation applies regardless of whether the application is sandboxed or whether it uses the Secret portal.
Limitations
Supports only plain algorithm for transferring secrets
When transferring secrets between the client and the Secret Service implementation, this client currently only supports Algorithm: plain.
This does not mean that secrets are stored on disk without encryption. It only refers to the algorithm used for transferring secret values between the client and the service.
The specification strongly recommends that service implementations (e.g., GNOME Keyring, KDE Wallet) support the plain algorithm:
It is strongly recommended that a service implementing this API support the plain algorithm.
The dh-ietf1024-sha256-aes128-cbc-pkcs7 algorithm provides an additional layer for transferring secrets, but is not intended as a required security measure. According to the specification:
The Secrets API has provision to encrypt secrets while in transit between the service and the client application. The encryption is not envisioned to withstand man in the middle attacks, or other active attacks. It is envisioned to minimize storage of plain text secrets in memory and prevent plain text storage of secrets in a swap file or other caching mechanism.
Many client applications may choose not to make use of the provisions to encrypt secrets in transit. In fact for applications unable to prevent their own memory from being paged to disk (eg: Java, C# or Python apps), transferring encrypted secrets would be an exercise of questionable value.
Dart applications generally fall into the same category due to managed memory and garbage collection, making transfer-only encryption less beneficial than in environments with more direct memory control (e.g., C, Rust).
This limitation may be addressed in the future, but it is not currently planned.
GNOME Libsecret seems to support both plain and dh-ietf1024-sha256-aes128-cbc-pkcs7 (source code).
Default collection usage
According to the specification:
A group of items together form a collection. A collection is similar in concept to the terms 'keyring' or 'wallet'.
The specification suggests that clients should use the default collection unless there are special requirements:
Client applications without special requirements should store in the default collection. The default collection is always accessible through a specific object path.
The library uses the default collection by default. While individual operations can still override the collection object path (from raw dbus), the library currently does not expose or maintain convenient APIs for creating or managing collections.
For most Flutter applications, this limitation is not relevant.
Most Flutter plugins and packages store secrets in the default collection as well (SECRET_COLLECTION_DEFAULT in GNOME Libsecret):
Partial Freedesktop Secret Service API coverage
This package does not expose the entire Freedesktop Secret Service API (that is, it is not a one-to-one mapping of the D-Bus interface). Instead, it focuses on the functionality commonly required by Flutter applications.
For example, it does not provide an option for the application to programmatically dismiss a service prompt dialog after it has been shown as part of an operation. The user must explicitly confirm or dismiss the prompt themselves.
For many Flutter applications, this limitation is unlikely to be relevant.
Related packages
xdg_secret_portal_storefor sandboxed applications (e.g., Flatpak, Snap) without direct D-Bus access toorg.freedesktop.secrets.
Known secret-tool lookup CLI issue (GNOME Libsecret)
FreeDesktopSecret.storeSecretText() stores text secrets with the content type text/plain; charset=utf-8:
await client.storeSecretText(secret: '...');
This content type is permitted by the Secret Service specification, which even uses text/plain; charset=utf8 as an example.
secret-tool search (from GNOME Libsecret) displays these secrets correctly. However, secret-tool lookup rejects the secret with the message secret does not contain a textual password because it expects the content type to be exactly text/plain.
This is a known GNOME Libsecret issue. At the time of writing, there is an open merge request to address it.
Acknowledgements
package:dbus(Dart package)org.freedesktop.Secrets.xmlD-Bus introspection data was copied from the GNOME Libsecret project source code (accompanying COPYING file).- Freedesktop organization.