class CryptoGzipModule { // CryptoGzipModule — VFS-level gzip/encrypt/tar commands for WebX11 newOS4 // Commands: gzip , gunzip , encrypt , decrypt , // tar , untar [dest-dir] constructor() { this.commands = {}; this.terminal = null; } getHelpInfo() { return { 'gzip': 'gzip — Compress a VFS file to .gz', 'gunzip': 'gunzip — Decompress a .gz file (binary-safe)', 'encrypt': 'encrypt — AES-256-GCM encrypt a file', 'decrypt': 'decrypt — Decrypt a .enc file', 'tar': 'tar — Bundle a VFS directory into a tar archive (gzip if .gz)', 'untar': 'untar [dest-dir] — Extract a tar archive into VFS (auto-detects gzip)', 'wget': 'wget [filename] — Download a URL (via /api/fetch) and save it to VFS', 'curl': 'curl [-s] [-o FILE] — Same as wget: proxy fetch + verified VFS write' }; } _vfsExists(path) { // readFile returns null if not found const normalized = path.replace(/^\/+/, ''); return this.terminal.fs.readFile(normalized) !== null; } _vfsDirExists(path) { // Check if a path is a directory (or empty string for root) const normalized = path.replace(/^\/+/, '').replace(/\/+$/, ''); if (!normalized) return true; // root always exists try { const items = this.terminal.fs.readDir(normalized); return Array.isArray(items); } catch (e) { return false; } } _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); } // Binary-safe variants: return raw Uint8Array, store as base64 in VFS async _gzipBytes(bytes) { const cs = new CompressionStream('gzip'); const writer = cs.writable.getWriter(); writer.write(bytes); writer.close(); const compressed = await new Response(cs.readable).arrayBuffer(); return new Uint8Array(compressed); } async _gunzipBytes(bytes) { const ds = new DecompressionStream('gzip'); const writer = ds.writable.getWriter(); writer.write(bytes); writer.close(); const decompressed = await new Response(ds.readable).arrayBuffer(); return new Uint8Array(decompressed); } _bytesToBase64(bytes) { let s = ''; for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]); return btoa(s); } _base64ToBytes(b64) { const s = atob(b64); const u8 = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) u8[i] = s.charCodeAt(i); return u8; } // Save binary file to VFS (stored as Latin-1 string — each byte → one char, round-trips via charCodeAt) _vfsWriteBytes(path, bytes) { const normalized = path.replace(/^\/+/, ''); let s = ''; for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]); this.terminal.fs.writeFile(normalized, s); } // Load binary file from VFS (auto-detects format: base64 via heuristic, or raw Latin-1 bytes) _vfsReadBytes(path) { const normalized = path.replace(/^\/+/, ''); const content = this.terminal.fs.readFile(normalized); if (content === null) throw new Error(`File not found: ${path}`); // Heuristic: if content looks like base64 (only [A-Za-z0-9+/\n\r]) and length is multiple of 4, decode const looksLikeBase64 = /^[A-Za-z0-9+/\n\r]+=*$/.test(content.slice(0, 200)); if (looksLikeBase64 && content.length % 4 === 0) { try { return this._base64ToBytes(content); } catch (e) {} } // Fallback: treat as Latin-1 bytes (each charCode is a byte) const u8 = new Uint8Array(content.length); for (let i = 0; i < content.length; i++) u8[i] = content.charCodeAt(i); return u8; } // Detect gzip magic bytes (1f 8b) _isGzip(bytes) { return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b; } // ============================================================ // TAR / UNTAR — POSIX ustar parser (pure JS, ~120 lines) // Supports: ustar + gnu tar formats, gzip auto-detect // ============================================================ _parseOctal(str, len) { // Parse octal string (null/space-padded) from tar header field const s = (str || '').replace(/[\x00\s]+$/g, '').replace(/^0+/, '') || '0'; return parseInt(s, 8) || 0; } _padOctal(num, len) { // Format number as octal, null-terminated, len bytes const oct = num.toString(8); return oct + '\0'.repeat(len - oct.length - 1); } // Parse a tar buffer (Uint8Array) into [{name, type, size, data, mode, mtime}] _tarParse(bytes) { const entries = []; const decoder = new TextDecoder(); let off = 0; while (off < bytes.length) { // Read 512-byte header if (off + 512 > bytes.length) break; const header = bytes.slice(off, off + 512); // Empty block = end of archive if (header.every(b => b === 0)) break; const nameRaw = decoder.decode(header.slice(0, 100)).replace(/\0+$/, ''); const size = this._parseOctal(decoder.decode(header.slice(124, 136)), 12); const typeFlag = String.fromCharCode(header[156]); const magic = decoder.decode(header.slice(257, 263)).replace(/\0+$/, ''); // ustar prefix (POSIX): name split across header.name + header.prefix let name = nameRaw; if (magic === 'ustar') { const prefix = decoder.decode(header.slice(345, 500)).replace(/\0+$/, ''); if (prefix) name = prefix + '/' + nameRaw; } off += 512; // Read file data (padded to 512) const data = bytes.slice(off, off + size); off += Math.ceil(size / 512) * 512; entries.push({ name, type: typeFlag, size, data, mode: this._parseOctal(decoder.decode(header.slice(100, 108)), 8) }); } return entries; } // Build a tar buffer from a list of {name, type, size, data, mode} entries _tarBuild(entries) { const blocks = []; const encoder = new TextEncoder(); for (const e of entries) { const header = new Uint8Array(512); // Name (100 bytes, null-padded) const nameBytes = encoder.encode(e.name.slice(0, 99)); header.set(nameBytes, 0); // Mode (8 bytes, octal null-terminated) - default 0644 const modeStr = (e.mode || 0o644).toString(8).padStart(7, '0') + '\0'; header.set(encoder.encode(modeStr), 100); // uid/gid (8 bytes each, "0000000\0") header.set(encoder.encode('0000000\0'), 108); header.set(encoder.encode('0000000\0'), 116); // Size (12 bytes, octal) const sizeStr = e.size.toString(8).padStart(11, '0') + '\0'; header.set(encoder.encode(sizeStr), 124); // Mtime (12 bytes, octal) - epoch const mtimeStr = Math.floor(Date.now() / 1000).toString(8).padStart(11, '0') + '\0'; header.set(encoder.encode(mtimeStr), 136); // Type flag at offset 156 header[156] = e.type.charCodeAt(0); // Magic "ustar\0" at 257 header.set(encoder.encode('ustar\0'), 257); // Version "00" at 263 header.set(encoder.encode('00'), 263); blocks.push(header); // Data block(s), padded to 512 const data = e.data; if (data && data.length > 0) { const padded = new Uint8Array(Math.ceil(data.length / 512) * 512); padded.set(data, 0); blocks.push(padded); } } // End-of-archive: two 512-byte zero blocks blocks.push(new Uint8Array(512)); blocks.push(new Uint8Array(512)); // Concatenate const total = blocks.reduce((s, b) => s + b.length, 0); const out = new Uint8Array(total); let pos = 0; for (const b of blocks) { out.set(b, pos); pos += b.length; } return out; } // Collect all files in a VFS directory recursively _vfsCollectFiles(dirPath) { const normalized = dirPath.replace(/^\/+/, '').replace(/\/+$/, ''); const entries = []; const walk = (rel, absParts) => { const items = this.terminal.fs.readDir(absParts.join('/')); for (const item of items) { const itemPath = absParts.concat([item.name]).join('/'); const relPath = rel ? `${rel}/${item.name}` : item.name; if (item.type === 'dir') { walk(relPath, absParts.concat([item.name])); } else { const content = this.terminal.fs.readFile(itemPath); entries.push({ vfsPath: itemPath, archivePath: relPath, content: content || '' }); } } }; // Handle root case if (!normalized) { walk('', []); } else { walk(normalized.split('/').pop() || '', normalized.split('/')); } return entries; } // ============================================================ // WGET — fetch a URL through the /api/fetch proxy and save to VFS // A direct browser fetch() only works same-origin: cross-origin URLs // throw "Failed to fetch" (CORS), measured 2026-11-06 on the served // v4.6.150 page. /api/fetch returns the raw source as UTF-8 text. // ============================================================ async _cmdWget(args) { if (args.length < 1) { this.terminal.print('Usage: wget [filename] — Download a URL and save to VFS', 'warning'); return; } const url = /^https?:\/\//i.test(args[0]) ? args[0] : 'https://' + args[0]; const filename = args[1] || url.split('/').pop().split('?')[0] || 'download.bin'; this.terminal.print(`Downloading ${url}...`, 'info'); let text; try { const resp = await fetch('/api/fetch?url=' + encodeURIComponent(url)); if (resp.status === 403) { this.terminal.print('wget error: URL blocked — internal network', 'error'); return; } if (!resp.ok) { let detail = 'HTTP ' + resp.status; try { const j = await resp.json(); detail = j.error || detail; } catch (e) {} this.terminal.print('wget error: ' + detail, 'error'); return; } text = await resp.text(); } catch (e) { this.terminal.print('wget error: ' + e.message, 'error'); return; } // writeFile() returns false rather than throwing when the parent directory is // missing — the old code printed ✅ on that path (measured: writeFile('nosuchdir/x.txt') === false). const dir = filename.includes('/') ? filename.slice(0, filename.lastIndexOf('/')) : ''; if (dir) { try { this.terminal.fs.mkdir(dir); } catch (e) {} } if (!this.terminal.fs.writeFile(filename, text)) { this.terminal.print(`wget error: could not write ${filename} (missing directory, or path is a directory)`, 'error'); return; } if (this.terminal.fs.readFile(filename) !== text) { this.terminal.print(`wget error: write verification failed for ${filename}`, 'error'); return; } this.terminal.print(`✅ wget: ${filename} (${(text.length / 1024).toFixed(1)} KB, ${text.length} chars)`, 'success'); } // ============================================================ // TAR / UNTAR COMMANDS // ============================================================ async _cmdTar(args) { if (args.length < 2) { this.terminal.print('Usage: tar ', 'warning'); return; } const [srcPath, outPath] = args; if (!this._vfsDirExists(srcPath)) { this.terminal.print(`Source not found (or not a directory): ${srcPath}`, 'error'); return; } this.terminal.print(`Bundling ${srcPath} → ${outPath}...`, 'info'); const files = this._vfsCollectFiles(srcPath); if (files.length === 0) { this.terminal.print('No files found in source directory', 'warning'); return; } // Build tar entries (all '0' = regular file) const entries = files.map(f => ({ name: f.archivePath, type: '0', size: f.content.length, data: new TextEncoder().encode(f.content), mode: 0o644 })); let tarBytes = this._tarBuild(entries); // Gzip if .gz suffix if (outPath.endsWith('.gz')) { tarBytes = await this._gzipBytes(tarBytes); } this._vfsWriteBytes(outPath, tarBytes); const sizeKb = (tarBytes.length / 1024).toFixed(1); this.terminal.print(`✅ tar: ${files.length} files → ${outPath} (${sizeKb} KB)`, 'success'); } async _cmdUntar(args) { if (args.length < 1) { this.terminal.print('Usage: untar [dest-dir]', 'warning'); return; } const srcPath = args[0]; const destDir = (args[1] || '/').replace(/^\/+/, '').replace(/\/+$/, ''); if (!this._vfsExists(srcPath)) { this.terminal.print(`File not found: ${srcPath}`, 'error'); return; } this.terminal.print(`Extracting ${srcPath} → /${destDir}...`, 'info'); let bytes = this._vfsReadBytes(srcPath); // Auto-detect and strip gzip if (this._isGzip(bytes)) { this.terminal.print(' detected gzip wrapper, decompressing...', 'info'); bytes = await this._gunzipBytes(bytes); } const entries = this._tarParse(bytes); if (entries.length === 0) { this.terminal.print('No entries found in tar (empty or invalid format)', 'error'); return; } // Write to VFS let fileCount = 0, dirCount = 0; for (const e of entries) { // Clean name (strip leading ./, normalize slashes) let cleanName = e.name.replace(/^\.?\//, '').replace(/\/+$/, ''); const targetPath = destDir ? `${destDir}/${cleanName}` : cleanName; if (e.type === '5' || e.type === 'd') { // Directory if (cleanName) { this.terminal.fs.mkdirRecursive(targetPath); dirCount++; } } else { // Regular file (or type '0' or '\0') const dir = targetPath.split('/').slice(0, -1).join('/'); if (dir) this.terminal.fs.mkdirRecursive(dir); const content = new TextDecoder('utf-8', { fatal: false }).decode(e.data); this.terminal.fs.writeFile(targetPath, content); fileCount++; } } this.terminal.print(`✅ untar: ${fileCount} files, ${dirCount} dirs from ${srcPath}`, 'success'); } // ============================================================ // 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.commands['tar'] = async (args) => self._cmdTar(args); this.commands['untar'] = async (args) => self._cmdUntar(args); this.commands['wget'] = async (args) => self._cmdWget(args); // curl — the flags an agent actually types. No shell semantics: -s/-L/-f are // accepted and ignored, -o/--output selects the VFS target. Delegates to the // single verified writer above so the two cannot drift. this.commands['curl'] = async (args) => { const positional = []; let out = null; for (let i = 0; i < args.length; i++) { const a = String(args[i]); if (a === '-o' || a === '-O' || a === '--output') { out = args[++i]; } else if (a.startsWith('-')) { /* accepted, ignored */ } else positional.push(a); } if (!positional[0]) { this.terminal.print('Usage: curl [-s] [-o FILE] ', 'warning'); return; } const target = out || (positional[0].split('/').pop().split('?')[0] || 'download.bin'); return self._cmdWget([positional[0], target]); }; this.commandsMeta = { gzip: { argType: 'file', description: 'Compress a VFS file to .gz' }, gunzip: { argType: 'file', description: 'Decompress a .gz file (binary-safe)' }, encrypt: { argType: 'file', description: 'Encrypt a file with passphrase' }, decrypt: { argType: 'file', description: 'Decrypt a .enc file' }, tar: { argType: 'file', description: 'Bundle a VFS directory into a tar archive' }, untar: { argType: 'file', description: 'Extract a tar archive into VFS' }, wget: { argType: 'url', description: 'Download a URL and save to VFS' }, curl: { argType: 'url', description: 'curl [-s] [-o FILE] — same as wget' } }; } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new CryptoGzipModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/cryptogzip.js?v=2026-06-08-v4.5.26'); }