-
-
Notifications
You must be signed in to change notification settings - Fork 64
Keyring Core
This is an in-depth document about the keyring-core crate, version 0.7 and above. It’s meant for programmers who have read the Keyring introduction document, which defines many of the terms used here. This document provides a lot of background information about API design and usage beyond that found in the API docs. It should be helpful to read whether you are developing a client application that uses the keyring API or a credential store module for a keyring-based app.
The keyring-core module provides two API layers for developers:
- The client layer, for writing programs that use keyring to store secrets for users.
- The credential store layer, for writing credential-store modules.
In addition, keyring-core provides two testing-only credential-store modules:
- The mock module, which allows clients to mock errors as well as success on calls.
- The sample module, which provides a cross-platform, (optionally) persistent store for client development and a code template for credential-store development.
Neither of the credential stores provided by keyring-core are either secure or robust, and neither should be used in production applications.
The client layer of the keyring core is organized around the Entry object, a concrete object that represents a named secret. Clients create an entry by providing a service and user name to identify the entry, and then operations on the entry are used to store, retrieve, and forget the the entry’s secret. Here is a complete sample program, albeit one that does absolutely nothing useful:
use keyring_core::{set_default_store, mock, Entry, Result};
fn main() -> Result<()> {
set_default_store(mock::Store::new()?);
let entry = Entry::new("my-service", "my-name")?;
entry.set_password("topS3cr3tP4$$w0rd")?;
let password = entry.get_password()?;
println!("My password is '{password}'");
entry.delete_credential()?;
unset_default_store();
Ok(())
}Most keyring-compatible applications can do everything they need with the calls demonstrated here, which work consistently across every keyring-compatible credential store. The rest of the client layer is for use by keyring applications that either need detailed control over specific credential stores, or that interoperate with non-keyring applications that use those stores.
The original, very-simple design for the v1 keyring client API made the following assumptions:
- The only kind of secret being stored in the keyring was a human-readable string.
- There was only one credential store available on each platform, and only the keyring code directly accessed that store.
- No other programs would be accessing (or creating) the credentials that were being used by keyring.
With these assumptions, the Entry::new API was all applications needed for entry creation, there was a 1-1 relationship between entries with passwords and credentials in the store, and keyring client programs never needed to know which credential store was in use. It also meant, however, that a keyring client application could never share passwords with a third-party application unless the storage conventions used by that other application exactly matched those used by keyring.
As the number of keyring clients grew, more and more of them started to ask for better interoperability with third-party applications. These requests led to many API extensions in the v2 and v3 keyring APIs:
-
Entry::new_with_target, which allowed limited configurability of the mapping from entry to credential. - The
CredentialApiandCredentialStoreApitraits, and their associatedCredentialandCredentialStoretypes, which allowed the introduction of new credential stores. -
Entry::new_with_credential, which allowed entries to manage an existing credential in the store without knowing its correspondingserviceoruservalues (if any). - The
Ambiguouserror, which allowed discovery of conflicting credentials written by non-keyring clients.
With the splitting of keyring into two crates—the API crate keyring-core and the application crate keyring—it’s now possible for client applications to control exactly which credential store(s) they use, regardless of OS platform, and also for them to fully interoperate with other clients of those stores. Which does not mean that clients today have to be more complicated than the clients of keyring v1 were, or that they have to interoperate with other clients. For the vast majority of client applications—those for whom the original assumptions above still hold—the simple code they would have written for keyring v1 will still work today.
There are two sources of Entry objects:
- Those created by the client using the
newandnew_with_optionsAPIs. These entries are called specifiers, because the client specifies aserviceanduser. - Those created by the credential store and returned from
get_credential,search_for_credentials, or anAmbiguouserror. These entries are called wrappers, because each wraps an existing credential in the underlying store.
Both specifiers and wrappers represent credentials, but in different ways:
- A specifier is what mathematicians call an intensional representation: it provides a specification that is resolved by the credential store at time of use, so the identity of the credential represented may vary over time.
- A wrapper is what mathematicians call an extensional representation: it holds a credential identity and always refers to the credential with that identity.
There is a natural duality between intensional and extensional representations, and there are calls on entries which reflect this:
- If a client wants to know which specific credential an entry refers to at a specific time, they can call
get_credentialto obtain a wrapper around that credential. - If a client wants to know the
serviceanduservalues that are associated with an entry, they can callget_specifiers. Not every wrapper will have specifiers, as not every credential in a store (e.g., those created by third parties) can be specified by keyring.
Keyring client applications, by and large, deal exclusively with specifiers. They know their user’s identity, and they know what service their user is accessing, so they construct a specifier entry with that information and set or retrieve the password for that entry, relying on the credential store to create a new credential or find an existing one that matches that specification. It is only keyring clients that have to interoperate with non-keyring clients, or who are doing migration of legacy data left by an earlier version of keyring, that make use of wrappers.
Many credential stores, in addition to storing secrets in credentials, allow them to be decorated with additional information called attributes. The keyring client API exposes this capability in a cross-platform way by providing two calls:
-
get_attributesreturns key-value string pairs that the store can use to expose decorations on the underlying credential. -
update_attributesasks the store to update any existing decorations with those provided by the client.
Stores often use the get_attributes call to expose attributes that cannot be updated, such as the store-specific identity of a credential. Consult the documentation of each credential store provider to understand what attributes are exposed and which can be updated by clients.
Although the keyring API names credentials using just service and user strings, most credential stores don’t. Thus, each credential store provider must map the service and user strings onto the native credential store’s identification strings and/or credential attributes when looking for existing credentials or creating new ones.
As long the only entity writing credentials into the store is the keyring client, it doesn’t much matter what conventions the credential store provider uses for identifying credentials, because there’s no chance of confusion with those created by another client. But the minute you have other clients writing into the credential store, the potential for ambiguity arises, especially if those other clients are trying to interoperate with the keyring provider.
For example, let’s look at the Secret Service, a popular credential store on Linux, whose credentials are called items. The Secret Service has a unique identifier for each item, but that identifier is generated by the Secret Service and cannot be altered by clients such as the keyring credential store provider, so it can’t be used to encode service and user values. Instead, the Secret Service offers the ability to annotate items with arbitrary key/value attributes, and to search for items by those attributes. So let’s suppose our credential store provider puts the user and service values on its items as attributes. Next, let’s suppose that there’s a Python client that does the same thing (in fact there is, and it’s also called keyring 😜—it was the module that inspired the creation of Rust’s keyring). So far, there’s no problem: the two providers (one Rust, one Python) are working exactly the same way, and each will recognize items created by the other properly.
But now let’s suppose that the two providers, although they agree that user and service names are case-insensitive, implement that requirement in different ways: the Python provider lowercases each entry before storing it and does a case-sensitive search for the store attributes, while the Rust provider leaves the case of entries alone and does a case-insensitive search for the store attributes. Then consider this scenario:
- A user on the Rust side sets the password for an entry for service
Fooand userBar. This writes an item with attributesFooandBarinto the store. - A user on the Python side tries to read the password for service
Foo, userBar. The provider looks for an item with attributesfooandbarinto the store but doesn’t find one. So the user sets the password again, creating a new item with attributesfooandbar. - Back on the Rust side, the original user goes to get the password for service
Fooand userBar. The provider does its case-insensitive search, and discovers two matching entries.
Holy Hubbel Telescope, Batman! This scenario has actually occured in the field, although it was due to client disagreement about conventions, rather than credential store provider disagreement.
While there is a requirement that all the entries for a given <service, user> pair match the same credential, there is no requirement that an entry for a given <service, user> pair can only match one credential. When a credential store provider finds two or more credentials in a store that match a given Entry, we say that the Entry in such cases is ambiguous, and any operation on such an entry will produce an Ambiguous error whose value is a list of wrapper entries, one for each matching credential. Clients can work with these entries in one of two ways:
- They can use
Entrycalls to get or set secrets, get or update attributes, or delete an ambiguous credential from the store. This approach can be used regardless of the underlying store. - They can downcast the
Entryto the appropriate concrete credential type for the underlying store, and then use calls on that type to do credential management. This allows very fine-grained control of the ambiguous credentials, but requires that the client know the credential store and its API.
There is an example program in the keyring-core crate that demonstrates these two approaches over the sample store.
Not all credential stores allow ambiguity. But even if a store does allow ambiguity, a call made on a wrapper entry will never produce an Ambiguous result. Only calls on specifiers can produce an Ambiguous error.
Many stores, especially those that allow ambiguity, support searching for existing credentials. These searches, made using the Entry::search function, return a separate entry for each of the credentials in the store that match the search spec. Since the returned entries are wrappers, they each denote a specific credential, and operations on them will never return an Ambiguous error.
The Entry::search function takes a search spec consisting of key-value string pairs. The interpretation of the spec, as well as the allowed keys, are store-specific, so consult the documentation for your credential store(s) to determine whether they support search and, if so, what their search specs allow.
Before a keyring client can start using entries, it must instantiate a credential store provided by a credential-store provider. As demonstrated in the example program above, this is typically done at application startup by instantiating a store and setting it as the default credential store. Then, at application shutdown, the default credential store is “unset” so that its resources can be properly released.
So how do you instantiate and use a credential store? As follows:
- Find a credential store provider for each platform your client supports. Most credential store providers are platform-specific, so this may require finding more than one store.
- Read the documentation for each store to find out how to instantiate it. (By convention, this is to call
Store::neworStore::new_with_configurationin the module containing the store.) - If you are only using one store (per platform), which is typical, hand the store to
set_default_storeas soon as you instantiate it. This will allow you to useEntrycalls directly to create entries. - If you are using multiple stores (per platform), which is rare but useful for migrations, you can ignore
set_default_storeand build your entries in the store directly using its entry builder (CredentialStore::build) directly.
The keyring application provides a lot of sample code for instantiating credential stores and releasing them when done. Credential store providers are encouraged to update the keyring application to use their store so that prospective clients can discover their store and see how to instantiate it.
The credential store layer of the keyring core is a service provider interface: it defines two types—Credential, and CredentialStore—that are implemented by concrete objects provided by a credential store module.
-
Credential. EachEntryobject in the client layer contains a concrete object that implements theCredentialtype. This trait has all the same calls that are made available to clients on anEntry, and anEntryindirects all these calls to its containedCredential. -
CredentialStore. Credential stores are the factories that produce entries at the request of a client, either for a specified service and user or by wrapping an existing credential found in the store.
In addition to providing objects that implement these two traits, credential store modules must provide a way for clients to obtain a CredentialStore object. It is recommended that credential store modules use the following convention:
- Use
Store(in some module path) as the name of the type that implementsCredentialStore. - There should be a function of no arguments
Store::new()that instantiates aStoreobject with a default configuration. - If the store supports configurations other than default, there should be a function
Store::new_with_configuration(&HashMap<&str, &str>)that instantiates a custom-configuredStoreobject.
This convention makes it very easy for any client to use any store.
The Credential type is actually a Sync and Store specialization of the CredentialApi trait. The CredentialApi trait defines essentially the same calls as are available on the Entry object, so that each entry can pass those calls on to its inner Credential. But implementors of the CredentialApi don’t have to define every one of these calls, because some of them have default implementations:
-
set_passwordis defined in terms ofset_secret. -
get_passwordis defined in terms ofget_secret. -
get_attributesneed only be defined if the store supports attributes. -
update_attributesneed only be defined if the store supports updating attributes.
In the description of the client API, there is a section distinguishing specifier entries from wrapper entries. Because all of the functionality of entries actually comes from their contained Credential objects, it is the concrete objects that implement Credential that have to implement the intensional vs. extensional distinction described. In particular:
- Specifier
Credentialobjects know how to resolve specifiers to existing credentials. In addition, they are the objects that implement the modifiers used on the specification when the entry was created. - Wrapper
Credentialobjects can hold the identification information for any existing credential, and are able to figure out whether there are specifiers that refer to it.
Most credential store providers use a single concrete type to implement both specifiers and wrappers, but this is not a requirement. Because the context in which credential stores produce an entry determines whether it’s a specifier or wrapper, the store can use different concrete types for the two different kinds of credential.
There is one subtle difference between the API on entries and that on credentials, and it’s related to the specifier vs. wrapper distinction: the get_credential call. The Entry version of this call returns an Entry, but the Credential version returns an optional entry. That’s because the implementation of a wrapper may be a singleton, so that the entry returned from a get_credential call on that instance must be an entry that wraps the exact same instance. In such a case, the wrapper can return None and the implementation on Entry will clone the Arc holding the existing wrapper and put that clone in the new entry.
It’s highly recommended that credential store providers implement or derive the std::fmt::Debug trait on the concrete types they use for credentials, so that clients can easily view the non-sensitive parts of the entries/credentials they are using. In addition, providers should also define the debug_fmt call as part of their CredentialApi implementation, so that debug print calls on the Credential interface objects turn into debug print calls on the concrete objects. Using this definition (taken from the sample store) will work:
fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}because it will pass along the debug_fmt call to the concrete instance.
Credential stores are ulitmately responsible for creating all the Credential objects that are contained by entries. While the code for how this is done may be split between a CredentialStore object and its created Credential objects, so that (for example) a specifier Credential object may not invoke the CredentialStore object directly when creating a wrapper Credential object, the logic used should be thought of as belonging to the credential store overall.
The most-used entry on every credential store is build: the one used by the client level to create specifier entries. The arguments to build are the service and user provided by the client and an optional modifiers hash table of key/value strings. The names and meanings of modifiers are specific to each store, and there is no requirement that a store accept any modifiers. The Credential object inside the entry produced by build must be a specifier.
Not all credential stores support searching for entries; those that do define the search method called by Entry::search. The Credential objects produced by search must be wrappers.
By convention, the concrete object type that is used to implement a credential store is called Store, and supports at least the Store::new() call to produce a credential store that clients can use. If the credential store provider allows configuration of the store, they are free to offer more calls on the Store type to produce them, and by convention they should offer a Store::new_with_configuration call that offers full configurability.
If a credential store supports store-specific modification of target entries, especially if the modification is useful for multiple entries, the store provider is strongly encouraged to provide a store configuration that builds that modification in to all entries. For example, on macOS/iOS, the native credential store supports a modification that requires biometric authentication whenever a credential is accessed. If a client is going to want that modification on one credential, they may well use it on many. If the store provider allows configuring the store so that all entries have that modification by default, then the client application can simply configure the store once at startup rather than modifying each entry as it’s created. Since store-creation code is always store-specific, but entry creation code need not be, this allows the client app to configure all their store-specific code in one place rather than sprinkling it throughout their application.
Separately-configured store instances from the same provider may well use the same underlying credential store, often with slightly different conventions for mapping entries to underlying credentials. It may even be the case that separately configured instances can access the same credentials. In the macOS/iOS example given above, for instance, whether a credential requires biometic authorization is hidden by the OS from the client application, so that a password written from a store instance that always applies the biometric modifier can be read by a store instance that doesn’t apply that modification, and the OS will prompt the user for authentication when that access is made.