// Extracted from newOS4 index.html — SecretVault class SecretVault { constructor() { this.commands = {}; this.terminal = null; this._key = null; this._prefix = 'webx11_secret_'; this._saltKey = 'webx11_secret_salt'; this._verifyName = 'secret_vault_verified'; this._openModeKey = 'webx11_secret_open_key'; } getHelpInfo() { return { 'secret-set': 'secret-set — Encrypt and store a secret', 'secret-get': 'secret-get — Decrypt and display a secret value', 'secret-list': 'secret-list — List all stored secret names', 'secret-del': 'secret-del — Remove a stored secret', 'secret-lock': 'secret-lock — Lock the vault (clear derived key from memory)', 'secret-unlock': 'secret-unlock — Unlock the vault with a PIN code', 'secret-setpin': 'secret-setpin — Set or change the vault PIN code', 'secret-status': 'secret-status — Show vault status (locked/unlocked/pin set/open mode)', }; } registerCommands() { this.commands['secret-set'] = (args) => this._cmdSet(args); this.commands['secret-get'] = (args) => this._cmdGet(args); this.commands['secret-list'] = () => this._cmdList(); this.commands['secret-del'] = (args) => this._cmdDel(args); this.commands['secret-lock'] = () => this._cmdLock(); this.commands['secret-unlock'] = (args) => this._cmdUnlock(args); this.commands['secret-setpin'] = (args) => this._cmdSetPin(args); this.commands['secret-status'] = () => this._cmdStatus(); } // ---------- Core API ---------- isPinSet() { return localStorage.getItem(this._saltKey) !== null; } isLocked() { // Locked only if PIN is set but key is not derived in memory return this.isPinSet() && this._key === null; } async setPin(pin) { if (!pin || typeof pin !== 'string' || pin.length === 0) { return { error: 'PIN must be a non-empty string' }; } // Generate new salt const salt = this._generateSalt(); localStorage.setItem(this._saltKey, this._arrayToBase64(salt)); // Derive new key from the new PIN const newKey = await this._deriveKeyFromPin(pin, salt); if (this.isPinSet() && this._key !== null) { // Re-encrypt all existing secrets with new key await this._reEncryptAll(newKey); } // Store the new key in memory this._key = newKey; // Store a verification token so we know the PIN works await this._encryptWithKey(this._verifyName, 'verified', this._key); return { success: true }; } lock() { this._key = null; return { success: true }; } async unlock(pin) { if (!pin || typeof pin !== 'string' || pin.length === 0) { return { error: 'PIN must be a non-empty string' }; } if (!this.isPinSet()) { return { error: 'No PIN has been set. Use secret-setpin first.' }; } const saltBase64 = localStorage.getItem(this._saltKey); if (!saltBase64) { return { error: 'Salt not found. Vault may be corrupted.' }; } const salt = this._base64ToArray(saltBase64); const key = await this._deriveKeyFromPin(pin, salt); // Verify PIN by checking the verification token const verifyData = await this._decryptWithKey(this._verifyName, key); if (verifyData === null) { return { error: 'Invalid PIN' }; } this._key = key; return { success: true }; } async encrypt(name, value) { if (this.isLocked()) { return { error: 'Vault is locked' }; } // Ensure key is available let key = this._key; if (!key) { // Open mode: use or generate a session-like key key = await this._getOpenModeKey(); } const stringValue = typeof value === 'string' ? value : JSON.stringify(value); return this._encryptWithKey(name, stringValue, key); } async decrypt(name) { if (this.isLocked()) { return null; } let key = this._key; if (!key) { key = await this._getOpenModeKey(); if (!key) return null; } return this._decryptWithKey(name, key); } remove(name) { localStorage.removeItem(this._prefix + name); return { success: true }; } list() { const names = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key.startsWith(this._prefix)) { names.push(key.substring(this._prefix.length)); } } return names; } has(name) { return localStorage.getItem(this._prefix + name) !== null; } /** * Scans text for any stored secret values and replaces them with '[REDACTED]'. * Used by terminal and AI context to prevent leaking secrets in output. */ async redactSecret(text) { if (!text || typeof text !== 'string') return text; const names = this.list(); if (names.length === 0) return text; let redacted = text; for (const name of names) { try { const value = await this.decrypt(name); if (value && typeof value === 'string' && value.length > 0) { // Escape special regex characters in the secret value const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); try { const regex = new RegExp(escaped, 'g'); redacted = redacted.replace(regex, '[REDACTED]'); } catch (e) { // If regex fails (e.g., complex Unicode), try manual replacement while (redacted.includes(value)) { redacted = redacted.replace(value, '[REDACTED]'); } } } } catch (e) { // Skip secrets that can't be decrypted } } return redacted; } // ---------- Internal crypto methods ---------- _generateSalt() { const salt = new Uint8Array(16); crypto.getRandomValues(salt); return salt; } _generateIV() { const iv = new Uint8Array(12); crypto.getRandomValues(iv); return iv; } _arrayToBase64(array) { return btoa(String.fromCharCode.apply(null, new Uint8Array(array))); } _base64ToArray(base64) { const binary = atob(base64); const array = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { array[i] = binary.charCodeAt(i); } return array; } async _deriveKeyFromPin(pin, salt) { const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', encoder.encode(pin), 'PBKDF2', false, ['deriveKey'] ); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt: salt, iterations: 100000, hash: 'SHA-256', }, keyMaterial, { name: 'AES-GCM', length: 256, }, false, ['encrypt', 'decrypt'] ); } async _getOpenModeKey() { // In open mode (no PIN set), we use a persistent 32-char hex key in localStorage // This is less secure than PIN mode but better than plaintext let keyHex = localStorage.getItem(this._openModeKey); if (!keyHex) { keyHex = this._generateOpenKey(); localStorage.setItem(this._openModeKey, keyHex); } return this._hexKeyToCryptoKey(keyHex); } _generateOpenKey() { const bytes = new Uint8Array(32); crypto.getRandomValues(bytes); return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); } async _hexKeyToCryptoKey(hex) { const bytes = new Uint8Array(32); for (let i = 0; i < 32; i++) { bytes[i] = parseInt(hex.substr(i * 2, 2), 16); } return crypto.subtle.importKey( 'raw', bytes, { name: 'AES-GCM', length: 256, }, false, ['encrypt', 'decrypt'] ); } async _encryptWithKey(name, value, key) { try { const encoder = new TextEncoder(); const data = encoder.encode(value); const iv = this._generateIV(); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv, }, key, data ); const encryptedArray = new Uint8Array(encrypted); const storageData = JSON.stringify({ iv: this._arrayToBase64(iv), data: this._arrayToBase64(encryptedArray), }); localStorage.setItem(this._prefix + name, storageData); return { success: true }; } catch (e) { return { error: 'Encryption failed: ' + e.message }; } } async _decryptWithKey(name, key) { try { const storageData = localStorage.getItem(this._prefix + name); if (!storageData) return null; const parsed = JSON.parse(storageData); if (!parsed.iv || !parsed.data) return null; const iv = this._base64ToArray(parsed.iv); const encryptedData = this._base64ToArray(parsed.data); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv, }, key, encryptedData ); const decoder = new TextDecoder(); return decoder.decode(decrypted); } catch (e) { return null; } } async _reEncryptAll(newKey) { const names = this.list(); const oldKey = this._key; for (const name of names) { // Skip verification token — it's regenerated in setPin if (name === this._verifyName) continue; const plaintext = await this._decryptWithKey(name, oldKey); if (plaintext !== null) { await this._encryptWithKey(name, plaintext, newKey); } } // Update verification token with new key await this._encryptWithKey(this._verifyName, 'verified', newKey); } // ---------- Command handlers ---------- async _cmdSet(args) { if (args.length < 2) { this.terminal.print('Usage: secret-set ', 'warning'); return; } const name = args[0]; const value = args.slice(1).join(' '); const result = await this.encrypt(name, value); if (result.error) { this.terminal.print(result.error, 'error'); } else { this.terminal.print(`Secret '${name}' saved`, 'success'); } } async _cmdGet(args) { if (!args[0]) { this.terminal.print('Usage: secret-get ', 'warning'); return; } const value = await this.decrypt(args[0]); if (value === null) { if (this.isLocked()) { this.terminal.print('Vault is locked. Use secret-unlock ', 'warning'); } else { this.terminal.print(`Secret '${args[0]}' not found`, 'error'); } } else { this.terminal.print(`Secret '${args[0]}': ${value}`, 'info'); } } _cmdList() { const names = this.list(); if (names.length === 0) { this.terminal.print('No secrets stored', 'info'); return; } this.terminal.print('=== Stored Secrets ===', 'info'); names.forEach(n => { // Skip internal verification token if (n === this._verifyName) return; this.terminal.print(` - ${n}`); }); } _cmdDel(args) { if (!args[0]) { this.terminal.print('Usage: secret-del ', 'warning'); return; } const result = this.remove(args[0]); if (result.success) { this.terminal.print(`Secret '${args[0]}' deleted`, 'success'); } } _cmdLock() { const result = this.lock(); if (result.success) { this.terminal.print('Vault locked', 'success'); } } async _cmdUnlock(args) { if (!args[0]) { this.terminal.print('Usage: secret-unlock ', 'warning'); return; } const result = await this.unlock(args[0]); if (result.error) { this.terminal.print(result.error, 'error'); } else { this.terminal.print('Vault unlocked', 'success'); } } async _cmdSetPin(args) { if (!args[0]) { this.terminal.print('Usage: secret-setpin ', 'warning'); return; } const result = await this.setPin(args[0]); if (result.error) { this.terminal.print(result.error, 'error'); } else { this.terminal.print('PIN set successfully. Vault is now in PIN-protected mode.', 'success'); } } _cmdStatus() { const pinSet = this.isPinSet(); const locked = this.isLocked(); const hasKey = this._key !== null; this.terminal.print('=== Secret Vault Status ===', 'info'); this.terminal.print(`PIN set: ${pinSet ? 'Yes' : 'No'}`, pinSet ? 'success' : 'info'); if (pinSet) { this.terminal.print(`Vault: ${locked ? '🔒 Locked' : '🔓 Unlocked'}`, locked ? 'warning' : 'success'); } else { this.terminal.print(`Mode: Open (no PIN — using persistent key)`, 'info'); } this.terminal.print(`Secrets stored: ${this.list().filter(n => n !== this._verifyName).length}`, 'info'); } }