LlmSecureCredentials.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.sandbox.jdt.internal.ui.preferences;
import java.io.IOException;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.equinox.security.storage.ISecurePreferences;
import org.eclipse.equinox.security.storage.SecurePreferencesFactory;
import org.eclipse.equinox.security.storage.StorageException;
import org.osgi.service.prefs.BackingStoreException;
import org.sandbox.jdt.triggerpattern.llm.LlmProvider;
/** Secure-storage access and one-time migration for the LLM API key. */
public final class LlmSecureCredentials {
private static final String SCREENSHOT_MODE = "sandbox.help.screenshot.mode"; //$NON-NLS-1$
private static final String NODE_PATH = "sandbox_triggerpattern/llm"; //$NON-NLS-1$
private static final String API_KEY = "apiKey"; //$NON-NLS-1$
private static final String API_KEY_PROVIDER = "apiKeyProvider"; //$NON-NLS-1$
private static final String LEGACY_DEFAULT_PROVIDER = "GEMINI"; //$NON-NLS-1$
private static final ILog LOG = Platform.getLog(LlmSecureCredentials.class);
private LlmSecureCredentials() {
}
/** Returns the API key owned by the currently configured provider, or an empty string. */
public static String loadApiKey() {
IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(LlmPreferencePage.PLUGIN_ID);
return loadApiKey(preferences.get(LlmPreferencePage.PREF_PROVIDER, LEGACY_DEFAULT_PROVIDER));
}
/** Returns the API key only when the secure credential belongs to {@code providerName}. */
public static String loadApiKey(String providerName) {
if (Boolean.getBoolean(SCREENSHOT_MODE)) {
return ""; //$NON-NLS-1$
}
String provider = canonicalProvider(providerName);
if (provider == null) {
return ""; //$NON-NLS-1$
}
try {
migrateLegacyPreference(provider);
ISecurePreferences root = SecurePreferencesFactory.getDefault();
return loadApiKeyFromSecureStore(provider, root);
} catch (StorageException | IOException | BackingStoreException e) {
LOG.log(Status.warning("Could not read the LLM API key from Eclipse Secure Storage", e)); //$NON-NLS-1$
return ""; //$NON-NLS-1$
}
}
/** Stores the API key for the currently configured provider. */
public static void storeApiKey(String apiKey) throws StorageException, IOException, BackingStoreException {
IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(LlmPreferencePage.PLUGIN_ID);
storeApiKey(preferences.get(LlmPreferencePage.PREF_PROVIDER, LEGACY_DEFAULT_PROVIDER), apiKey);
}
/**
* Stores the API key encrypted and associates it with {@code providerName}.
* An empty value removes a credential only when it belongs to that provider.
*/
public static void storeApiKey(String providerName, String apiKey)
throws StorageException, IOException, BackingStoreException {
String provider = canonicalProvider(providerName);
if (provider == null) {
throw new IOException("Unsupported LLM provider: " + providerName); //$NON-NLS-1$
}
migrateLegacyPreference(provider);
ISecurePreferences root = SecurePreferencesFactory.getDefault();
storeApiKeyInSecureStore(provider, apiKey, root);
removeLegacyPreference();
}
static String loadApiKeyFromSecureStore(String providerName, ISecurePreferences root)
throws StorageException, IOException {
String provider = canonicalProvider(providerName);
if (provider == null || root == null) {
return ""; //$NON-NLS-1$
}
ISecurePreferences node = root.node(NODE_PATH);
String value = node.get(API_KEY, ""); //$NON-NLS-1$
if (value.isBlank()) {
return ""; //$NON-NLS-1$
}
String credentialProvider = node.get(API_KEY_PROVIDER, ""); //$NON-NLS-1$
if (credentialProvider.isBlank()) {
// Secure Storage from the first implementation had one unscoped key. Its
// only defensible migration is to bind it to the provider selected when
// the upgraded version first reads it.
node.put(API_KEY_PROVIDER, provider, false);
node.flush();
credentialProvider = provider;
}
return credentialForProvider(provider, credentialProvider, value);
}
static void storeApiKeyInSecureStore(String providerName, String apiKey, ISecurePreferences root)
throws StorageException, IOException {
String provider = canonicalProvider(providerName);
if (provider == null) {
throw new IOException("Unsupported LLM provider: " + providerName); //$NON-NLS-1$
}
if (root == null) {
throw new IOException("Eclipse Secure Storage is unavailable"); //$NON-NLS-1$
}
ISecurePreferences node = root.node(NODE_PATH);
String value = apiKey == null ? "" : apiKey.trim(); //$NON-NLS-1$
if (value.isEmpty()) {
String credentialProvider = node.get(API_KEY_PROVIDER, ""); //$NON-NLS-1$
if (credentialProvider.isBlank() || provider.equals(credentialProvider)) {
node.remove(API_KEY);
node.remove(API_KEY_PROVIDER);
node.flush();
}
} else {
node.put(API_KEY, value, true);
node.put(API_KEY_PROVIDER, provider, false);
node.flush();
}
}
/** Moves an API key saved by older Sandbox versions out of ordinary preferences. */
static void migrateLegacyPreference() throws StorageException, IOException, BackingStoreException {
IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(LlmPreferencePage.PLUGIN_ID);
migrateLegacyPreference(preferences.get(LlmPreferencePage.PREF_PROVIDER, LEGACY_DEFAULT_PROVIDER));
}
static void migrateLegacyPreference(String providerName)
throws StorageException, IOException, BackingStoreException {
String provider = canonicalProvider(providerName);
if (provider == null) {
return;
}
IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(LlmPreferencePage.PLUGIN_ID);
String legacy = preferences.get(LlmPreferencePage.PREF_API_KEY, ""); //$NON-NLS-1$
if (legacy == null || legacy.isBlank()) {
return;
}
ISecurePreferences root = SecurePreferencesFactory.getDefault();
if (root == null) {
return;
}
ISecurePreferences node = root.node(NODE_PATH);
if (node.get(API_KEY, "").isBlank()) { //$NON-NLS-1$
node.put(API_KEY, legacy, true);
node.put(API_KEY_PROVIDER, provider, false);
node.flush();
}
removeLegacyPreference();
}
static String canonicalProvider(String providerName) {
String value = providerName == null || providerName.isBlank()
? LEGACY_DEFAULT_PROVIDER
: providerName;
try {
return LlmProvider.fromString(value).name();
} catch (IllegalArgumentException e) {
return null;
}
}
static String credentialForProvider(String requestedProvider, String credentialProvider, String value) {
return requestedProvider != null && requestedProvider.equals(credentialProvider)
&& value != null && !value.isBlank()
? value
: ""; //$NON-NLS-1$
}
private static void removeLegacyPreference() throws BackingStoreException {
IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(LlmPreferencePage.PLUGIN_ID);
preferences.remove(LlmPreferencePage.PREF_API_KEY);
preferences.flush();
}
}