/** * EncryptionService - Zero-Knowledge Encryption for Cloud Sync * * Implements AES-256-GCM encryption with PBKDF2 key derivation. * All encryption/decryption happens client-side; cloud providers never see plaintext. * * Security Model: * - Master password → PBKDF2 (600k iterations) → AES-256 key * - Each sync file has unique IV and salt * - Key verification via hash comparison (not by storing the key) */ import { SYNC_CONSTANTS, type EncryptionResult, type DecryptionInput, type MasterKeyConfig, type UnlockedMasterKey, type SyncedFile, type SyncFileMeta, type SyncPayload, } from '../../domain/sync'; import { validateConvergentSyncPayload } from '../../domain/convergentSync'; // ============================================================================ // Utility Functions // ============================================================================ /** * Convert Uint8Array to ArrayBuffer for Web Crypto API compatibility * TypeScript 5.x requires explicit conversion from Uint8Array to BufferSource */ const toArrayBuffer = (bytes: Uint8Array): ArrayBuffer => { return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; }; /** * Convert ArrayBuffer to Base64 string */ export const arrayBufferToBase64 = (buffer: ArrayBuffer | Uint8Array): string => { const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); }; /** * Convert Base64 string to Uint8Array */ export const base64ToUint8Array = (base64: string): Uint8Array => { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; }; /** * Generate cryptographically secure random bytes */ export const generateRandomBytes = (length: number): Uint8Array => { return crypto.getRandomValues(new Uint8Array(length)); }; /** * Compute SHA-256 hash of data */ export const sha256 = async (data: Uint8Array): Promise => { const hashBuffer = await crypto.subtle.digest('SHA-256', toArrayBuffer(data)); return new Uint8Array(hashBuffer); }; /** * Convert string to Uint8Array using UTF-8 encoding */ const stringToBytes = (str: string): Uint8Array => { return new TextEncoder().encode(str); }; /** * Convert Uint8Array to string using UTF-8 decoding */ const bytesToString = (bytes: Uint8Array): string => { return new TextDecoder().decode(bytes); }; // ============================================================================ // Key Derivation // ============================================================================ /** * Derive an AES-256 key from password using PBKDF2 * * @param password - User's master password * @param salt - Random salt (32 bytes recommended) * @param iterations - PBKDF2 iterations (600000 recommended) * @returns CryptoKey suitable for AES-256-GCM operations */ export const deriveKey = async ( password: string, salt: Uint8Array, iterations: number = SYNC_CONSTANTS.PBKDF2_ITERATIONS ): Promise => { // Import password as key material const passwordKey = await crypto.subtle.importKey( 'raw', toArrayBuffer(stringToBytes(password)), 'PBKDF2', false, ['deriveBits', 'deriveKey'] ); // Derive AES key using PBKDF2 const derivedKey = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt: toArrayBuffer(salt), iterations: iterations, hash: SYNC_CONSTANTS.PBKDF2_HASH, }, passwordKey, { name: 'AES-GCM', length: SYNC_CONSTANTS.AES_KEY_LENGTH, }, true, // extractable for verification ['encrypt', 'decrypt'] ); return derivedKey; }; /** * Export CryptoKey to raw bytes for verification purposes */ export const exportKey = async (key: CryptoKey): Promise => { const exported = await crypto.subtle.exportKey('raw', key); return new Uint8Array(exported); }; /** * Create a verification hash from derived key * Used to verify correct password without storing the key */ export const createVerificationHash = async (derivedKey: CryptoKey): Promise => { const keyBytes = await exportKey(derivedKey); const hash = await sha256(keyBytes); return arrayBufferToBase64(hash); }; /** * Verify that a password produces the expected verification hash */ export const verifyPassword = async ( password: string, config: MasterKeyConfig ): Promise => { try { const salt = base64ToUint8Array(config.salt); const derivedKey = await deriveKey( password, salt, config.kdfIterations || SYNC_CONSTANTS.PBKDF2_ITERATIONS ); const hash = await createVerificationHash(derivedKey); return hash === config.verificationHash; } catch { return false; } }; // ============================================================================ // Encryption / Decryption // ============================================================================ /** * Encrypt plaintext using AES-256-GCM * * @param plaintext - Data to encrypt (as string) * @param key - AES-256 CryptoKey * @param salt - Salt used for key derivation (stored in result) * @returns Encrypted data with IV */ export const encrypt = async ( plaintext: string, key: CryptoKey, salt: Uint8Array ): Promise => { // Generate random IV const iv = generateRandomBytes(SYNC_CONSTANTS.GCM_IV_LENGTH); // Encrypt const ciphertextBuffer = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: toArrayBuffer(iv), tagLength: SYNC_CONSTANTS.GCM_TAG_LENGTH, }, key, toArrayBuffer(stringToBytes(plaintext)) ); return { ciphertext: new Uint8Array(ciphertextBuffer), iv: iv, salt: salt, algorithm: 'AES-256-GCM', kdf: 'PBKDF2', kdfIterations: SYNC_CONSTANTS.PBKDF2_ITERATIONS, }; }; /** * Decrypt ciphertext using AES-256-GCM * * @param input - Encrypted data with IV * @param key - AES-256 CryptoKey * @returns Decrypted plaintext */ export const decrypt = async ( input: DecryptionInput, key: CryptoKey ): Promise => { const plaintextBuffer = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: toArrayBuffer(input.iv), tagLength: SYNC_CONSTANTS.GCM_TAG_LENGTH, }, key, toArrayBuffer(input.ciphertext) ); return bytesToString(new Uint8Array(plaintextBuffer)); }; // ============================================================================ // High-Level Encryption API // ============================================================================ /** * Encrypt a sync payload to create a SyncedFile * * @param payload - Data to encrypt * @param password - Master password * @param deviceId - Device identifier * @param appVersion - App version string * @returns Complete SyncedFile ready for upload */ export const encryptPayload = async ( payload: SyncPayload, password: string, deviceId: string, deviceName: string, appVersion: string, existingVersion?: number ): Promise => { const syncSchemaVersion = payload.convergentSync?.schemaVersion; if (syncSchemaVersion !== undefined) { validateConvergentSyncPayload( { syncSchemaVersion }, payload, ); } // Generate new salt for each encryption const salt = generateRandomBytes(SYNC_CONSTANTS.SALT_LENGTH); // Derive key from password const key = await deriveKey(password, salt); // Encrypt the payload const plaintext = JSON.stringify(payload); const encrypted = await encrypt(plaintext, key, salt); // Create metadata const meta: SyncFileMeta = { version: (existingVersion || 0) + 1, updatedAt: Date.now(), deviceId: deviceId, deviceName: deviceName, appVersion: appVersion, iv: arrayBufferToBase64(encrypted.iv), salt: arrayBufferToBase64(encrypted.salt), algorithm: 'AES-256-GCM', kdf: 'PBKDF2', kdfIterations: SYNC_CONSTANTS.PBKDF2_ITERATIONS, ...(syncSchemaVersion ? { syncSchemaVersion } : {}), }; return { meta, payload: arrayBufferToBase64(encrypted.ciphertext), }; }; /** * Decrypt a SyncedFile to retrieve the payload * * @param syncedFile - Encrypted file from cloud * @param password - Master password * @returns Decrypted payload */ export const decryptPayload = async ( syncedFile: SyncedFile, password: string ): Promise => { const { meta, payload } = syncedFile; // Decode Base64 values const salt = base64ToUint8Array(meta.salt); const iv = base64ToUint8Array(meta.iv); const ciphertext = base64ToUint8Array(payload); // Derive key from password const key = await deriveKey( password, salt, meta.kdfIterations || SYNC_CONSTANTS.PBKDF2_ITERATIONS ); // Decrypt const decrypted = await decrypt( { ciphertext, iv, salt, kdf: meta.kdf, kdfIterations: meta.kdfIterations }, key ); const parsed = JSON.parse(decrypted) as SyncPayload; validateConvergentSyncPayload(meta, parsed); return parsed; }; /** * Verify a SyncedFile can be decrypted with given password * Does not return the payload, just validates the password */ export const verifySyncedFile = async ( syncedFile: SyncedFile, password: string ): Promise => { try { await decryptPayload(syncedFile, password); return true; } catch { return false; } }; // ============================================================================ // Master Key Management // ============================================================================ /** * Create a new master key configuration * * @param password - User's master password * @returns Configuration to store (contains verification hash, not the key) */ export const createMasterKeyConfig = async ( password: string ): Promise => { const salt = generateRandomBytes(SYNC_CONSTANTS.SALT_LENGTH); const key = await deriveKey(password, salt); const verificationHash = await createVerificationHash(key); return { verificationHash, salt: arrayBufferToBase64(salt), kdf: 'PBKDF2', kdfIterations: SYNC_CONSTANTS.PBKDF2_ITERATIONS, createdAt: Date.now(), }; }; /** * Unlock the master key and return it for use * * @param password - User's master password * @param config - Stored master key configuration * @returns Unlocked key state (keep in memory only!) */ export const unlockMasterKey = async ( password: string, config: MasterKeyConfig ): Promise => { const isValid = await verifyPassword(password, config); if (!isValid) return null; const salt = base64ToUint8Array(config.salt); const derivedKey = await deriveKey( password, salt, config.kdfIterations || SYNC_CONSTANTS.PBKDF2_ITERATIONS ); return { derivedKey, salt, unlockedAt: Date.now(), }; }; /** * Change master password * Requires re-encrypting all synced data with new password * * @param oldPassword - Current master password * @param newPassword - New master password * @param config - Current master key configuration * @returns New configuration, or null if old password is wrong */ export const changeMasterPassword = async ( oldPassword: string, newPassword: string, config: MasterKeyConfig ): Promise => { // Verify old password first const isValid = await verifyPassword(oldPassword, config); if (!isValid) return null; // Create new configuration with new password return createMasterKeyConfig(newPassword); }; // ============================================================================ // Export Service Class // ============================================================================ /** * EncryptionService class - stateless encryption operations */ export class EncryptionService { static deriveKey = deriveKey; static encrypt = encrypt; static decrypt = decrypt; static encryptPayload = encryptPayload; static decryptPayload = decryptPayload; static createMasterKeyConfig = createMasterKeyConfig; static unlockMasterKey = unlockMasterKey; static changeMasterPassword = changeMasterPassword; static verifyPassword = verifyPassword; static createVerificationHash = createVerificationHash; static generateRandomBytes = generateRandomBytes; static arrayBufferToBase64 = arrayBufferToBase64; static base64ToUint8Array = base64ToUint8Array; } export default EncryptionService;