class CryptoGzipModule { // CryptoGzipModule — VFS-level gzip/encrypt commands for WebX11 newOS4 // Commands: gzip , gunzip , encrypt , decrypt constructor() { this.commands = {}; this.terminal = null; } getHelpInfo() { return { 'gzip': 'gzip — Compress a VFS file to .gz', 'gunzip': 'gunzip — Decompress a .gz file', 'encrypt': 'encrypt — AES-256-GCM encrypt a VFS file', 'decrypt': 'decrypt — Decrypt a .enc file' }; } _vfsExists(path) { // readFile returns null if not found const normalized = path.replace(/^\/+/, ''); return this.terminal.fs.readFile(normalized) !== null; } _vfsRead(path) { const normalized = path.replace(/^\/+/, ''); const content = this.terminal.fs.readFile(normalized); if (content === null) throw new Error(`File not found: ${path}`); return content; } _vfsWrite(path, content) { const normalized = path.replace(/^\/+/, ''); return this.terminal.fs.writeFile(normalized, content); } _vfsDelete(path) { const normalized = path.replace(/^\/+/, ''); this.terminal.fs.deleteFile(normalized); } // ============================================================ // Prompt Utilities (WebX11 terminal compatible) // ============================================================ _promptPassphrase(message = 'Enter passphrase: ') { return new Promise((resolve) => { this.terminal.print(message, 'info'); const savedHandler = this.terminal.inputCallback || this.terminal._onCommand; const handler = (line) => { if (this.terminal.inputCallback === handler) this.terminal.inputCallback = savedHandler; if (this.terminal._onCommand === handler) this.terminal._onCommand = savedHandler; resolve(line); }; this.terminal.inputCallback = handler; this.terminal._onCommand = handler; }); } _confirmPrompt(message) { return new Promise((resolve) => { this.terminal.print(message + ' (yes/no): ', 'warning'); const savedHandler = this.terminal.inputCallback || this.terminal._onCommand; const handler = (line) => { if (this.terminal.inputCallback === handler) this.terminal.inputCallback = savedHandler; if (this.terminal._onCommand === handler) this.terminal._onCommand = savedHandler; resolve(line.trim().toLowerCase() === 'yes'); }; this.terminal.inputCallback = handler; this.terminal._onCommand = handler; }); } // ============================================================ // GZIP // ============================================================ async _cmdGzip(args) { const path = args[0]; if (!path) { this.terminal.print('Usage: gzip — Compress a VFS file to .gz', 'warning'); return; } if (!this._vfsExists(path)) { this.terminal.print('File not found: ' + path, 'error'); return; } const outPath = path + '.gz'; if (this._vfsExists(outPath)) { const ok = await this._confirmPrompt(`Overwrite ${outPath}?`); if (!ok) { this.terminal.print('Cancelled.', 'info'); return; } } try { const content = this._vfsRead(path); const compressed = await this._gzipString(content); this._vfsWrite(outPath, compressed); this._vfsDelete(path); const ratio = (compressed.length / Math.max(content.length, 1)).toFixed(2); this.terminal.print(`gzipped: ${path} → ${outPath} (${content.length} → ${compressed.length} chars, ratio ${ratio})`, 'success'); } catch (e) { this.terminal.print('gzip error: ' + e.message, 'error'); } } async _cmdGunzip(args) { const path = args[0]; if (!path || !path.endsWith('.gz')) { this.terminal.print('Usage: gunzip — Decompress a .gz file in VFS', 'warning'); return; } if (!this._vfsExists(path)) { this.terminal.print('File not found: ' + path, 'error'); return; } const outPath = path.replace(/\.gz$/, ''); if (this._vfsExists(outPath)) { const ok = await this._confirmPrompt(`Overwrite ${outPath}?`); if (!ok) { this.terminal.print('Cancelled.', 'info'); return; } } try { const compressed = this._vfsRead(path); const content = await this._gunzipString(compressed); this._vfsWrite(outPath, content); this._vfsDelete(path); this.terminal.print(`gunzipped: ${path} → ${outPath} (${compressed.length} → ${content.length} chars)`, 'success'); } catch (e) { this.terminal.print('gunzip error: ' + e.message, 'error'); } } 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]; const passphrase = args[1]; if (!path || !passphrase) { this.terminal.print('Usage: encrypt — AES-256-GCM encrypt a VFS file', 'warning'); return; } if (!this._vfsExists(path)) { this.terminal.print('File not found: ' + path, 'error'); return; } if (passphrase.length < 4) { this.terminal.print('Passphrase too short (min 4 chars)', 'error'); return; } const outPath = path + '.enc'; if (this._vfsExists(outPath)) { this.terminal.print(`Overwriting existing ${outPath}`, 'warning'); } try { const plaintext = this._vfsRead(path); const envelope = await this._encryptString(plaintext, passphrase); this._vfsWrite(outPath, JSON.stringify(envelope, null, 0)); this._vfsDelete(path); this.terminal.print(`encrypted: ${path} → ${outPath}`, 'success'); } catch (e) { this.terminal.print('encrypt error: ' + e.message, 'error'); } } async _cmdDecrypt(args) { const path = args[0]; const passphrase = args[1]; if (!path || !path.endsWith('.enc') || !passphrase) { this.terminal.print('Usage: decrypt — Decrypt a .enc file', 'warning'); return; } if (!this._vfsExists(path)) { this.terminal.print('File not found: ' + path, 'error'); return; } const outPath = path.replace(/\.enc$/, ''); if (this._vfsExists(outPath)) { this.terminal.print(`Overwriting existing ${outPath}`, 'warning'); } try { const envelopeJson = 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); this._vfsWrite(outPath, plaintext); this._vfsDelete(path); this.terminal.print(`decrypted: ${path} → ${outPath}`, 'success'); } catch (e) { this.terminal.print('decrypt error: ' + e.message, 'error'); } } 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, 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 // ============================================================ _bytesToBase64(bytes) { 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)); } // ============================================================ registerCommands() { const self = this; this.commands['gzip'] = async (args) => self._cmdGzip(args); this.commands['gunzip'] = async (args) => self._cmdGunzip(args); this.commands['encrypt'] = async (args) => self._cmdEncrypt(args); this.commands['decrypt'] = async (args) => self._cmdDecrypt(args); this.commandsMeta = { gzip: { argType: 'file', description: 'Compress a VFS file to .gz' }, gunzip: { argType: 'file', description: 'Decompress a .gz file' }, encrypt: { argType: 'file', description: 'Encrypt a file with passphrase' }, decrypt: { argType: 'file', description: 'Decrypt a .enc file' } }; } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new CryptoGzipModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/cryptogzip.js'); }