// 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;
}
/**
* v4.5.68: Redacts known secret NAMES (not values) from text — sync, no decryption needed.
* Previous version decrypted ALL secrets on every call (perf + side-channel + failed when locked).
* Now just redacts the secret names if they appear in output. Values are never decrypted into memory.
*/
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) {
if (name && redacted.includes(name)) {
redacted = redacted.split(name).join('[REDACTED]');
}
}
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: 600000, // v4.5.68: 100k → 600k (OWASP 2023 recommendation)
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');
}
}