Skip to content

Keyring — Secrets on the Client

Overview

A client that remembers a secret, such as the tokens of a single sign-on or a password, needs a place to keep it. The keyring API in tentackle-core (package org.tentackle.keyring) gives it one, with a single, platform-agnostic API:

Keyring keyring = Keyring.getInstance();
SecretId id = new SecretId("com.example.myapp/google", clientId);

keyring.store(id, "MyApp Google sign-in", token);   // insert or replace
char[] token = keyring.load(id);                    // null if there is none
keyring.remove(id);

Out of the box, the secrets are kept in the Java user preferences, encrypted with the application's Cryptor, or only in memory if there is no cryptor. That works everywhere and needs nothing, but it does not protect a secret.

The real protection is the credential store that every desktop OS has built in. It binds the secrets to the logged-in user and encrypts them with a key that the user's login unlocks. The tentackle-keyring module adds it. The module is opt-in: add it to the application's dependencies, and it replaces the default keyring via @Service. No code changes. Without it, the application runs as before.

The API

Type Purpose
Keyring store, load, remove, plus isSecure() and isPersistent().
SecretId The identity of a secret: a service and an account.
KeyringFactory Selects the keyring once; replaceable via @Service(KeyringFactory.class).
KeyringException The store could not be accessed. A missing secret is not an exception.
AbstractKeyring Base class of the keyrings: argument checks, locking, the availability probe.

Every credential store organizes its entries by service and account, and so does SecretId. The service says what the secret is for, typically the application plus a purpose ("com.example.myapp/google"). The account says whose it is within that service, for example the user or an OAuth client id. The optional label is what the OS tools show the user: Keychain Access, Credential Manager, Seahorse, KWalletManager. Without a label, it is derived from the id as service (account).

Secrets are char[], never String, so they can be wiped. The keyring never keeps a reference to an array that is passed in, and it always hands out a fresh array, which the caller should wipe when done. Internally, all intermediate buffers are wiped before they are released.

SecretsHelper has the tools for this: wipe(char[]) and wipe(byte[]) clear an array (null-safe), and encode(char[]) / decode(byte[]) convert a secret to and from UTF-8 bytes without the detour through a String, which could not be wiped. The arrays they return are to be wiped by the caller as well. A secret must not be empty and must not contain NUL characters, because some of the native APIs take C strings.

All keyrings are thread-safe.

The keyrings

Name Module Store isSecure isPersistent
preferences tentackle-core Java user preferences, encrypted with the Cryptor no yes
memory tentackle-core The JVM's heap yes no
macos tentackle-keyring Login keychain yes yes
windows tentackle-keyring Credential Manager yes yes
libsecret tentackle-keyring freedesktop.org Secret Service yes yes

The preferences keyring keeps the secrets in the node /org/tentackle/keyring, each under a hash of its id. The Java preferences are local to the machine and user, unlike Tentackle's database-backed preferences, which roam with the user and are therefore the wrong place for a secret bound to one device. The encryption keeps the secrets from being picked up by simply looking at the file. It is obfuscation, not protection: whoever has the file and the application, which carries the cryptor's key, has the secrets. That is why the keyring reports isSecure() == false.

Selection

The keyring is picked once, on first use, by the KeyringFactory:

  1. the keyring named by the system property tentackle.keyring, if set. Forcing a keyring that is not usable is an error, never a silent fallback;
  2. the credential store of the OS, if tentackle-keyring is present and the store is usable;
  3. the preferences keyring, if the application provides a Cryptor;
  4. the memory keyring.

Without tentackle-keyring, no @Service(KeyringFactory.class) exists, and the DefaultKeyringFactory steps in, covering steps 1, 3 and 4. The module registers the PlatformKeyringFactory, a subclass that adds step 2. Since only the module registers a provider, it wins in modular and classpath mode alike, whatever the order on the path.

An application that would rather not remember a secret at all than keep it in an insecure place checks isSecure(). An application that wants a different policy, or a keyring of its own (a hardware token, a password manager's CLI), replaces the factory. It extends PlatformKeyringFactory if it uses tentackle-keyring, and DefaultKeyringFactory otherwise:

@Service(KeyringFactory.class)
public class MyKeyringFactory extends PlatformKeyringFactory {

  @Override
  protected Keyring selectBestKeyring() {
    Keyring keyring = super.selectBestKeyring();
    if (!keyring.isSecure()) {
      return new MemoryKeyring();   // rather forget than store insecurely
    }
    return keyring;
  }
}

Testing

In an application's tests, force the memory keyring with -Dtentackle.keyring=memory, so the tests neither depend on nor pollute the developer's keychain or preferences.

See also