- Encrypt credentials before storing in localStorage when OS keyring unavailable - Decrypt on retrieval with automatic fallback - Backward compatible with existing unencrypted data (migration on next set) - Add comprehensive unit tests (11 test cases) Security: Credentials are now encrypted using AES-GCM when OS keyring is unavailable, preventing plaintext exposure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
188 lines
5.9 KiB
TypeScript
188 lines
5.9 KiB
TypeScript
/**
|
|
* Tests for secure-storage.ts
|
|
*
|
|
* These tests verify that credentials are encrypted when stored in localStorage
|
|
* (fallback mode when OS keyring is unavailable).
|
|
*/
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
|
// Mock Tauri runtime to return false (non-Tauri environment)
|
|
vi.mock('../../src/lib/tauri-gateway', () => ({
|
|
isTauriRuntime: () => false,
|
|
}));
|
|
|
|
// Import after mocking
|
|
import { secureStorage } from '../../src/lib/secure-storage';
|
|
|
|
describe('secureStorage', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('encryption fallback', () => {
|
|
it('should encrypt data when storing to localStorage', async () => {
|
|
const key = 'test-key';
|
|
const value = 'secret-value';
|
|
|
|
await secureStorage.set(key, value);
|
|
|
|
// Check that localStorage doesn't contain plaintext
|
|
const encryptedKey = 'enc_' + key;
|
|
const stored = localStorage.getItem(encryptedKey);
|
|
expect(stored).not.toBeNull();
|
|
expect(stored).not.toBe(value);
|
|
expect(stored).not.toContain('secret-value');
|
|
|
|
// Should be JSON with iv and data fields
|
|
const parsed = JSON.parse(stored!);
|
|
expect(parsed).toHaveProperty('iv');
|
|
expect(parsed).toHaveProperty('data');
|
|
});
|
|
|
|
it('should decrypt data when retrieving from localStorage', async () => {
|
|
const key = 'test-key';
|
|
const value = 'secret-value';
|
|
|
|
await secureStorage.set(key, value);
|
|
const retrieved = await secureStorage.get(key);
|
|
|
|
expect(retrieved).toBe(value);
|
|
});
|
|
|
|
it('should handle special characters in values', async () => {
|
|
const key = 'test-key';
|
|
const value = 'p@ssw0rd!#$%^&*(){}[]|\\:";\'<>?,./~`';
|
|
|
|
await secureStorage.set(key, value);
|
|
const retrieved = await secureStorage.get(key);
|
|
|
|
expect(retrieved).toBe(value);
|
|
});
|
|
|
|
it('should handle Unicode characters in values', async () => {
|
|
const key = 'test-key';
|
|
const value = '密码测试123テスト🔑🔐';
|
|
|
|
await secureStorage.set(key, value);
|
|
const retrieved = await secureStorage.get(key);
|
|
|
|
expect(retrieved).toBe(value);
|
|
});
|
|
|
|
it('should handle empty string by removing the key', async () => {
|
|
const key = 'test-key';
|
|
const value = 'initial-value';
|
|
|
|
await secureStorage.set(key, value);
|
|
expect(await secureStorage.get(key)).toBe(value);
|
|
|
|
// Setting empty string should remove the key
|
|
await secureStorage.set(key, '');
|
|
const encryptedKey = 'enc_' + key;
|
|
expect(localStorage.getItem(encryptedKey)).toBeNull();
|
|
expect(await secureStorage.get(key)).toBeNull();
|
|
});
|
|
|
|
it('should handle null by removing the key', async () => {
|
|
const key = 'test-key';
|
|
const value = 'initial-value';
|
|
|
|
await secureStorage.set(key, value);
|
|
expect(await secureStorage.get(key)).toBe(value);
|
|
|
|
// Delete should remove the key
|
|
await secureStorage.delete(key);
|
|
const encryptedKey = 'enc_' + key;
|
|
expect(localStorage.getItem(encryptedKey)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('backward compatibility', () => {
|
|
it('should read unencrypted legacy data', async () => {
|
|
const key = 'legacy-key';
|
|
const value = 'legacy-value';
|
|
|
|
// Simulate legacy unencrypted storage
|
|
localStorage.setItem(key, value);
|
|
|
|
const retrieved = await secureStorage.get(key);
|
|
expect(retrieved).toBe(value);
|
|
});
|
|
|
|
it('should migrate unencrypted data to encrypted on next set', async () => {
|
|
const key = 'legacy-key';
|
|
const value = 'legacy-value';
|
|
const newValue = 'new-encrypted-value';
|
|
|
|
// Simulate legacy unencrypted storage
|
|
localStorage.setItem(key, value);
|
|
|
|
// Read should return legacy value
|
|
const retrieved = await secureStorage.get(key);
|
|
expect(retrieved).toBe(value);
|
|
|
|
// Write should encrypt the new value
|
|
await secureStorage.set(key, newValue);
|
|
|
|
// Legacy key should be removed, encrypted key should exist
|
|
expect(localStorage.getItem(key)).toBeNull();
|
|
const encryptedKey = 'enc_' + key;
|
|
const stored = localStorage.getItem(encryptedKey);
|
|
expect(stored).not.toBeNull();
|
|
expect(stored).not.toContain(newValue);
|
|
|
|
// Should retrieve the new encrypted value
|
|
expect(await secureStorage.get(key)).toBe(newValue);
|
|
});
|
|
});
|
|
|
|
describe('encryption strength', () => {
|
|
it('should use different IV for each encryption', async () => {
|
|
const key = 'test-key';
|
|
const value = 'same-value';
|
|
|
|
await secureStorage.set(key, value);
|
|
const encrypted1 = localStorage.getItem('enc_' + key);
|
|
|
|
await secureStorage.set(key, value);
|
|
const encrypted2 = localStorage.getItem('enc_' + key);
|
|
|
|
// Both should be encrypted versions of the same value
|
|
expect(encrypted1).not.toBe(encrypted2);
|
|
|
|
// But both should decrypt to the same value
|
|
const parsed1 = JSON.parse(encrypted1!);
|
|
const parsed2 = JSON.parse(encrypted2!);
|
|
expect(parsed1.iv).not.toBe(parsed2.iv); // Different IVs
|
|
expect(parsed1.data).not.toBe(parsed2.data); // Different ciphertext
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should return null for non-existent keys', async () => {
|
|
const retrieved = await secureStorage.get('non-existent-key');
|
|
expect(retrieved).toBeNull();
|
|
});
|
|
|
|
it('should handle corrupted encrypted data gracefully', async () => {
|
|
const key = 'corrupted-key';
|
|
const value = 'valid-value';
|
|
|
|
// Store valid encrypted data
|
|
await secureStorage.set(key, value);
|
|
|
|
// Corrupt the encrypted data
|
|
const encryptedKey = 'enc_' + key;
|
|
const encrypted = localStorage.getItem(encryptedKey);
|
|
const parsed = JSON.parse(encrypted!);
|
|
parsed.data = 'corrupted-data';
|
|
localStorage.setItem(encryptedKey, JSON.stringify(parsed));
|
|
|
|
// Should return null for corrupted data
|
|
const retrieved = await secureStorage.get(key);
|
|
expect(retrieved).toBeNull();
|
|
});
|
|
});
|
|
});
|