/** * CryptoGzipModule — Draft for Dev Agent * * Commands: * gzip → creates .gz * gunzip → restores * encrypt → prompts passphrase → creates .enc * decrypt → prompts passphrase → restores * * Wires into WebX11 module system. Copy into modules/ext/ or modules/tools/. */ class CryptoGzipModule { constructor() { this.commands = {}; this.terminal = null; this._passphraseCache = new Map(); // session-only, never persisted } requirements() { const hasCrypto = !!(window.crypto && crypto.subtle); const hasCompression = !!(window.CompressionStream); const missing = []; if (!hasCrypto) missing.push('Web Crypto API not available (requires secure context)'); if (!hasCompression) missing.push('CompressionStream not available (requires Chromium-based browser or recent Firefox)'); return { met: missing.length === 0, missing }; } getHelpInfo() { return { 'gzip': 'gzip — Compress a VFS file to .gz', 'gunzip': 'gunzip — Decompress a .gz file in VFS', 'encrypt':'encrypt — AES-256-GCM encrypt a VFS file (interactive passphrase)', 'decrypt':'decrypt — Decrypt a .enc file (interactive passphrase)', }; } init(terminal) { this.terminal = terminal; this.commands = { 'gzip': (args) => this._cmdGzip(args), 'gunzip': (args) => this._cmdGunzip(args), 'encrypt':(args) => this._cmdEncrypt(args), 'decrypt':(args) => this._cmdDecrypt(args), }; } // ============================================================ // VFS WIRE POINTS — Dev Agent: replace with real VFS calls // ============================================================ async _vfsRead(path) { // WIRE: return await this.terminal.vfs.readFile(path, 'string'); // OR: return window.webx11_vfs.read(path); throw new Error('_vfsRead not wired'); } async _vfsWrite(path, content) { // WIRE: await this.terminal.vfs.writeFile(path, content); // content is a string (base64 for binary-ish data) throw new Error('_vfsWrite not wired'); } async _vfsExists(path) { // WIRE: return this.terminal.vfs.exists(path); throw new Error('_vfsExists not wired'); } async _vfsDelete(path) { // WIRE: await this.terminal.vfs.delete(path); throw new Error('_vfsDelete not wired'); } // ============================================================ // Prompt Utility // ============================================================ _promptPassphrase(message = 'Enter passphrase: ') { return new Promise((resolve) => { // WIRE: adapt to WebX11 terminal prompt // Option A: if terminal has hidden prompt: // this.terminal.prompt(message, { hidden: true }).then(resolve); // Option B: temporary override: const savedHandler = this.terminal.onCommand; this.terminal.print(message); this.terminal.onCommand = (line) => { this.terminal.onCommand = savedHandler; resolve(line); }; }); } _confirmPrompt(message) { return new Promise((resolve) => { this.terminal.print(message + ' (yes/no): '); const savedHandler = this.terminal.onCommand; this.terminal.onCommand = (line) => { this.terminal.onCommand = savedHandler; resolve(line.trim().toLowerCase() === 'yes'); }; }); } // ============================================================ // GZIP // ============================================================ async _cmdGzip(args) { const path = args[0]; if (!path) throw new Error('Usage: gzip '); const exists = await this._vfsExists(path); if (!exists) throw new Error(`File not found: ${path}`); const outPath = path + '.gz'; if (await this._vfsExists(outPath)) { const ok = await this._confirmPrompt(`Overwrite ${outPath}?`); if (!ok) return 'Cancelled.'; } const content = await this._vfsRead(path); const compressed = await this._gzipString(content); await this._vfsWrite(outPath, compressed); await this._vfsDelete(path); const ratio = (compressed.length / Math.max(content.length, 1)).toFixed(2); return `gzipped: ${path} → ${outPath} (${content.length} → ${compressed.length} chars, ratio ${ratio})`; } async _cmdGunzip(args) { const path = args[0]; if (!path || !path.endsWith('.gz')) throw new Error('Usage: gunzip '); const exists = await this._vfsExists(path); if (!exists) throw new Error(`File not found: ${path}`); const outPath = path.replace(/\.gz$/, ''); if (await this._vfsExists(outPath)) { const ok = await this._confirmPrompt(`Overwrite ${outPath}?`); if (!ok) return 'Cancelled.'; } const compressed = await this._vfsRead(path); const content = await this._gunzipString(compressed); await this._vfsWrite(outPath, content); await this._vfsDelete(path); return `gunzipped: ${path} → ${outPath} (${compressed.length} chars)`; } async _gzipString(str) { const encoder = new TextEncoder(); const bytes = encoder.encode(str); const cs = new CompressionStream('gzip'); const writer = cs.writable.getWriter(); writer.write(bytes); writer.close(); const compressed = await new Response(cs.readable).arrayBuffer(); return this._bytesToBase64(new Uint8Array(compressed)); } async _gunzipString(b64) { const compressed = this._base64ToBytes(b64); const ds = new DecompressionStream('gzip'); const writer = ds.writable.getWriter(); writer.write(compressed); writer.close(); const decompressed = await new Response(ds.readable).arrayBuffer(); return new TextDecoder().decode(decompressed); } // ============================================================ // ENCRYPT / DECRYPT (AES-256-GCM) // ============================================================ async _cmdEncrypt(args) { const path = args[0]; if (!path) throw new Error('Usage: encrypt '); const exists = await this._vfsExists(path); if (!exists) throw new Error(`File not found: ${path}`); const outPath = path + '.enc'; if (await this._vfsExists(outPath)) { const ok = await this._confirmPrompt(`Overwrite ${outPath}?`); if (!ok) return 'Cancelled.'; } const passphrase = await this._promptPassphrase('Enter passphrase: '); if (!passphrase || passphrase.length < 4) throw new Error('Passphrase too short (min 4 chars)'); const plaintext = await this._vfsRead(path); const envelope = await this._encryptString(plaintext, passphrase); await this._vfsWrite(outPath, JSON.stringify(envelope, null, 0)); await this._vfsDelete(path); return `encrypted: ${path} → ${outPath}`; } async _cmdDecrypt(args) { const path = args[0]; if (!path || !path.endsWith('.enc')) throw new Error('Usage: decrypt '); const exists = await this._vfsExists(path); if (!exists) throw new Error(`File not found: ${path}`); const outPath = path.replace(/\.enc$/, ''); if (await this._vfsExists(outPath)) { const ok = await this._confirmPrompt(`Overwrite ${outPath}?`); if (!ok) return 'Cancelled.'; } const passphrase = await this._promptPassphrase('Enter passphrase: '); if (!passphrase) throw new Error('Passphrase required'); const envelopeJson = await this._vfsRead(path); let envelope; try { envelope = JSON.parse(envelopeJson); } catch (e) { throw new Error('Invalid .enc file format (expected JSON envelope)'); } const plaintext = await this._decryptEnvelope(envelope, passphrase); await this._vfsWrite(outPath, plaintext); await this._vfsDelete(path); return `decrypted: ${path} → ${outPath}`; } async _encryptString(plaintext, passphrase) { const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); const key = await this._deriveKey(passphrase, salt); const encoder = new TextEncoder(); const ciphertext = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, encoder.encode(plaintext) ); return { v: 1, // format version alg: 'AES-256-GCM+PBKDF2', salt: this._bytesToBase64(salt), iv: this._bytesToBase64(iv), ct: this._bytesToBase64(new Uint8Array(ciphertext)), }; } async _decryptEnvelope(envelope, passphrase) { if (!envelope || envelope.v !== 1) throw new Error('Unsupported envelope version'); const salt = this._base64ToBytes(envelope.salt); const iv = this._base64ToBytes(envelope.iv); const ct = this._base64ToBytes(envelope.ct); const key = await this._deriveKey(passphrase, salt); try { const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, key, ct ); return new TextDecoder().decode(decrypted); } catch (e) { throw new Error('Decryption failed — wrong passphrase or corrupted file'); } } async _deriveKey(passphrase, salt) { const encoder = new TextEncoder(); const baseKey = await crypto.subtle.importKey( 'raw', encoder.encode(passphrase), { name: 'PBKDF2' }, false, ['deriveKey'] ); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt: salt, iterations: 100000, hash: 'SHA-256', }, baseKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'] ); } // ============================================================ // Base64 Helpers (binary-safe, no deprecated btoa/unescape) // ============================================================ _bytesToBase64(bytes) { // Works in all modern browsers + WebX11 const bin = Array.from(bytes, (b) => String.fromCharCode(b)).join(''); return btoa(bin); } _base64ToBytes(b64) { const bin = atob(b64); return Uint8Array.from(bin, (c) => c.charCodeAt(0)); } } // ================================================================ // EXPORTS — uncomment the appropriate line for your loader: // ================================================================ // window.CryptoGzipModule = CryptoGzipModule; // global registration // export { CryptoGzipModule }; // ES module