Files
ERitors-Scribe-Desktop/electron/database/encryption.ts
natreex d5eb1691d9 Add database schema, encryption utilities, and local database service
- Implement `schema.ts` for SQLite schema creation, indexing, and sync metadata initialization.
- Develop `encryption.ts` with AES-256-GCM encryption utilities for securing database data.
- Add `database.service.ts` to manage CRUD operations with encryption support, user-specific databases, and schema initialization.
- Integrate book, chapter, and character operations with encrypted content handling and sync preparation.
2025-11-17 09:34:54 -05:00

138 lines
4.3 KiB
TypeScript

import crypto from 'crypto';
/**
* Encryption utilities using AES-256-GCM for local database encryption
* Each user has a unique encryption key derived from their userId and a master secret
*/
const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32; // 256 bits
const IV_LENGTH = 16; // 128 bits
const SALT_LENGTH = 64;
const TAG_LENGTH = 16;
export interface EncryptedData {
encryptedData: string;
iv: string;
authTag: string;
}
/**
* Generate a unique encryption key for a user
* This key is generated once at first login and stored securely in electron-store
* @param userId - The user's unique identifier
* @returns Base64 encoded encryption key
*/
export function generateUserEncryptionKey(userId: string): string {
// Generate a random salt for this user
const salt = crypto.randomBytes(SALT_LENGTH);
// Create a deterministic key based on userId and random salt
// This ensures each user has a unique, strong key
const key = crypto.pbkdf2Sync(
userId,
salt,
100000, // iterations
KEY_LENGTH,
'sha512'
);
// Combine salt and key for storage
const combined = Buffer.concat([salt, key]);
return combined.toString('base64');
}
/**
* Extract the actual encryption key from the stored combined salt+key
* @param storedKey - Base64 encoded salt+key combination
* @returns Encryption key buffer
*/
function extractKeyFromStored(storedKey: string): Buffer {
const combined = Buffer.from(storedKey, 'base64');
// Extract key (last KEY_LENGTH bytes)
return combined.subarray(SALT_LENGTH, SALT_LENGTH + KEY_LENGTH);
}
/**
* Encrypt sensitive data using AES-256-GCM
* @param data - Plain text data to encrypt
* @param userKey - User's encryption key (base64)
* @returns Encrypted data with IV and auth tag
*/
export function encrypt(data: string, userKey: string): EncryptedData {
try {
const key = extractKeyFromStored(userKey);
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encryptedData: encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
} catch (error) {
throw new Error(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Decrypt data encrypted with AES-256-GCM
* @param encryptedData - Encrypted data object
* @param userKey - User's encryption key (base64)
* @returns Decrypted plain text
*/
export function decrypt(encryptedData: EncryptedData, userKey: string): string {
try {
const key = extractKeyFromStored(userKey);
const iv = Buffer.from(encryptedData.iv, 'hex');
const authTag = Buffer.from(encryptedData.authTag, 'hex');
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData.encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Encrypt an object by converting it to JSON first
* @param obj - Object to encrypt
* @param userKey - User's encryption key
* @returns Encrypted data
*/
export function encryptObject<T>(obj: T, userKey: string): EncryptedData {
const jsonString = JSON.stringify(obj);
return encrypt(jsonString, userKey);
}
/**
* Decrypt and parse an encrypted object
* @param encryptedData - Encrypted data object
* @param userKey - User's encryption key
* @returns Decrypted and parsed object
*/
export function decryptObject<T>(encryptedData: EncryptedData, userKey: string): T {
const decrypted = decrypt(encryptedData, userKey);
return JSON.parse(decrypted) as T;
}
/**
* Hash data using SHA-256 (for non-reversible hashing like titles)
* @param data - Data to hash
* @returns Hex encoded hash
*/
export function hash(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex');
}