API Key Management
OpenWork provides secure storage and validation for API keys from multiple AI service providers. The system uses machine-specific encryption to protect sensitive credentials while maintaining usability across different platforms.
Supported Providers
| Provider | Service | Validation Endpoint |
|---|---|---|
anthropic |
Anthropic Claude | https://api.anthropic.com/v1/messages |
openai |
OpenAI GPT | https://api.openai.com/v1/models |
google |
Google Gemini | https://generativelanguage.googleapis.com/v1/models |
groq |
Groq Llama | https://api.groq.com/openapi.json |
custom |
Custom endpoint | User-defined endpoint |
Implementation
The API key system is implemented with AES-256-GCM encryption and machine-specific key derivation.
Source: /Users/nateb/openwork-repo/apps/desktop/src/main/store/secureStorage.ts
Encryption Architecture
/**
* Secure storage using electron-store with custom AES-256-GCM encryption.
*
* This implementation derives an encryption key from machine-specific values
* (hostname, platform, user home directory, app path) to avoid macOS Keychain
* prompts while still providing reasonable security for API keys.
*
* Security considerations:
* - Keys are encrypted at rest using AES-256-GCM
* - Encryption key is derived from machine-specific data (not stored)
* - Less secure than Keychain (key derivation could be reverse-engineered)
* - Suitable for API keys that can be rotated if compromised
*/
Key Derivation
function getDerivedKey(): Buffer {
// Combine machine-specific values to create a unique identifier
const machineData = [
os.platform(),
os.homedir(),
os.userInfo().username,
app.getPath('userData'),
'ai.accomplish.desktop', // App identifier
].join(':');
const salt = getSalt();
// Use PBKDF2 to derive a 256-bit key
_derivedKey = crypto.pbkdf2Sync(
machineData,
salt,
100000, // iterations
32, // key length (256 bits)
'sha256'
);
return _derivedKey;
}
Storage Format
Encrypted values are stored as base64 strings with the format:
iv:authTag:ciphertext
API Methods
Core Operations
// Store an API key securely
export function storeApiKey(provider: string, apiKey: string): void
// Retrieve an API key
export function getApiKey(provider: string): string | null
// Delete an API key
export function deleteApiKey(provider: string): boolean
Batch Operations
// Get all API keys for all providers
export async function getAllApiKeys(): Promise<Record<ApiKeyProvider, string | null>>
// Check if any API key is stored
export async function hasAnyApiKey(): Promise<boolean>
// List all stored credentials
export function listStoredCredentials(): Array<{ account: string; password: string }>
Utility Functions
// Clear all secure storage (used during fresh install cleanup)
export function clearSecureStorage(): void
Security Implementation
Multi-Layer Protection
- Encryption at Rest: All API keys are encrypted using AES-256-GCM
- Machine-Specific Keys: Encryption keys are derived from unique machine identifiers
- Salt Management: Each installation generates a unique salt for key derivation
- Secure Storage: Data persisted using electron-store with encrypted values
Key Derivation Process
graph TD
A[Machine Data] --> B[Combine Values]
B --> C[Add Installation Salt]
C --> D[PBKDF2 with 100k iterations]
D --> E[256-bit Encryption Key]
E --> F[AES-256-GCM Encryption]
F --> G[Store: iv:authTag:ciphertext]
Security Considerations
Strengths
- AES-256-GCM provides strong encryption
- 100k PBKDF2 iterations prevent brute-force attacks
- Machine-specific keys prevent data portability between devices
- Base64 encoding prevents file system encoding issues
Limitations
- Less secure than OS Keychain integration
- Key derivation could potentially be reverse-engineered
- Requires users to re-enter keys after system reinstalls
Validation System
Provider-Specific Validation
Each provider has a dedicated validation endpoint:
// Anthropic validation
async function validateAnthropicKey(apiKey: string): Promise<boolean> {
const response = await fetchWithTimeout(
'https://api.anthropic.com/v1/messages',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-3-haiku-20240307',
max_tokens: 10,
messages: [{ role: 'user', content: 'test' }],
}),
},
API_KEY_VALIDATION_TIMEOUT_MS
);
return response.ok;
}
Validation Workflow
- User Input: API key entered through UI
- Secure Storage: Key encrypted and stored
- Validation: Test request to provider API
- Result: Success or failure feedback to user
- Fallback: Handle rate limits, invalid keys, network errors
Usage Patterns
Basic Operations
// Store a key
storeApiKey('anthropic', 'sk-ant-api03-...');
// Retrieve a key
const key = getApiKey('anthropic');
if (key) {
// Use the key for API calls
}
// Delete a key
deleteApiKey('anthropic');
Batch Management
// Get all configured providers
const allKeys = await getAllApiKeys();
const activeProviders = Object.entries(allKeys)
.filter(([_, key]) => key !== null)
.map(([provider, _]) => provider);
// Check for any configured keys
if (await hasAnyApiKey()) {
// App is ready to use
} else {
// Show onboarding flow
}
Error Handling
Common Scenarios
Decryption Failure
function decryptValue(encryptedData: string): string | null {
try {
// Attempt decryption
return decryptedValue;
} catch {
// Don't log error details to avoid leaking sensitive context
return null;
}
}
Validation Timeouts
const API_KEY_VALIDATION_TIMEOUT_MS = 15000;
Storage Access Issues
- Handle cases where storage becomes unavailable
- Graceful degradation when encryption fails
- User-friendly error messages
Platform Differences
macOS
- Uses standard Node.js crypto (no Keychain integration)
- Machine-specific data includes platform and user directory
- No additional authentication prompts
Windows
- Same encryption implementation as macOS
- Machine-specific data includes Windows-specific paths
- Compatible with Windows Credential Manager (though not used)
Linux
- Uses standard Node.js crypto
- Machine-specific data includes Linux-specific paths
- Compatible with various Linux keyring implementations (though not used)
Migration and Backup
Data Format
- Encrypted values stored in electron-store configuration files
- Salt generated per installation
- No migration path between different machines
Fresh Install Cleanup
// During fresh install, old data is cleared
if (process.env.CLEAN_START === '1') {
const userDataPath = app.getPath('userData');
if (fs.existsSync(userDataPath)) {
fs.rmSync(userDataPath, { recursive: true, force: true });
}
// Note: Secure storage (API keys, auth tokens) is stored in electron-store
// which lives in userData, so it gets cleared with the directory above
}
Performance Considerations
Lazy Initialization
let _secureStore: Store<SecureStorageSchema> | null = null;
let _derivedKey: Buffer | null = null;
function getSecureStore(): Store<SecureStorageSchema> {
if (!_secureStore) {
_secureStore = new Store<SecureStorageSchema>({
name: getStoreName(),
defaults: { values: {} },
});
}
return _secureStore;
}
Cache Management
- Derived keys are cached in memory for performance
- Stores are initialized on first access
- Clear cache on storage reset
Security Best Practices
- Never log raw API keys in application logs
- Validate keys before storing to ensure they work
- Use timeouts for validation requests
- Handle errors gracefully without exposing sensitive information
- Consider key rotation for production deployments
- Document security boundaries for users and developers
The API key management system provides a good balance between security and usability, ensuring sensitive credentials are protected while maintaining a smooth user experience.