Hi Raghav,
I’m glad to hear you’re getting closer to the finish line 
another requirement is to configure the secret keys dynamically with out application restart.
I would recommend using a custom Keyring that supports key rotation.
The Couchbase Field-Level Encryption RFC has a section on key rotation that describes why you’d want to do this and how you might implement it.
There’s also a Keyring.rotating(Keyring) helper method that takes an existing keyring and decorates it with key rotation behavior. It’s worth reading the Javadoc for that method, even if you don’t end up using it. The basic idea is the Encrypter asks the Keyring for a key using the “base name”, and the Keyring returns the latest version of the key with that base name.
Incidentally, the Keyring interface is designed to be narrow and composable; check out some of the other decoration methods like Keyring.caching
and Keyring.reloading
.
Okay. So let’s focus on using these building blocks to meet your requirement.
The first task is to decide on a naming convention for your keys. Let’s assume you want to keep things simple and follow the examples, which use ISO 8601 dates for the version strings, and --
as the delimiter that separates a key’s base name from the version.
Next, implement the backing Keyring. Because we want to use the rotating
decorator method, the keyring also needs to implement the ListableKeyring
interface (which just provides a method to get the names of all the keys in the keyring).
To keep things really simple, you could continue reading the encryption keys from the filesystem using KeyStoreKeyring
, and wrap it with a reloading
decorator so the keys are periodically reloaded from disk. (NOTE: the keyring returned by Keyring.reloading
does not implement the ListableKeyring interface required by Keyring.rotating
, so the following code sample includes a reloadingListable
decorator that does.)
Here’s what all of this might look like in code.
KeyStoreKeyring loadBackingKeyringFromDisk() { ... }
Keyring myRotatingKeyring() {
Duration reloadInterval = Duration.ofMinutes(5);
ListableKeyring reloadingKeyring = reloadingListable(
reloadInterval,
() -> loadBackingKeyringFromDisk()
);
String versionDelimiter = "--";
Comparator<String> versionComparator = Comparator.naturalOrder();
Keyring rotatingKeyring = Keyring.rotating(
versionDelimiter,
versionComparator,
reloadingKeyring
);
// Keyring.rotating uses linear search to find latest key version.
// This can be expensive if there are many keys, so...
Duration cacheExpiry = reloadInterval;
int maxCacheEntries = 10_000;
return Keyring.caching(
cacheExpiry,
maxCacheEntries,
rotatingKeyring
);
}
/**
* Returns a listable keyring wrapper whose backing keyring is
* periodically refreshed by calling the given supplier.
*/
public static ListableKeyring reloadingListable(
Duration reloadInterval,
Supplier<? extends ListableKeyring> loader
) {
requireNonNull(loader);
Cache<String, ListableKeyring> cache = Caffeine.newBuilder()
.expireAfterWrite(reloadInterval)
.maximumSize(1)
.build();
return new ListableKeyring() {
private ListableKeyring getBackingKeyring() {
return cache.get("", ignore -> loader.get());
}
@Override
public Collection<String> keyIds() {
return getBackingKeyring().keyIds();
}
@Override
public Optional<Key> get(String keyId) {
return getBackingKeyring().get(keyId);
}
};
}
One caveat is that you might see minor performance hiccups when the keyring is reloaded or there are cache misses. This can be addressed down the road by writing a custom Keyring
optimized for your use case instead of relying on the decorators.
tl;dr: Have a look at the Keyring source code on GitHub, and experiment with the above sample code. Remember that keys in the keyring should have names like myKey--2021-06-25
and the Encrypter should be configured with just myKey
.
Let me know how it goes.
Thanks,
David