How 5ire Encrypts Stored API Keys and Protects Sensitive Data
5ire encrypts API keys using AES-256-CBC with a per-installation secret derived from the CRYPTO_SECRET environment variable, storing only the encrypted payload in the local Electron store while keeping the encryption key in memory or the OS keychain.
The open-source Electron application 5ire (available at nanbingxyz/5ire) functions as a local AI assistant that requires users to input sensitive provider credentials. To prevent credential leakage, the codebase implements a multi-layer encryption pipeline that ensures API keys never persist in plaintext on disk.
Encryption Architecture Overview
5ire’s security model relies on symmetric encryption combined with environment-based secrets. The architecture ensures that even if an attacker gains read-only access to the user’s configuration directory at ~/.config/5ire, they cannot recover usable API keys without the per-installation master secret.
The protection layers include:
- Per-installation master secret (
CRYPTO_SECRET) loaded at runtime - AES-256-CBC encryption implemented in
src/main/services/encryptor.ts - Random IV generation for every encryption operation to prevent pattern analysis
- IPC isolation where encryption operations run in the main process, separate from the renderer
- Optional OS keychain storage for the master secret itself, as documented in
docs/ARCHITECTURE.md
The Encryption Pipeline
Loading the Master Secret
During application startup, src/main/main.ts (line 87) reads the CRYPTO_SECRET environment variable and injects it into the Environment class as cryptoSecret. This value serves as the salt for all subsequent encryption operations and never gets written to the store itself.
// Conceptual flow from main.ts
const cryptoSecret = process.env.CRYPTO_SECRET || '';
Environment.cryptoSecret = cryptoSecret;
Key Derivation Logic
Before encrypting any data, the Encryptor class in src/main/services/encryptor.ts (lines 21-23) derives a unique 32-byte key using the SHA-256 hash of the concatenated master secret and a user-provided context string (such as the provider identifier).
// From encryptor.ts lines 21-23
static #makeKey(key: string): Buffer {
return createHash('sha256')
.update(`${Environment.cryptoSecret}${key}`)
.digest()
.slice(0, 32); // Truncate to 32 bytes for AES-256
}
This derivation strategy ensures that each provider credential uses a distinct encryption key, limiting the blast radius if a single key were compromised.
AES-256-CBC Implementation
The core encryption logic resides in src/main/services/encryptor.ts (lines 32-39). For every encryption operation, the system:
- Generates a cryptographically random 16-byte IV
- Creates an AES-256-CBC cipher using the derived key
- Returns the IV (as hex) and ciphertext (as Base64)
// Encryption implementation (lines 32-39)
static encrypt(text: string, key: string): EncryptedData {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-cbc', this.#makeKey(key), iv);
const encrypted = cipher.update(text, 'utf8', 'base64') + cipher.final('base64');
return {
iv: iv.toString('hex'),
encrypted
};
}
Decryption (lines 50-53) reverses this process using the stored IV and the same derived key, returning the original plaintext only in memory.
Secure Storage Flow
IPC Bridge Communication
The renderer process never handles raw encryption keys. Instead, it communicates with the main process through the bridge defined in src/main/bridge/encryptor-bridge.ts. This exposes window.electron.crypto.encrypt and window.electron.crypto.decrypt, which marshal requests to the main process where the Encryptor class executes.
// Renderer-side usage pattern
const { iv, encrypted } = await window.electron.crypto.encrypt(
apiKey,
`provider-${userId}`
);
Electron Store Persistence
The encrypted payload is persisted via electron-store in src/renderer/pages/providers/index.tsx (lines 152-158). The code stores only the iv and encrypted fields, never the plaintext or the master secret.
// From providers/index.tsx lines 152-158
await window.electron.store.upsert({
id: userId,
data: { iv, encrypted },
});
System Keychain Integration
According to the ARCHITECTURE.md security section, 5ire supports storing the CRYPTO_SECRET itself in the OS keychain (macOS Keychain, Windows Credential Manager) rather than as an environment variable. This provides OS-level protection for the master encryption key, ensuring it never appears in the filesystem or process environment listings.
Practical Implementation Example
The following pattern demonstrates how 5ire handles provider credentials in the UI layer, combining encryption, storage, and decryption:
// Saving a new provider API key
async function saveProviderKey(userId: string, apiKey: string) {
// Encrypt on the main process side via IPC
const { iv, encrypted } = await window.electron.crypto.encrypt(
apiKey,
`provider-${userId}`
);
// Store only the encrypted blob
await window.electron.store.upsert({
id: userId,
data: { iv, encrypted },
});
}
// Retrieving the key for API calls
async function getProviderKey(userId: string): Promise<string> {
const { iv, encrypted } = await window.electron.store.get(userId);
// Decrypt only when needed, keep in memory briefly
const plain = await window.electron.crypto.decrypt(
encrypted,
`provider-${userId}`,
iv
);
return plain;
}
Summary
5ire implements defense-in-depth for credential protection:
- AES-256-CBC encryption with per-provider key derivation prevents unauthorized decryption
- Environment-based master secrets (
CRYPTO_SECRET) ensure installation-specific encryption - IPC isolation keeps cryptographic operations in the main process, away from renderer vulnerabilities
- Random IV generation for every encryption operation prevents ciphertext pattern analysis
- OS keychain support provides hardware-backed storage for the master secret when available
Frequently Asked Questions
What encryption algorithm does 5ire use?
5ire uses AES-256-CBC symmetric encryption. The implementation in src/main/services/encryptor.ts utilizes Node.js’s native crypto module, generating a random 16-byte IV for each encryption operation to ensure semantic security.
Where is the encryption key stored?
The master encryption key (derived from CRYPTO_SECRET) remains in process memory only. According to the architecture documentation, the CRYPTO_SECRET environment variable can optionally be stored in the OS keychain (macOS Keychain or Windows Credential Manager), preventing the secret from ever residing in the application’s configuration files.
Can encrypted API keys be decrypted by other 5ire installations?
No. Because the encryption key derivation incorporates the per-installation CRYPTO_SECRET environment variable, encrypted credentials stored by one 5ire instance cannot be decrypted by another installation unless both share the identical CRYPTO_SECRET value. This effectively binds credential encryption to the specific device and installation.
Does 5ire store API keys in plain text at any point?
No. API keys are encrypted immediately in the main process via the IPC bridge before reaching the Electron store. The plaintext exists only transiently in memory during the active encryption or decryption operation, and the persistent storage at ~/.config/5ire contains only the AES-256-CBC encrypted payload with its associated IV.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →