feat(providers): implement secure API key storage and provider management

Add complete provider configuration system with the following features:

- Secure API key storage using Electron's safeStorage encryption
- Provider CRUD operations with IPC handlers
- Lazy-loaded electron-store for ESM compatibility
- Provider settings UI component with add/edit/delete functionality
- API key masking for display (shows first/last 4 chars)
- Basic API key format validation per provider type
- Default provider selection
- Provider enable/disable toggle

New files:
- electron/utils/secure-storage.ts: Encrypted key storage and provider config
- src/stores/providers.ts: Zustand store for provider state
- src/components/settings/ProvidersSettings.tsx: Provider management UI
This commit is contained in:
Haze
2026-02-05 23:24:31 +08:00
Unverified
parent 18dc3bf53f
commit ebb6f515a7
7 changed files with 1089 additions and 1 deletions

View File

@@ -4,6 +4,21 @@
*/
import { ipcMain, BrowserWindow, shell, dialog, app } from 'electron';
import { GatewayManager } from '../gateway/manager';
import {
storeApiKey,
getApiKey,
deleteApiKey,
hasApiKey,
saveProvider,
getProvider,
getAllProviders,
deleteProvider,
setDefaultProvider,
getDefaultProvider,
getAllProvidersWithKeyInfo,
isEncryptionAvailable,
type ProviderConfig,
} from '../utils/secure-storage';
/**
* Register all IPC handlers
@@ -15,6 +30,9 @@ export function registerIpcHandlers(
// Gateway handlers
registerGatewayHandlers(gatewayManager, mainWindow);
// Provider handlers
registerProviderHandlers();
// Shell handlers
registerShellHandlers();
@@ -136,6 +154,136 @@ function registerGatewayHandlers(
});
}
/**
* Provider-related IPC handlers
*/
function registerProviderHandlers(): void {
// Check if encryption is available
ipcMain.handle('provider:encryptionAvailable', () => {
return isEncryptionAvailable();
});
// Get all providers with key info
ipcMain.handle('provider:list', async () => {
return await getAllProvidersWithKeyInfo();
});
// Get a specific provider
ipcMain.handle('provider:get', async (_, providerId: string) => {
return await getProvider(providerId);
});
// Save a provider configuration
ipcMain.handle('provider:save', async (_, config: ProviderConfig, apiKey?: string) => {
try {
// Save the provider config
await saveProvider(config);
// Store the API key if provided
if (apiKey) {
await storeApiKey(config.id, apiKey);
}
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
});
// Delete a provider
ipcMain.handle('provider:delete', async (_, providerId: string) => {
try {
await deleteProvider(providerId);
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
});
// Update API key for a provider
ipcMain.handle('provider:setApiKey', async (_, providerId: string, apiKey: string) => {
try {
await storeApiKey(providerId, apiKey);
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
});
// Delete API key for a provider
ipcMain.handle('provider:deleteApiKey', async (_, providerId: string) => {
try {
await deleteApiKey(providerId);
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
});
// Check if a provider has an API key
ipcMain.handle('provider:hasApiKey', async (_, providerId: string) => {
return await hasApiKey(providerId);
});
// Get the actual API key (for internal use only - be careful!)
ipcMain.handle('provider:getApiKey', async (_, providerId: string) => {
return await getApiKey(providerId);
});
// Set default provider
ipcMain.handle('provider:setDefault', async (_, providerId: string) => {
try {
await setDefaultProvider(providerId);
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
});
// Get default provider
ipcMain.handle('provider:getDefault', async () => {
return await getDefaultProvider();
});
// Validate API key by making a test request (simulated for now)
ipcMain.handle('provider:validateKey', async (_, providerId: string, apiKey: string) => {
// In a real implementation, this would make a test API call to the provider
// For now, we'll just do basic format validation
try {
// Basic validation based on provider type
const provider = await getProvider(providerId);
if (!provider) {
return { valid: false, error: 'Provider not found' };
}
switch (provider.type) {
case 'anthropic':
if (!apiKey.startsWith('sk-ant-')) {
return { valid: false, error: 'Anthropic keys should start with sk-ant-' };
}
break;
case 'openai':
if (!apiKey.startsWith('sk-')) {
return { valid: false, error: 'OpenAI keys should start with sk-' };
}
break;
case 'google':
if (apiKey.length < 20) {
return { valid: false, error: 'Google API key seems too short' };
}
break;
}
// Simulate API validation delay
await new Promise((resolve) => setTimeout(resolve, 1000));
return { valid: true };
} catch (error) {
return { valid: false, error: String(error) };
}
});
}
/**
* Shell-related IPC handlers
*/

View File

@@ -52,6 +52,17 @@ const electronAPI = {
'env:setApiKey',
'env:deleteApiKey',
// Provider
'provider:encryptionAvailable',
'provider:list',
'provider:get',
'provider:save',
'provider:delete',
'provider:setApiKey',
'provider:deleteApiKey',
'provider:hasApiKey',
'provider:getApiKey',
'provider:setDefault',
'provider:getDefault',
'provider:validateKey',
// Cron
'cron:list',

View File

@@ -0,0 +1,273 @@
/**
* Secure Storage Utility
* Uses Electron's safeStorage for encrypting sensitive data like API keys
*/
import { safeStorage } from 'electron';
// Lazy-load electron-store (ESM module)
let store: any = null;
let providerStore: any = null;
async function getStore() {
if (!store) {
const Store = (await import('electron-store')).default;
store = new Store({
name: 'clawx-secure',
defaults: {
encryptedKeys: {},
},
});
}
return store;
}
async function getProviderStore() {
if (!providerStore) {
const Store = (await import('electron-store')).default;
providerStore = new Store({
name: 'clawx-providers',
defaults: {
providers: {},
},
});
}
return providerStore;
}
/**
* Provider configuration
*/
export interface ProviderConfig {
id: string;
name: string;
type: 'anthropic' | 'openai' | 'google' | 'ollama' | 'custom';
baseUrl?: string;
model?: string;
enabled: boolean;
createdAt: string;
updatedAt: string;
}
/**
* Check if encryption is available
*/
export function isEncryptionAvailable(): boolean {
return safeStorage.isEncryptionAvailable();
}
/**
* Store an API key securely
*/
export async function storeApiKey(providerId: string, apiKey: string): Promise<boolean> {
try {
const s = await getStore();
if (!safeStorage.isEncryptionAvailable()) {
console.warn('Encryption not available, storing key in plain text');
// Fallback to plain storage (not recommended for production)
const keys = s.get('encryptedKeys') as Record<string, string>;
keys[providerId] = Buffer.from(apiKey).toString('base64');
s.set('encryptedKeys', keys);
return true;
}
// Encrypt the API key
const encrypted = safeStorage.encryptString(apiKey);
const keys = s.get('encryptedKeys') as Record<string, string>;
keys[providerId] = encrypted.toString('base64');
s.set('encryptedKeys', keys);
return true;
} catch (error) {
console.error('Failed to store API key:', error);
return false;
}
}
/**
* Retrieve an API key
*/
export async function getApiKey(providerId: string): Promise<string | null> {
try {
const s = await getStore();
const keys = s.get('encryptedKeys') as Record<string, string>;
const encryptedBase64 = keys[providerId];
if (!encryptedBase64) {
return null;
}
if (!safeStorage.isEncryptionAvailable()) {
// Fallback for plain storage
return Buffer.from(encryptedBase64, 'base64').toString('utf-8');
}
// Decrypt the API key
const encrypted = Buffer.from(encryptedBase64, 'base64');
return safeStorage.decryptString(encrypted);
} catch (error) {
console.error('Failed to retrieve API key:', error);
return null;
}
}
/**
* Delete an API key
*/
export async function deleteApiKey(providerId: string): Promise<boolean> {
try {
const s = await getStore();
const keys = s.get('encryptedKeys') as Record<string, string>;
delete keys[providerId];
s.set('encryptedKeys', keys);
return true;
} catch (error) {
console.error('Failed to delete API key:', error);
return false;
}
}
/**
* Check if an API key exists for a provider
*/
export async function hasApiKey(providerId: string): Promise<boolean> {
const s = await getStore();
const keys = s.get('encryptedKeys') as Record<string, string>;
return providerId in keys;
}
/**
* List all provider IDs that have stored keys
*/
export async function listStoredKeyIds(): Promise<string[]> {
const s = await getStore();
const keys = s.get('encryptedKeys') as Record<string, string>;
return Object.keys(keys);
}
// ==================== Provider Configuration ====================
/**
* Save a provider configuration
*/
export async function saveProvider(config: ProviderConfig): Promise<void> {
const s = await getProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
providers[config.id] = config;
s.set('providers', providers);
}
/**
* Get a provider configuration
*/
export async function getProvider(providerId: string): Promise<ProviderConfig | null> {
const s = await getProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
return providers[providerId] || null;
}
/**
* Get all provider configurations
*/
export async function getAllProviders(): Promise<ProviderConfig[]> {
const s = await getProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
return Object.values(providers);
}
/**
* Delete a provider configuration
*/
export async function deleteProvider(providerId: string): Promise<boolean> {
try {
// Delete the API key first
await deleteApiKey(providerId);
// Delete the provider config
const s = await getProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
delete providers[providerId];
s.set('providers', providers);
// Clear default if this was the default
if (s.get('defaultProvider') === providerId) {
s.delete('defaultProvider');
}
return true;
} catch (error) {
console.error('Failed to delete provider:', error);
return false;
}
}
/**
* Set the default provider
*/
export async function setDefaultProvider(providerId: string): Promise<void> {
const s = await getProviderStore();
s.set('defaultProvider', providerId);
}
/**
* Get the default provider
*/
export async function getDefaultProvider(): Promise<string | undefined> {
const s = await getProviderStore();
return s.get('defaultProvider') as string | undefined;
}
/**
* Get provider with masked key info (for UI display)
*/
export async function getProviderWithKeyInfo(providerId: string): Promise<(ProviderConfig & { hasKey: boolean; keyMasked: string | null }) | null> {
const provider = await getProvider(providerId);
if (!provider) return null;
const apiKey = await getApiKey(providerId);
let keyMasked: string | null = null;
if (apiKey) {
// Show first 4 and last 4 characters
if (apiKey.length > 12) {
keyMasked = `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`;
} else {
keyMasked = '*'.repeat(apiKey.length);
}
}
return {
...provider,
hasKey: !!apiKey,
keyMasked,
};
}
/**
* Get all providers with key info (for UI display)
*/
export async function getAllProvidersWithKeyInfo(): Promise<Array<ProviderConfig & { hasKey: boolean; keyMasked: string | null }>> {
const providers = await getAllProviders();
const results: Array<ProviderConfig & { hasKey: boolean; keyMasked: string | null }> = [];
for (const provider of providers) {
const apiKey = await getApiKey(provider.id);
let keyMasked: string | null = null;
if (apiKey) {
if (apiKey.length > 12) {
keyMasked = `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`;
} else {
keyMasked = '*'.repeat(apiKey.length);
}
}
results.push({
...provider,
hasKey: !!apiKey,
keyMasked,
});
}
return results;
}