|
|
|
@@ -0,0 +1,220 @@
|
|
|
|
|
import type { SecurityCore } from '@core/security';
|
|
|
|
|
import { DatabaseProvider } from './DatabaseProvider';
|
|
|
|
|
import { SchemaRegistry } from './SchemaRegistry';
|
|
|
|
|
import type { StorageConfig, CollectionSchema, DatabaseInstance } from './types';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Core Storage SDK
|
|
|
|
|
* Provides encrypted local database management with RxDB/SQLCipher
|
|
|
|
|
*
|
|
|
|
|
* Key Features (per FRD):
|
|
|
|
|
* - F.STC.001: Abstraction layer over RxDB/SQLite with SQLCipher encryption
|
|
|
|
|
* - F.STC.002: Schema registration for feature SDKs
|
|
|
|
|
* - F.STC.003: Conflict resolution strategies (CRDT configuration, LWW handling)
|
|
|
|
|
* - F.STC.004: Encrypted file vault for local media storage
|
|
|
|
|
* - F.STC.005: Automated database schema migration
|
|
|
|
|
* - F.STC.006: Incremental encryption re-keying
|
|
|
|
|
* - F.STC.007: Selective purge mechanisms
|
|
|
|
|
*/
|
|
|
|
|
export class StorageCore {
|
|
|
|
|
private config: StorageConfig;
|
|
|
|
|
private securityCore: SecurityCore;
|
|
|
|
|
private databaseProvider: DatabaseProvider;
|
|
|
|
|
private schemaRegistry: SchemaRegistry;
|
|
|
|
|
private initialized = false;
|
|
|
|
|
|
|
|
|
|
constructor(config: StorageConfig, securityCore: SecurityCore) {
|
|
|
|
|
this.config = config;
|
|
|
|
|
this.securityCore = securityCore;
|
|
|
|
|
this.databaseProvider = new DatabaseProvider(config, securityCore);
|
|
|
|
|
this.schemaRegistry = new SchemaRegistry();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* F.STC.001: Initialize the storage layer with encryption
|
|
|
|
|
*/
|
|
|
|
|
async initialize(): Promise<void> {
|
|
|
|
|
if (this.initialized) return;
|
|
|
|
|
|
|
|
|
|
await this.databaseProvider.initialize();
|
|
|
|
|
this.initialized = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* F.STC.002: Register schema for feature SDKs
|
|
|
|
|
*/
|
|
|
|
|
registerSchema(collectionName: string, schema: CollectionSchema): void {
|
|
|
|
|
this.schemaRegistry.register(collectionName, schema);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the database instance
|
|
|
|
|
*/
|
|
|
|
|
getDatabase(): DatabaseInstance {
|
|
|
|
|
this.ensureInitialized();
|
|
|
|
|
return this.databaseProvider.getInstance();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* F.STC.002: Create collection with registered schema
|
|
|
|
|
*/
|
|
|
|
|
async createCollection(name: string): Promise<void> {
|
|
|
|
|
this.ensureInitialized();
|
|
|
|
|
|
|
|
|
|
const schema = this.schemaRegistry.getSchema(name);
|
|
|
|
|
if (!schema) {
|
|
|
|
|
throw new Error(`Schema not registered for collection: ${name}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.databaseProvider.createCollection(name, schema);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* F.STC.004: Store encrypted file
|
|
|
|
|
*/
|
|
|
|
|
async storeEncryptedFile(
|
|
|
|
|
filename: string,
|
|
|
|
|
data: Buffer,
|
|
|
|
|
mimeType: string,
|
|
|
|
|
orgId?: string
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
this.ensureInitialized();
|
|
|
|
|
|
|
|
|
|
// Encrypt file content
|
|
|
|
|
const encryptResult = orgId
|
|
|
|
|
? await this.securityCore.encryptForOrganization(data.toString('base64'), orgId)
|
|
|
|
|
: await this.encryptFileLocally(data);
|
|
|
|
|
|
|
|
|
|
// Store encrypted file to filesystem
|
|
|
|
|
const encryptedPath = await this.writeEncryptedFile(filename, encryptResult.encryptedData);
|
|
|
|
|
|
|
|
|
|
// Store metadata in database
|
|
|
|
|
const fileId = this.generateFileId();
|
|
|
|
|
const metadata = {
|
|
|
|
|
id: fileId,
|
|
|
|
|
filename,
|
|
|
|
|
mimeType,
|
|
|
|
|
size: data.length,
|
|
|
|
|
encryptedPath,
|
|
|
|
|
keyReference: JSON.stringify(encryptResult.keyReference),
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
updatedAt: new Date().toISOString()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const db = this.getDatabase();
|
|
|
|
|
await db.collections.fileVault.insert(metadata);
|
|
|
|
|
|
|
|
|
|
return fileId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* F.STC.004: Retrieve encrypted file
|
|
|
|
|
*/
|
|
|
|
|
async retrieveEncryptedFile(fileId: string, orgId?: string): Promise<Buffer> {
|
|
|
|
|
this.ensureInitialized();
|
|
|
|
|
|
|
|
|
|
const db = this.getDatabase();
|
|
|
|
|
const metadata = await db.collections.fileVault.findOne(fileId).exec();
|
|
|
|
|
|
|
|
|
|
if (!metadata) {
|
|
|
|
|
throw new Error(`File not found: ${fileId}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read encrypted file
|
|
|
|
|
const encryptedData = await this.readEncryptedFile(metadata.encryptedPath);
|
|
|
|
|
const keyReference = JSON.parse(metadata.keyReference);
|
|
|
|
|
|
|
|
|
|
// Decrypt file content
|
|
|
|
|
const decryptedData = orgId
|
|
|
|
|
? await this.securityCore.decryptForOrganization(encryptedData, keyReference)
|
|
|
|
|
: await this.decryptFileLocally(encryptedData, keyReference);
|
|
|
|
|
|
|
|
|
|
return Buffer.from(decryptedData, 'base64');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* F.STC.007: Selective purge by organization
|
|
|
|
|
*/
|
|
|
|
|
async purgeOrganizationData(orgId: string): Promise<void> {
|
|
|
|
|
this.ensureInitialized();
|
|
|
|
|
|
|
|
|
|
const db = this.getDatabase();
|
|
|
|
|
|
|
|
|
|
// Find all collections and purge org-specific data
|
|
|
|
|
for (const [collectionName, collection] of Object.entries(db.collections)) {
|
|
|
|
|
// Remove documents with matching orgId
|
|
|
|
|
await collection.find({ orgId }).remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Purge encrypted files belonging to organization
|
|
|
|
|
const orgFiles = await db.collections.fileVault
|
|
|
|
|
.find()
|
|
|
|
|
.where('keyReference')
|
|
|
|
|
.regex(new RegExp(orgId))
|
|
|
|
|
.exec();
|
|
|
|
|
|
|
|
|
|
for (const file of orgFiles) {
|
|
|
|
|
await this.deleteEncryptedFile(file.encryptedPath);
|
|
|
|
|
await file.remove();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* F.STC.006: Re-encrypt data with new keys
|
|
|
|
|
*/
|
|
|
|
|
async reEncryptData(orgId: string, newKeyReference: any): Promise<void> {
|
|
|
|
|
this.ensureInitialized();
|
|
|
|
|
|
|
|
|
|
// This would implement re-encryption of existing data
|
|
|
|
|
// when organizational keys are rotated
|
|
|
|
|
console.warn('Re-encryption implementation needed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get sync status for collections
|
|
|
|
|
*/
|
|
|
|
|
getSyncStatus(collectionName: string): any {
|
|
|
|
|
this.ensureInitialized();
|
|
|
|
|
|
|
|
|
|
const db = this.getDatabase();
|
|
|
|
|
return db.collections[collectionName]?.find({ syncStatus: 'PENDING' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async encryptFileLocally(data: Buffer): Promise<any> {
|
|
|
|
|
// Implementation would use local encryption for non-org files
|
|
|
|
|
throw new Error('Local file encryption not implemented');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async decryptFileLocally(encryptedData: string, keyReference: any): Promise<string> {
|
|
|
|
|
// Implementation would use local decryption for non-org files
|
|
|
|
|
throw new Error('Local file decryption not implemented');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async writeEncryptedFile(filename: string, encryptedData: string): Promise<string> {
|
|
|
|
|
// Implementation would write to secure app documents directory
|
|
|
|
|
const path = `/secure/${this.generateFileId()}_${filename}`;
|
|
|
|
|
console.warn('File system integration needed');
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async readEncryptedFile(path: string): Promise<string> {
|
|
|
|
|
// Implementation would read from secure app documents directory
|
|
|
|
|
console.warn('File system integration needed');
|
|
|
|
|
return 'mock-encrypted-data';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async deleteEncryptedFile(path: string): Promise<void> {
|
|
|
|
|
// Implementation would securely delete file
|
|
|
|
|
console.warn('Secure file deletion needed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private generateFileId(): string {
|
|
|
|
|
return `file_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ensureInitialized(): void {
|
|
|
|
|
if (!this.initialized) {
|
|
|
|
|
throw new Error('StorageCore must be initialized before use');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|