feat: add API key encryption utilities using Electron safeStorage
Implement encryption utilities for securely storing API keys: - encryptApiKey(): encrypts keys using Electron safeStorage with base64 encoding - decryptApiKey(): decrypts stored keys - isEncryptionAvailable(): checks platform support Encryption falls back to plaintext on platforms without keyring support. Includes comprehensive test coverage with Electron mock.
This commit is contained in:
61
src/utils/encryption-utils.ts
Normal file
61
src/utils/encryption-utils.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { safeStorage } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts an API key using Electron's safeStorage API
|
||||||
|
* Falls back to plaintext if encryption is not available (e.g., Linux without keyring)
|
||||||
|
* @param apiKey The plaintext API key to encrypt
|
||||||
|
* @returns Encrypted API key with "encrypted:" prefix, or plaintext if encryption unavailable
|
||||||
|
*/
|
||||||
|
export function encryptApiKey(apiKey: string): string {
|
||||||
|
if (!apiKey) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if encryption is available
|
||||||
|
if (!safeStorage.isEncryptionAvailable()) {
|
||||||
|
console.warn('Encryption not available, storing API key in plaintext');
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encrypted = safeStorage.encryptString(apiKey);
|
||||||
|
return `encrypted:${encrypted.toString('base64')}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to encrypt API key, falling back to plaintext:', error);
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts an API key encrypted with encryptApiKey
|
||||||
|
* @param stored The stored API key (encrypted or plaintext)
|
||||||
|
* @returns Decrypted API key
|
||||||
|
*/
|
||||||
|
export function decryptApiKey(stored: string): string {
|
||||||
|
if (!stored) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is an encrypted key
|
||||||
|
if (!stored.startsWith('encrypted:')) {
|
||||||
|
// Legacy plaintext key or fallback
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encryptedData = stored.substring(10); // Remove "encrypted:" prefix
|
||||||
|
const buffer = Buffer.from(encryptedData, 'base64');
|
||||||
|
return safeStorage.decryptString(buffer);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to decrypt API key:', error);
|
||||||
|
throw new Error('Failed to decrypt API key. You may need to regenerate it.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if encryption is available on the current platform
|
||||||
|
* @returns true if safeStorage encryption is available
|
||||||
|
*/
|
||||||
|
export function isEncryptionAvailable(): boolean {
|
||||||
|
return safeStorage.isEncryptionAvailable();
|
||||||
|
}
|
||||||
13
tests/__mocks__/electron.ts
Normal file
13
tests/__mocks__/electron.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Mock Electron API for testing
|
||||||
|
* This provides minimal mocks for the Electron types used in tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const safeStorage = {
|
||||||
|
isEncryptionAvailable: jest.fn(() => true),
|
||||||
|
encryptString: jest.fn((data: string) => Buffer.from(`encrypted:${data}`)),
|
||||||
|
decryptString: jest.fn((buffer: Buffer) => {
|
||||||
|
const str = buffer.toString();
|
||||||
|
return str.replace('encrypted:', '');
|
||||||
|
})
|
||||||
|
};
|
||||||
73
tests/encryption-utils.test.ts
Normal file
73
tests/encryption-utils.test.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { encryptApiKey, decryptApiKey } from '../src/utils/encryption-utils';
|
||||||
|
|
||||||
|
// Mock electron module
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
safeStorage: {
|
||||||
|
isEncryptionAvailable: jest.fn(() => true),
|
||||||
|
encryptString: jest.fn((data: string) => Buffer.from(`encrypted:${data}`)),
|
||||||
|
decryptString: jest.fn((buffer: Buffer) => {
|
||||||
|
const str = buffer.toString();
|
||||||
|
return str.replace('encrypted:', '');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Encryption Utils', () => {
|
||||||
|
describe('encryptApiKey', () => {
|
||||||
|
it('should encrypt API key when encryption is available', () => {
|
||||||
|
const apiKey = 'test-api-key-12345';
|
||||||
|
const encrypted = encryptApiKey(apiKey);
|
||||||
|
|
||||||
|
expect(encrypted).toMatch(/^encrypted:/);
|
||||||
|
expect(encrypted).not.toContain('test-api-key-12345');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return plaintext when encryption is not available', () => {
|
||||||
|
const { safeStorage } = require('electron');
|
||||||
|
safeStorage.isEncryptionAvailable.mockReturnValueOnce(false);
|
||||||
|
|
||||||
|
const apiKey = 'test-api-key-12345';
|
||||||
|
const result = encryptApiKey(apiKey);
|
||||||
|
|
||||||
|
expect(result).toBe(apiKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty string', () => {
|
||||||
|
const result = encryptApiKey('');
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('decryptApiKey', () => {
|
||||||
|
it('should decrypt encrypted API key', () => {
|
||||||
|
const apiKey = 'test-api-key-12345';
|
||||||
|
const encrypted = encryptApiKey(apiKey);
|
||||||
|
const decrypted = decryptApiKey(encrypted);
|
||||||
|
|
||||||
|
expect(decrypted).toBe(apiKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return plaintext if not encrypted format', () => {
|
||||||
|
const plaintext = 'plain-api-key';
|
||||||
|
const result = decryptApiKey(plaintext);
|
||||||
|
|
||||||
|
expect(result).toBe(plaintext);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty string', () => {
|
||||||
|
const result = decryptApiKey('');
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('round-trip encryption', () => {
|
||||||
|
it('should successfully encrypt and decrypt', () => {
|
||||||
|
const original = 'my-secret-api-key-abc123';
|
||||||
|
const encrypted = encryptApiKey(original);
|
||||||
|
const decrypted = decryptApiKey(encrypted);
|
||||||
|
|
||||||
|
expect(decrypted).toBe(original);
|
||||||
|
expect(encrypted).not.toBe(original);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user