aboutsummaryrefslogtreecommitdiff
path: root/CryptoGzipModule-draft.js
diff options
context:
space:
mode:
Diffstat (limited to 'CryptoGzipModule-draft.js')
-rw-r--r--CryptoGzipModule-draft.js315
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