import { deriveKey } from "@/encryption/deriveKey"; import { AES256Encryption, BoxEncryption, SecretBoxEncryption, Encryptor, Decryptor } from "./encryptor"; import { encodeHex } from "@/encryption/hex"; import { EncryptionCache } from "./encryptionCache"; import { SessionEncryption } from "./sessionEncryption"; import { MachineEncryption } from "./machineEncryption"; import { encodeBase64, decodeBase64 } from "@/encryption/base64"; import sodium from '@/encryption/libsodium.lib'; import { decryptBox, encryptBox } from "@/encryption/libsodium"; import { randomUUID } from 'expo-crypto'; export class Encryption { static async create(masterSecret: Uint8Array) { // Derive content data key to open session and machine records const contentDataKey = await deriveKey(masterSecret, 'Happy EnCoder', ['content']); // Derive content data key keypair const contentKeyPair = sodium.crypto_box_seed_keypair(contentDataKey); // Derive anonymous ID const anonID = encodeHex((await deriveKey(masterSecret, 'Happy Coder', ['analytics', 'id']))).slice(0, 16).toLowerCase(); // Derive master blob key for legacy sessions (those with no per-session dataKey) const masterBlobKey = await deriveKey(masterSecret, 'Happy Blobs', ['master']); // Create encryption return new Encryption(anonID, masterSecret, contentKeyPair, masterBlobKey); } private readonly legacyEncryption: SecretBoxEncryption; private readonly contentKeyPair: sodium.KeyPair; private readonly masterBlobKey: Uint8Array; readonly anonID: string; readonly contentDataKey: Uint8Array; // Session and machine encryption management private sessionEncryptions = new Map(); private machineEncryptions = new Map(); private sessionBlobKeys = new Map(); private cache: EncryptionCache; private constructor(anonID: string, masterSecret: Uint8Array, contentKeyPair: sodium.KeyPair, masterBlobKey: Uint8Array) { this.anonID = anonID; this.contentKeyPair = contentKeyPair; this.legacyEncryption = new SecretBoxEncryption(masterSecret); this.masterBlobKey = masterBlobKey; this.cache = new EncryptionCache(); this.contentDataKey = contentKeyPair.publicKey; } // // Core encryption opening // async openEncryption(dataEncryptionKey: Uint8Array | null): Promise { if (!dataEncryptionKey) { return this.legacyEncryption; } return new AES256Encryption(dataEncryptionKey); } // // Session operations // /** * Initialize sessions with their encryption keys * This should be called once when sessions are loaded */ async initializeSessions(sessions: Map): Promise { for (const [sessionId, dataKey] of sessions) { // Skip if already initialized if (this.sessionEncryptions.has(sessionId)) { continue; } // Create appropriate encryptor based on data key const encryptor = await this.openEncryption(dataKey); // Create and cache session encryption const sessionEnc = new SessionEncryption( sessionId, encryptor, this.cache ); this.sessionEncryptions.set(sessionId, sessionEnc); // Derive blob key for this session. // Legacy sessions (null dataKey) use the master blob key. // Newer sessions derive a subkey from their own dataKey so blobs // are cryptographically isolated from message encryption. const blobKey = dataKey ? await deriveKey(dataKey, 'Happy Blobs', ['session']) : this.masterBlobKey; this.sessionBlobKeys.set(sessionId, blobKey); } } /** * Get session encryption if it has been initialized * Returns null if not initialized (should never happen in normal flow) */ getSessionEncryption(sessionId: string): SessionEncryption | null { return this.sessionEncryptions.get(sessionId) || null; } /** * Remove session encryption from memory when session is deleted */ removeSessionEncryption(sessionId: string): void { this.sessionEncryptions.delete(sessionId); this.sessionBlobKeys.delete(sessionId); // Also clear any cached data for this session this.cache.clearSessionCache(sessionId); } /** * Get the 32-byte NaCl secretbox key for encrypting binary blobs * (image attachments) in a session. Distinct from the message encryption * key to maintain cryptographic separation. * Returns null if the session has not been initialized. */ getSessionBlobKey(sessionId: string): Uint8Array | null { return this.sessionBlobKeys.get(sessionId) ?? null; } // // Machine operations // /** * Initialize machines with their encryption keys * This should be called once when machines are loaded */ async initializeMachines(machines: Map): Promise { for (const [machineId, dataKey] of machines) { // Skip if already initialized if (this.machineEncryptions.has(machineId)) { continue; } // Create appropriate encryptor based on data key const encryptor = await this.openEncryption(dataKey); // Create and cache machine encryption const machineEnc = new MachineEncryption( machineId, encryptor, this.cache ); this.machineEncryptions.set(machineId, machineEnc); } } /** * Get machine encryption if it has been initialized * Returns null if not initialized (should never happen in normal flow) */ getMachineEncryption(machineId: string): MachineEncryption | null { return this.machineEncryptions.get(machineId) || null; } /** * Remove machine encryption from memory when the machine is deleted */ removeMachineEncryption(machineId: string): void { this.machineEncryptions.delete(machineId); } // // Legacy methods for machine metadata (temporary until machines are migrated) // async encryptRaw(data: any): Promise { const encrypted = await this.legacyEncryption.encrypt([data]); return encodeBase64(encrypted[0], 'base64'); } async decryptRaw(encrypted: string): Promise { try { const encryptedData = decodeBase64(encrypted, 'base64'); const decrypted = await this.legacyEncryption.decrypt([encryptedData]); return decrypted[0] || null; } catch (error) { return null; } } // // Data Encryption Key decryption // async decryptEncryptionKey(encrypted: string) { // Never throw: callers (fetchMachines/fetchSessions/artifacts) iterate // many keys, and an exception on one malformed/foreign key would // reject the whole sync and silently drop every item. Always degrade // to null so the caller can decide per-item. try { const encryptedKey = decodeBase64(encrypted, 'base64'); if (encryptedKey[0] !== 0) { return null; } const decrypted = decryptBox(encryptedKey.slice(1), this.contentKeyPair.privateKey); if (!decrypted) { return null; } return decrypted; } catch (error) { console.error('decryptEncryptionKey failed:', error); return null; } } async encryptEncryptionKey(key: Uint8Array): Promise { // Use public key for encryption (encrypt TO ourselves) const encrypted = encryptBox(key, this.contentKeyPair.publicKey); const result = new Uint8Array(encrypted.length + 1); result[0] = 0; // Version byte result.set(encrypted, 1); return result; } generateId(): string { return randomUUID(); } }