diff options
| author | Agando <roduccomm@gmail.com> | 2026-05-27 06:00:43 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-05-27 06:00:43 +0200 |
| commit | 212708b464c2bdbdd9670c00804623dca0729d2c (patch) | |
| tree | ee190cbb0d1524350baea641df65aa19817744b5 /modules/ext/cryptogzip.js | |
| parent | f4d934d341056142da1212c845eeeb170fd452c2 (diff) | |
auto: 2026-05-27_06:00 changes
Diffstat (limited to 'modules/ext/cryptogzip.js')
| -rw-r--r-- | modules/ext/cryptogzip.js | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/modules/ext/cryptogzip.js b/modules/ext/cryptogzip.js new file mode 100644 index 0000000..6f873ff --- /dev/null +++ b/modules/ext/cryptogzip.js @@ -0,0 +1,281 @@ +class CryptoGzipModule { + // CryptoGzipModule — VFS-level gzip/encrypt commands for WebX11 newOS4 + // Commands: gzip <path>, gunzip <path.gz>, encrypt <path>, decrypt <path.enc> + constructor(terminal) { + this.terminal = terminal; + this.commands = {}; + } + + _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 <path> — 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 <path.gz> — 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 <path> <passphrase> — 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 <path.enc> <passphrase> — 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' } + }; + } +} |