aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/cryptogzip.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext/cryptogzip.js')
-rw-r--r--modules/ext/cryptogzip.js281
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' }
+ };
+ }
+}