Secret Store

This is where Sluice keeps your API key: your operating system's own credential store - Credential Manager on Windows, Keychain on macOS, Secret Service on Linux. It is a separate library rather than part of Sluice, so the rest of this page is written for developers. What matters to you is the rule in the box below, and that the key never goes into a settings file.

For developers: a Java library for desktop and command-line applications that have to keep an API key somewhere. One call puts a credential in the strongest place the machine offers, on Windows, macOS and Linux.

Where a credential goes

Three places. A credential is read from the first that answers, and saved to the strongest that can be written to.

  1. An environment variable, read first, because someone who sets one is making a deliberate override.
  2. The operating system's own store. Windows Credential Manager, the macOS keychain, or the freedesktop Secret Service on Linux.
  3. A permission-restricted file in a directory you name, narrowed to your account.

Where the filesystem will not narrow that credential file to your account, it refuses to store the credential at all. It does not fall back to a file anyone can read.

The first and the third are on only when specifically requested in the library builder. Both hold a credential in the clear, so an application under a policy against that leaves one or both out. The operating system's store has no such switch and is used wherever the platform offers one.

What each platform gets

PlatformThe store it reachesThrough
WindowsCredential Manageradvapi32, the Cred* functions
macOSKeychainSecurity.framework, the SecItem* functions
Linuxfreedesktop Secret Servicelibsecret, which desktop distributions ship

All three are bound through java.lang.foreign, so there is no third-party binding layer to keep in step with the JDK. A container image without libsecret simply has no such place. There a save falls through to the credential file if you asked for one, and refuses outright if you did not.

What it checks rather than assumes

A library can set owner-only permissions and report success. A mount can accept that change and drop it. So this one sets the rule, reads it back, and compares. A file that did not end up narrowed is reported as a failure rather than as a stored credential.

That is what the refusal to store a badly-permissioned credential rests on.

Asking where a credential lives is separated from reading it. The calls that report the place cannot return the credential at all, so a settings screen showing where a key is kept cannot leak the key it was never handed.

What it protects against, and what it does not

Each place is only as strong as the platform underneath it, and the two stored places are not equally strong.

Nothing here defends against code running as you, against root or an administrator, or against a memory dump. The credential is an ordinary String on the heap for as long as you hold it. It is built for an application that runs as one person on their own machine, rather than for a service.

Using it

Java 25 or newer. java.lang.foreign is only final from 22, and the floor sits at the current LTS. There is no dependency beyond the JDK.

<dependency>
  <groupId>photos.sluice</groupId>
  <artifactId>secret-store</artifactId>
  <version>1.0.1</version>
</dependency>

Storing and reading one credential:

SecretStore store = SecretStore.forApplication("YourApp")
        .inNamespace("com.example.yourapp")
        .withEnvironmentOverride()
        .withCredentialFilesIn(configDirectory)
        .open();

SecretId key = new SecretId("anthropic", "ANTHROPIC_API_KEY");

store.save(key, "sk-...");
store.secret(key);      // Optional<String>, from whichever place answered
store.status(key);      // which place is in force, carrying no credential
store.remove(key);      // clears every place a save can write to

Reaching the operating system's store needs a native-access grant, which your application passes at launch.

--enable-native-access=ALL-UNNAMED           # on the classpath
--enable-native-access=photos.sluice.secrets # on the module path

The application name and the namespace decide the native keys, so changing either afterwards stops the store finding what it saved. Nothing enumerates what is stored: you read and write credentials you already name.

Licence, and where it came from

MIT, with no contributor licence agreement, because MIT contributions need no relicensing right. That is deliberately not the AGPL that Sluice itself carries. Sluice is an application, and this is a component anyone should be able to use.

It was cut out of Sluice, which needed somewhere to put an API key and found the two Java keyring libraries most people reach for were no longer moving. Nothing photo-specific survived the cut, and the photos.sluice group id is the domain verified with Maven Central rather than a statement about the subject.