trojanhorse-js
Version:
A comprehensive JavaScript library for fetching, managing, and analyzing global threat intelligence from multiple open-source feeds and security news sources. Unlike its mythological namesake, this Trojan protects your digital fortress.
507 lines (429 loc) • 13.8 kB
text/typescript
/**
* Secure Storage Layer for TrojanHorse.js
*
* Provides encrypted storage using IndexedDB via Dexie
* - Secure storage for threat indicators and API keys
* - Automatic encryption at rest using CryptoJS
* - TTL (Time To Live) support for temporary data
* - Compression for large datasets
* - Transaction support for atomic operations
*/
import Dexie, { Table } from 'dexie';
import CryptoJS from 'crypto-js';
import { CacheEntry, StorageQuota, EncryptedVault, ThreatIndicator, TrojanHorseError } from '../types';
export interface StorageEntry {
id?: number;
key: string;
encryptedData: string;
iv: string;
timestamp: number;
expiresAt?: number;
tags?: string[];
size: number;
}
export interface StorageConfig {
dbName: string;
encryptionKey: string;
maxSizeBytes?: number;
defaultTTL?: number; // Time to live in milliseconds
compressionEnabled?: boolean;
}
export class SecureStorage extends Dexie {
private storage!: Table<StorageEntry>;
private config: StorageConfig;
private isInitialized = false;
constructor(config: StorageConfig) {
super(config.dbName);
this.config = {
maxSizeBytes: 50 * 1024 * 1024, // 50MB default
defaultTTL: 24 * 60 * 60 * 1000, // 24 hours default
compressionEnabled: true,
...config
};
this.version(1).stores({
storage: '++id, key, timestamp, expiresAt, *tags'
});
this.storage = this.table('storage');
}
/**
* Initialize the storage database and perform cleanup
*/
public async initialize(): Promise<void> {
try {
await this.open();
await this.cleanup();
this.isInitialized = true;
} catch (error) {
throw new TrojanHorseError(
`Failed to initialize secure storage: ${error instanceof Error ? error.message : 'Unknown error'}`,
'STORAGE_INIT_FAILED'
);
}
}
/**
* Store encrypted data with optional TTL
*/
public async store(
key: string,
data: any,
options: {
ttl?: number;
tags?: string[];
compress?: boolean;
} = {}
): Promise<void> {
this.ensureInitialized();
try {
// Serialize the data
let serializedData = JSON.stringify(data);
// Optionally compress the data
if (options.compress ?? this.config.compressionEnabled) {
serializedData = this.compress(serializedData);
}
// Generate a random IV for this encryption
const iv = CryptoJS.lib.WordArray.random(16).toString();
// Encrypt the data
const encrypted = CryptoJS.AES.encrypt(serializedData, this.config.encryptionKey, {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
// Calculate expiration time
const ttl = options.ttl ?? this.config.defaultTTL;
const expiresAt = ttl ? Date.now() + ttl : undefined;
// Check storage quota
await this.checkStorageQuota(encrypted.length);
// Store in IndexedDB
const entry = {
key,
encryptedData: encrypted,
iv,
timestamp: Date.now(),
tags: options.tags || [],
size: encrypted.length,
...(expiresAt && { expiresAt })
};
await this.storage.put(entry);
} catch (error) {
throw new TrojanHorseError(
`Failed to store data: ${error instanceof Error ? error.message : 'Unknown error'}`,
'STORAGE_WRITE_FAILED',
500,
{ key }
);
}
}
/**
* Retrieve and decrypt data by key
*/
public async retrieve<T = any>(key: string): Promise<T | null> {
this.ensureInitialized();
try {
const entry = await this.storage.where('key').equals(key).first();
if (!entry) {
return null;
}
// Check if data has expired
if (entry.expiresAt && Date.now() > entry.expiresAt) {
await this.deleteData(key);
return null;
}
// Decrypt the data
const decrypted = CryptoJS.AES.decrypt(entry.encryptedData, this.config.encryptionKey, {
iv: CryptoJS.enc.Utf8.parse(entry.iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);
// Decompress if needed
let decompressedData = decrypted;
if (this.config.compressionEnabled && this.isCompressed(decrypted)) {
decompressedData = this.decompress(decrypted);
}
return JSON.parse(decompressedData);
} catch (error) {
throw new TrojanHorseError(
`Failed to retrieve data: ${error instanceof Error ? error.message : 'Unknown error'}`,
'STORAGE_READ_FAILED',
500,
{ key }
);
}
}
/**
* Delete data by key
*/
public async deleteData(key: string): Promise<boolean> {
this.ensureInitialized();
try {
const deleteCount = await this.storage.where('key').equals(key).delete();
return deleteCount > 0;
} catch (error) {
throw new TrojanHorseError(
`Failed to delete data: ${error instanceof Error ? error.message : 'Unknown error'}`,
'STORAGE_DELETE_FAILED',
500,
{ key }
);
}
}
/**
* Store threat indicators with caching metadata
*/
public async storeThreatIndicators(
indicators: ThreatIndicator[],
source: string,
ttl?: number
): Promise<void> {
const cacheEntry = {
data: indicators,
timestamp: Date.now(),
source,
hash: this.calculateHash(indicators),
...(ttl && { expiresAt: Date.now() + ttl })
} as CacheEntry<ThreatIndicator[]>;
const storeOptions = {
tags: ['threats', source],
compress: true,
...(ttl && { ttl })
};
await this.store(`threats:${source}`, cacheEntry, storeOptions);
}
/**
* Retrieve cached threat indicators
*/
public async getThreatIndicators(source: string): Promise<ThreatIndicator[] | null> {
const cacheEntry = await this.retrieve<CacheEntry<ThreatIndicator[]>>(`threats:${source}`);
if (!cacheEntry || (cacheEntry.expiresAt && Date.now() > cacheEntry.expiresAt)) {
return null;
}
return cacheEntry.data;
}
/**
* Store encrypted vault
*/
public async storeVault(vault: EncryptedVault, key: string = 'default'): Promise<void> {
await this.store(`vault:${key}`, vault, {
tags: ['vault', 'sensitive'],
compress: false // Already encrypted
});
}
/**
* Retrieve encrypted vault
*/
public async getVault(key: string = 'default'): Promise<EncryptedVault | null> {
return await this.retrieve<EncryptedVault>(`vault:${key}`);
}
/**
* Search for entries by tags
*/
public async findByTags(tags: string[]): Promise<StorageEntry[]> {
this.ensureInitialized();
try {
return await this.storage
.where('tags')
.anyOf(tags)
.and(entry => !entry.expiresAt || Date.now() <= entry.expiresAt)
.toArray();
} catch (error) {
throw new TrojanHorseError(
`Failed to search by tags: ${error instanceof Error ? error.message : 'Unknown error'}`,
'STORAGE_SEARCH_FAILED'
);
}
}
/**
* Get storage statistics
*/
public async getStorageStats(): Promise<StorageQuota & {
totalEntries: number;
expiredEntries: number;
tagStats: Record<string, number>;
}> {
this.ensureInitialized();
try {
const allEntries = await this.storage.toArray();
const now = Date.now();
let totalSize = 0;
let expiredEntries = 0;
const tagStats: Record<string, number> = {};
for (const entry of allEntries) {
totalSize += entry.size;
if (entry.expiresAt && now > entry.expiresAt) {
expiredEntries++;
}
if (entry.tags) {
for (const tag of entry.tags) {
tagStats[tag] = (tagStats[tag] || 0) + 1;
}
}
}
const total = this.config.maxSizeBytes || totalSize;
const available = Math.max(0, total - totalSize);
return {
used: totalSize,
available,
total,
percentage: total > 0 ? (totalSize / total) * 100 : 0,
totalEntries: allEntries.length,
expiredEntries,
tagStats
};
} catch (error) {
throw new TrojanHorseError(
`Failed to get storage stats: ${error instanceof Error ? error.message : 'Unknown error'}`,
'STORAGE_STATS_FAILED'
);
}
}
/**
* Clean up expired entries and optimize storage
*/
public async cleanup(): Promise<{ deletedEntries: number; freedBytes: number }> {
this.ensureInitialized();
try {
const now = Date.now();
const expiredEntries = await this.storage
.where('expiresAt')
.below(now)
.toArray();
const freedBytes = expiredEntries.reduce((total, entry) => total + entry.size, 0);
await this.storage
.where('expiresAt')
.below(now)
.delete();
return {
deletedEntries: expiredEntries.length,
freedBytes
};
} catch (error) {
throw new TrojanHorseError(
`Failed to cleanup storage: ${error instanceof Error ? error.message : 'Unknown error'}`,
'STORAGE_CLEANUP_FAILED'
);
}
}
/**
* Clear all data (use with caution!)
*/
public async clear(): Promise<void> {
this.ensureInitialized();
try {
await this.storage.clear();
} catch (error) {
throw new TrojanHorseError(
`Failed to clear storage: ${error instanceof Error ? error.message : 'Unknown error'}`,
'STORAGE_CLEAR_FAILED'
);
}
}
/**
* Create a secure backup of all data
*/
public async createBackup(): Promise<string> {
this.ensureInitialized();
try {
const allData = await this.storage.toArray();
const backup = {
version: '1.0.1',
timestamp: Date.now(),
entries: allData
};
// Encrypt the backup itself
const backupString = JSON.stringify(backup);
const iv = CryptoJS.lib.WordArray.random(16).toString();
const encrypted = CryptoJS.AES.encrypt(backupString, this.config.encryptionKey, {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
return `${iv}:${encrypted}`;
} catch (error) {
throw new TrojanHorseError(
`Failed to create backup: ${error instanceof Error ? error.message : 'Unknown error'}`,
'STORAGE_BACKUP_FAILED'
);
}
}
/**
* Restore from a secure backup
*/
public async restoreBackup(encryptedBackup: string): Promise<void> {
this.ensureInitialized();
try {
const [iv, encrypted] = encryptedBackup.split(':');
if (!encrypted || !iv) {
throw new TrojanHorseError('Invalid encrypted backup format', 'DECRYPTION_ERROR');
}
const decrypted = CryptoJS.AES.decrypt(encrypted, this.config.encryptionKey, {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);
const backup = JSON.parse(decrypted);
// Validate backup format
if (!backup.version || !backup.entries) {
throw new Error('Invalid backup format');
}
// Clear existing data and restore
await this.clear();
await this.storage.bulkAdd(backup.entries);
} catch (error) {
throw new TrojanHorseError(
`Failed to restore backup: ${error instanceof Error ? error.message : 'Unknown error'}`,
'STORAGE_RESTORE_FAILED'
);
}
}
/**
* Private helper methods
*/
private ensureInitialized(): void {
if (!this.isInitialized) {
throw new TrojanHorseError(
'Storage not initialized. Call initialize() first.',
'STORAGE_NOT_INITIALIZED'
);
}
}
private async checkStorageQuota(dataSize: number): Promise<void> {
if (!this.config.maxSizeBytes) {
return;
}
const stats = await this.getStorageStats();
if (stats.used + dataSize > this.config.maxSizeBytes) {
// Try to free up space by cleaning expired entries
await this.cleanup();
const newStats = await this.getStorageStats();
if (newStats.used + dataSize > this.config.maxSizeBytes) {
throw new TrojanHorseError(
'Storage quota exceeded',
'STORAGE_QUOTA_EXCEEDED',
507,
{
required: dataSize,
available: newStats.available,
used: newStats.used,
total: newStats.total
}
);
}
}
}
private compress(data: string): string {
// Simple compression - in production, consider using a proper compression library
return `COMPRESSED:${data}`;
}
private decompress(data: string): string {
if (data.startsWith('COMPRESSED:')) {
return data.substring(11);
}
return data;
}
private isCompressed(data: string): boolean {
return data.startsWith('COMPRESSED:');
}
private calculateHash(data: any): string {
const dataString = JSON.stringify(data);
return CryptoJS.SHA256(dataString).toString();
}
}