diff options
Diffstat (limited to 'CryptoGzipModule-draft.js')
| -rw-r--r-- | CryptoGzipModule-draft.js | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/CryptoGzipModule-draft.js b/CryptoGzipModule-draft.js new file mode 100644 index 0000000..2c1e45f --- /dev/null +++ b/CryptoGzipModule-draft.js @@ -0,0 +1,315 @@ +/** + * CryptoGzipModule — Draft for Dev Agent + * + * Commands: + * gzip <path> → creates <path>.gz + * gunzip <path.gz> → restores <path> + * encrypt <path> → prompts passphrase → creates <path>.enc + * decrypt <path.enc> → prompts passphrase → restores <path> + * + * 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 <path> — Compress a VFS file to .gz', + 'gunzip': 'gunzip <path.gz> — Decompress a .gz file in VFS', + 'encrypt':'encrypt <path> — AES-256-GCM encrypt a VFS file (interactive passphrase)', + 'decrypt':'decrypt <path.enc> — 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 <path>'); + + 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 <path.gz>'); + + 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 <path>'); + + 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 <path.enc>'); + + 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 |