Package org.forgerock.secrets

Provides a unified API for accessing secrets of various kinds. Secrets here means system credentials such as connection passwords and API keys, and also cryptographic key material such as encryption keys or digital signature keys. The API has been designed to be simple to use for common operations, whilst allowing the platform to support a number of core requirements around secret management:
  • support for rotating secrets, with a grace period during which the previous secret/key is still valid;
  • support for pluggable secure backends for storing secrets, including support for Hardware Security Modules (HSMs) and dev-ops friendly options such as Docker or Kubernetes Secrets, and more sophisticated secret management systems such as Hashicorp Vault;
  • ability to separate ("externalize") secrets from other configuration, such that configuration can be shared between environments while secrets are not shared.

Secrets Providers

The entry point for getting hold of secrets in an application is the SecretsProvider class. This API is organized around the concept of Secrets being used for specific Purposes. A purpose is simply a string name together with an indication of the type of secret that is required for that purpose (e.g., a SigningKey is needed for signing messages). The purpose system is designed to be extensible, and application developers are encouraged to define their own Purpose instances for application-specific usage. For example, IG might define a purpose for decrypting password replay messages:

     private static final Purpose<DataDecryptionKey> PASSWORD_REPLAY_DECRYPTION
          = purpose("password_replay", DataDecryptionKey.class);
 
This can then be used to locate the current key to use for decryption:

      SecretsProvider provider = ...; // Application-specific
      DataDecryptionKey key = provider.getActiveSecret(PASSWORD_REPLAY_DECRYPTION).getOrThrow();
      byte[] decrypted = key.getCipher(algorithm).doFinal(encryptedPassword);
 
Alternatively, to support key rotation you can try to decrypt with all valid keys for the given purpose:

      Optional<byte[]> decrypted = provider.getValidSecrets(PASSWORD_REPLAY_DECRYPTION)
                                           .getOrThrow()
                                           .map(key -> tryDecrypt(key, encryptedPassword))
                                           .filter(Objects::nonNull)
                                           .findAny();
 
Finally, if you know the specific key/secret that was used for a particular operation then you can lookup using a stable identifier:

      DataDecryptionKey key = provider.getNamedSecret(PASSWORD_REPLAY_DECRYPTION, keyId).getOrThrow();
 

Typically an application would use the first method when producing data using a particular secret (possibly using Secret.getStableId() to record the particular secret used, for instance as a JWT "kid" claim), while the second and third methods would be used when consuming data.

Asynchronous API

All of the methods in the SecretsProvider API are designed to be asynchronous, using Promises to return results or exceptions.

Secret Stores

A single SecretsProvider may have multiple back-end SecretStores configured to handle storage of secrets for different purposes. The provider will route requests for secrets to the appropriate store. As with secrets, stores themselves may be rotated, allowing for migration from one secret store to another. For instance, migrating from a file-based store to a network-attached HSM.

As well as methods to query secrets for different purposes, stores may optionally also support three basic management functions:

rotate(purpose, newActiveId)
This method allows to rotate the secret for a given purpose, installing the secret with the given stable id as the new active secret for that purpose. The previous active secret for that purpose will still be available for named/valid secret queries.
retire(purpose, oldSecretId)
This method allows to retire a secret for a given purpose when it is no longer to be used.
revoke(secretId)
This method immediately revokes the given secret for all purposes and prevents it being used any more in any capacity. As such this method is the "break glass in emergency" button that can be used in case of suspected secret compromise.
As these methods are all optional, callers should be prepared to catch UnsupportedOperationException when calling them. They are all synchronous methods to enable the caller to be sure when the change has taken effect. They should therefore be performed in a background thread if asynchronous operation is desired.

Generic Secrets

Generic secrets can be used to store arbitrary binary or text data that should be kept confidential. Examples would include connection passwords or OAuth 2.0 client secrets and other API keys. The mechanism for accessing these kinds of secrets is the GenericSecret class, which provides a number of methods for revealing the secret data in a controlled fashion. In each case, the consumer of the secret material supplies a callback function that will be called with the secret material and can use it to e.g. initialise an LDAP bind request:

      BindRequest bind = provider.getActiveSecret(LDAP_BIND_PASSWORD)
                                 .revealAsUtf8(password -> Requests.newSimpleBindRequest(username, password));
 
Code should take care not to assume the availability of the secret beyond the end of the request, and should take a defensive copy if the secret must be retained beyond that scope (but consider storing the Secret object directly instead in this case).

Cryptographic Keys

The API provides extensive support for operations using cryptographic keys for various purposes, including:
  • data encryption and decryption;
  • key encryption ("wrapping") and decryption ("unwrapping");
  • digital signature signing and verification;
  • key agreement (e.g., Elliptic Curve Diffie-Hellman) and key derivation.
The requirement for the secrets API is to support the wide variety of cryptographic algorithms that our products already use, and will be required to use in the future. The API therefore remains agnostic as to particular algorithms that will be used with particular keys, and it should be possible to perform any operation that would otherwise be possible using a standard Java Key object. Nevertheless, the API has been designed to guide towards safe usage of keys. For instance, providing convenience methods that correctly initialise cryptographic services with safe defaults. In some cases it is also possible to perform operations without specifying the algorithm to use, in which case the API will attempt to select a suitable secure default.

The root of the cryptographic key hierarchy is the CryptoKey abstract class. Typically this would not be used directly, but instead one of the concrete classes:

Secret References

For the case where an application wants to configure the specification of a secret once and then not worry about refreshing it, a SecretReference can be used to store a long-lived reference to the active secret for a given purpose. The reference will ensure that the secret is refreshed periodically to ensure that the current active secret is always used. This ensures that key rotations are picked up in a timely fashion.