Add vault system for encrypted settings storage
- Add vault.js: encrypted settings with AES-256 + 7-day session persistence - Integrate vault UI in settings modal with friendly messaging - Update config.js and un.js to use vault for credentials - Add translation strings for vault UI (en.js) - Show "Logged in" status with Lock button instead of session info - Vault password only asked when opening settings
This commit is contained in:
parent
637064bf99
commit
3d07909854
5 changed files with 958 additions and 34 deletions
|
|
@ -190,14 +190,29 @@ export const VLLM_ENDPOINTS = [
|
|||
},
|
||||
];
|
||||
|
||||
// Helper to get vault value or localStorage fallback
|
||||
function getVaultOrStorage(key, defaultValue = null) {
|
||||
// Try vault first if available
|
||||
if (typeof window !== 'undefined' && window.UncloseVault && window.UncloseVault.isUnlocked()) {
|
||||
return window.UncloseVault.get(key, defaultValue);
|
||||
}
|
||||
// Fallback to localStorage
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored === null) return defaultValue;
|
||||
// Parse booleans
|
||||
if (stored === 'true') return true;
|
||||
if (stored === 'false') return false;
|
||||
return stored;
|
||||
}
|
||||
|
||||
// Function to get API configuration (custom or default)
|
||||
export async function getAPIConfig() {
|
||||
try {
|
||||
const useCustomAPI = localStorage.getItem("useCustomAPI") === "true";
|
||||
|
||||
const useCustomAPI = getVaultOrStorage("useCustomAPI", false) === true;
|
||||
|
||||
if (useCustomAPI) {
|
||||
const customBaseURL = localStorage.getItem("customBaseURL");
|
||||
const customAPIKey = localStorage.getItem("customAPIKey");
|
||||
const customBaseURL = getVaultOrStorage("customBaseURL", "");
|
||||
const customAPIKey = getVaultOrStorage("customAPIKey", "");
|
||||
|
||||
// Import model functions to get the currently selected model
|
||||
const { getSelectedModel } = await import("./models.js");
|
||||
|
|
|
|||
|
|
@ -117,4 +117,35 @@ export const en = {
|
|||
useUnsandbox: "Use unsandbox.com API keys for code execution.",
|
||||
unsandboxPublicKey: "Public Key (unsb-pk-...)",
|
||||
unsandboxSecretKey: "Secret Key (unsb-sk-...)",
|
||||
|
||||
// Vault settings
|
||||
vaultLoggedIn: "Logged in",
|
||||
vaultLocked: "Settings Locked",
|
||||
vaultUnlocked: "Settings Unlocked",
|
||||
vaultCreate: "Save Settings",
|
||||
vaultUnlock: "Unlock",
|
||||
vaultLock: "Lock",
|
||||
vaultLockTooltip: "Lock your settings (log out)",
|
||||
vaultPassword: "Password",
|
||||
vaultPasswordPlaceholder: "Choose a password...",
|
||||
vaultConfirmPassword: "Confirm password",
|
||||
vaultConfirmPlaceholder: "Confirm your password...",
|
||||
vaultWrongPassword: "That password didn't work. Please try again.",
|
||||
vaultPasswordMismatch: "Passwords don't match.",
|
||||
vaultPasswordTooShort: "Password needs at least 8 characters.",
|
||||
vaultCreated: "Settings saved!",
|
||||
vaultUnlockedMsg: "Welcome back!",
|
||||
vaultLockedMsg: "Settings locked.",
|
||||
vaultRequired: "Enter your password to access settings.",
|
||||
vaultWelcome: "Your settings (model, voice, API keys) are saved safely behind a password of your choice. This keeps them private on this device.",
|
||||
vaultWelcomeBack: "Welcome back! Enter your password to access your saved settings.",
|
||||
vaultCreateInfo: "Choose a password to protect your settings. Your preferences and API keys will be encrypted and stored locally.",
|
||||
vaultSessionInfo: "You'll stay logged in for {days} days.",
|
||||
vaultChangePassword: "Change Password",
|
||||
vaultDeleteVault: "Delete Settings",
|
||||
vaultDeleteConfirm: "Are you sure? This will remove all your saved settings from this device.",
|
||||
vaultCurrentPassword: "Current password",
|
||||
vaultNewPassword: "New password",
|
||||
vaultPasswordChanged: "Password changed!",
|
||||
vaultDeleted: "Settings deleted.",
|
||||
};
|
||||
|
|
@ -10,9 +10,24 @@ const API_BASE = "https://api.unsandbox.com";
|
|||
const POLL_DELAYS_MS = [300, 450, 700, 900, 650, 1600, 2000];
|
||||
|
||||
/**
|
||||
* Get credentials from localStorage
|
||||
* Get credentials from vault (preferred) or localStorage (fallback)
|
||||
*/
|
||||
export function getCredentials() {
|
||||
// Try to get from vault first
|
||||
if (typeof window !== 'undefined' && window.UncloseVault && window.UncloseVault.isUnlocked()) {
|
||||
const useUnsandbox = window.UncloseVault.get("useUnsandbox", false);
|
||||
if (!useUnsandbox) return null;
|
||||
|
||||
const publicKey = window.UncloseVault.get("unsandboxPublicKey");
|
||||
const secretKey = window.UncloseVault.get("unsandboxSecretKey");
|
||||
|
||||
if (publicKey && secretKey) {
|
||||
return { publicKey, secretKey };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fallback to localStorage (for migration or if vault not available)
|
||||
const useUnsandbox = localStorage.getItem("useUnsandbox") === "true";
|
||||
if (!useUnsandbox) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,23 @@ import {
|
|||
setUserLanguagePreference,
|
||||
} from "./ui-translations.js";
|
||||
import { NATIVE_LANGUAGE_NAMES } from "./translation.js";
|
||||
import { UncloseVault } from "./vault.js";
|
||||
|
||||
// Load CryptoJS if not already loaded
|
||||
async function ensureCryptoJS() {
|
||||
if (typeof CryptoJS !== 'undefined') return true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js';
|
||||
script.onload = () => resolve(true);
|
||||
script.onerror = () => {
|
||||
console.warn('Failed to load CryptoJS - vault features disabled');
|
||||
resolve(false);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
// Get USE_CUSTOM_STYLING from window or default
|
||||
const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false;
|
||||
|
|
@ -179,6 +196,12 @@ function refreshModalUI() {
|
|||
|
||||
async function openUncloseaiEmbeddedModalNew() {
|
||||
try {
|
||||
// Load CryptoJS and initialize vault
|
||||
await ensureCryptoJS();
|
||||
if (UncloseVault.isAvailable()) {
|
||||
UncloseVault.init();
|
||||
}
|
||||
|
||||
// Detect if PicoCSS is actually present on the page
|
||||
const hasPicoCSS = document.querySelector('link[href*="pico"]') !== null;
|
||||
|
||||
|
|
@ -277,6 +300,311 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
const settingsPanel = document.createElement("div");
|
||||
settingsPanel.className = "uncloseai-modal-settings";
|
||||
|
||||
// Vault UI section
|
||||
const vaultSection = document.createElement("div");
|
||||
vaultSection.className = "uncloseai-section uncloseai-vault-section";
|
||||
vaultSection.style.gridColumn = "1 / -1"; // Full width
|
||||
vaultSection.style.marginBottom = "16px";
|
||||
vaultSection.style.padding = "12px";
|
||||
vaultSection.style.borderRadius = "8px";
|
||||
vaultSection.style.border = "1px solid var(--uncloseai-border-color, #ccc)";
|
||||
|
||||
// Container for the rest of settings (will be hidden when vault locked)
|
||||
const settingsContent = document.createElement("div");
|
||||
settingsContent.className = "uncloseai-settings-content";
|
||||
|
||||
// Helper to get vault value or localStorage fallback (for migration)
|
||||
function getVaultOrStorage(key, defaultValue = null) {
|
||||
if (UncloseVault.isAvailable() && UncloseVault.isUnlocked()) {
|
||||
return UncloseVault.get(key, defaultValue);
|
||||
}
|
||||
// Fallback for when vault not available
|
||||
const stored = localStorage.getItem(key);
|
||||
return stored !== null ? stored : defaultValue;
|
||||
}
|
||||
|
||||
// Helper to set vault value
|
||||
function setVaultValue(key, value) {
|
||||
if (UncloseVault.isAvailable() && UncloseVault.isUnlocked()) {
|
||||
UncloseVault.set(key, value);
|
||||
} else {
|
||||
// If vault not unlocked, store temporarily (will be lost)
|
||||
console.warn('Vault locked - setting not persisted:', key);
|
||||
}
|
||||
}
|
||||
|
||||
// Build vault UI based on current state
|
||||
function buildVaultUI() {
|
||||
vaultSection.innerHTML = '';
|
||||
|
||||
if (!UncloseVault.isAvailable()) {
|
||||
// CryptoJS not loaded - settings work but won't persist across sessions
|
||||
vaultSection.style.display = 'none';
|
||||
settingsContent.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (UncloseVault.isUnlocked()) {
|
||||
// Vault unlocked - show simple "Logged in" status with lock button
|
||||
vaultSection.style.display = 'block';
|
||||
const unlockedUI = document.createElement("div");
|
||||
unlockedUI.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: #28a745;">✓ ${getUIText("vaultLoggedIn")}</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="vault-lock-btn uncloseai-btn-small" title="${getUIText("vaultLockTooltip")}">🔒 ${getUIText("vaultLock")}</button>
|
||||
<button class="vault-settings-btn uncloseai-btn-small" title="${getUIText("vaultChangePassword")}">⚙️</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const lockBtn = unlockedUI.querySelector('.vault-lock-btn');
|
||||
const settingsBtn = unlockedUI.querySelector('.vault-settings-btn');
|
||||
|
||||
lockBtn.onclick = () => {
|
||||
UncloseVault.lock();
|
||||
buildVaultUI();
|
||||
// Close settings panel after locking
|
||||
settingsOpen = false;
|
||||
settingsPanel.classList.remove("open");
|
||||
};
|
||||
|
||||
settingsBtn.onclick = () => {
|
||||
showVaultSettings();
|
||||
};
|
||||
|
||||
vaultSection.appendChild(unlockedUI);
|
||||
settingsContent.style.display = 'block'; // Show settings when unlocked
|
||||
} else {
|
||||
// Vault locked or doesn't exist - show prompt
|
||||
vaultSection.style.display = 'block';
|
||||
const hasVault = UncloseVault.hasVault();
|
||||
|
||||
const promptUI = document.createElement("div");
|
||||
promptUI.innerHTML = `
|
||||
<div style="text-align: center; padding: 8px 0;">
|
||||
<div style="margin-bottom: 12px; font-size: 0.95em;">
|
||||
${hasVault ? getUIText("vaultWelcomeBack") : getUIText("vaultWelcome")}
|
||||
</div>
|
||||
<input type="password" class="vault-password uncloseai-input" placeholder="${getUIText("vaultPasswordPlaceholder")}" style="margin-bottom: 8px; width: 100%;">
|
||||
${!hasVault ? `<input type="password" class="vault-confirm uncloseai-input" placeholder="${getUIText("vaultConfirmPlaceholder")}" style="margin-bottom: 8px; width: 100%;">` : ''}
|
||||
<button class="vault-submit-btn uncloseai-btn-control" style="width: 100%;">
|
||||
${hasVault ? getUIText("vaultUnlock") : getUIText("vaultCreate")}
|
||||
</button>
|
||||
<div class="vault-error" style="color: #dc3545; margin-top: 8px; display: none;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const passwordInput = promptUI.querySelector('.vault-password');
|
||||
const confirmInput = promptUI.querySelector('.vault-confirm');
|
||||
const submitBtn = promptUI.querySelector('.vault-submit-btn');
|
||||
const errorDiv = promptUI.querySelector('.vault-error');
|
||||
|
||||
submitBtn.onclick = () => {
|
||||
const password = passwordInput.value;
|
||||
|
||||
if (hasVault) {
|
||||
// Unlock existing vault
|
||||
const result = UncloseVault.unlock(password);
|
||||
if (result.success) {
|
||||
buildVaultUI();
|
||||
refreshSettingsFromVault();
|
||||
} else {
|
||||
errorDiv.textContent = getUIText("vaultWrongPassword");
|
||||
errorDiv.style.display = 'block';
|
||||
passwordInput.value = '';
|
||||
passwordInput.focus();
|
||||
}
|
||||
} else {
|
||||
// Create new vault
|
||||
const confirm = confirmInput ? confirmInput.value : '';
|
||||
|
||||
if (password.length < 8) {
|
||||
errorDiv.textContent = getUIText("vaultPasswordTooShort");
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirm) {
|
||||
errorDiv.textContent = getUIText("vaultPasswordMismatch");
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const result = UncloseVault.create(password);
|
||||
if (result.success) {
|
||||
migrateSettingsToVault();
|
||||
buildVaultUI();
|
||||
} else {
|
||||
errorDiv.textContent = result.error;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Allow Enter key to submit
|
||||
const lastInput = confirmInput || passwordInput;
|
||||
lastInput.onkeydown = (e) => {
|
||||
if (e.key === 'Enter') submitBtn.click();
|
||||
};
|
||||
|
||||
vaultSection.appendChild(promptUI);
|
||||
settingsContent.style.display = 'none'; // Hide settings until unlocked
|
||||
}
|
||||
}
|
||||
|
||||
// Show vault settings dialog (change password, delete)
|
||||
function showVaultSettings() {
|
||||
const existingDialog = document.getElementById('vault-settings-dialog');
|
||||
if (existingDialog) existingDialog.remove();
|
||||
|
||||
const dialog = document.createElement('dialog');
|
||||
dialog.id = 'vault-settings-dialog';
|
||||
dialog.style.cssText = 'padding: 20px; border-radius: 8px; max-width: 400px; border: 1px solid var(--uncloseai-border-color, #ccc);';
|
||||
dialog.innerHTML = `
|
||||
<h3 style="margin-top: 0;">🔑 ${getUIText("vaultChangePassword")}</h3>
|
||||
<input type="password" class="current-password uncloseai-input" placeholder="${getUIText("vaultCurrentPassword")}" style="margin-bottom: 8px; width: 100%;">
|
||||
<input type="password" class="new-password uncloseai-input" placeholder="${getUIText("vaultNewPassword")}" style="margin-bottom: 8px; width: 100%;">
|
||||
<input type="password" class="confirm-password uncloseai-input" placeholder="${getUIText("vaultConfirmPlaceholder")}" style="margin-bottom: 12px; width: 100%;">
|
||||
<button class="change-password-btn uncloseai-btn-control" style="width: 100%; margin-bottom: 16px;">${getUIText("vaultChangePassword")}</button>
|
||||
|
||||
<hr style="margin: 16px 0; border: none; border-top: 1px solid #ccc;">
|
||||
|
||||
<details style="margin-bottom: 12px;">
|
||||
<summary style="cursor: pointer; color: #6c757d; font-size: 0.9em;">${getUIText("vaultDeleteVault")}</summary>
|
||||
<div style="margin-top: 12px;">
|
||||
<p style="font-size: 0.9em; opacity: 0.8;">${getUIText("vaultDeleteConfirm")}</p>
|
||||
<input type="password" class="delete-password uncloseai-input" placeholder="${getUIText("vaultCurrentPassword")}" style="margin-bottom: 8px; width: 100%;">
|
||||
<button class="delete-vault-btn uncloseai-btn-control" style="width: 100%; background: #dc3545; border-color: #dc3545;">${getUIText("vaultDeleteVault")}</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="vault-dialog-error" style="color: #dc3545; margin-top: 12px; display: none;"></div>
|
||||
|
||||
<button class="close-dialog-btn uncloseai-btn-control" style="width: 100%; margin-top: 8px;">Close</button>
|
||||
`;
|
||||
|
||||
const changeBtn = dialog.querySelector('.change-password-btn');
|
||||
const deleteBtn = dialog.querySelector('.delete-vault-btn');
|
||||
const closeBtn = dialog.querySelector('.close-dialog-btn');
|
||||
const errorDiv = dialog.querySelector('.vault-dialog-error');
|
||||
|
||||
changeBtn.onclick = () => {
|
||||
const current = dialog.querySelector('.current-password').value;
|
||||
const newPass = dialog.querySelector('.new-password').value;
|
||||
const confirm = dialog.querySelector('.confirm-password').value;
|
||||
|
||||
if (newPass.length < 8) {
|
||||
errorDiv.textContent = getUIText("vaultPasswordTooShort");
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPass !== confirm) {
|
||||
errorDiv.textContent = getUIText("vaultPasswordMismatch");
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const result = UncloseVault.changePassword(current, newPass);
|
||||
if (result.success) {
|
||||
alert(getUIText("vaultPasswordChanged"));
|
||||
dialog.close();
|
||||
dialog.remove();
|
||||
} else {
|
||||
errorDiv.textContent = result.error;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
deleteBtn.onclick = () => {
|
||||
const password = dialog.querySelector('.delete-password').value;
|
||||
const result = UncloseVault.deleteVault(password);
|
||||
|
||||
if (result.success) {
|
||||
alert(getUIText("vaultDeleted"));
|
||||
dialog.close();
|
||||
dialog.remove();
|
||||
buildVaultUI();
|
||||
} else {
|
||||
errorDiv.textContent = result.error;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
closeBtn.onclick = () => {
|
||||
dialog.close();
|
||||
dialog.remove();
|
||||
};
|
||||
|
||||
document.body.appendChild(dialog);
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
// Migrate existing localStorage settings to vault
|
||||
function migrateSettingsToVault() {
|
||||
if (!UncloseVault.isUnlocked()) return;
|
||||
|
||||
const keysToMigrate = [
|
||||
'selectedModel', 'selectedEndpoint', 'selectedVoice',
|
||||
'useCustomAPI', 'customBaseURL', 'customAPIKey',
|
||||
'useUnsandbox', 'unsandboxPublicKey', 'unsandboxSecretKey',
|
||||
'hermesSettingsOpen', 'uncloseai_language'
|
||||
];
|
||||
|
||||
keysToMigrate.forEach(key => {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value !== null) {
|
||||
// Parse booleans
|
||||
let parsedValue = value;
|
||||
if (value === 'true') parsedValue = true;
|
||||
else if (value === 'false') parsedValue = false;
|
||||
|
||||
UncloseVault.set(key, parsedValue);
|
||||
// Remove from plain localStorage after migration
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Settings migrated to vault');
|
||||
}
|
||||
|
||||
// Refresh UI inputs from vault values
|
||||
function refreshSettingsFromVault() {
|
||||
// Model selection will be loaded from vault when loadModels is called
|
||||
// Voice selection will be loaded from vault when loadVoices is called
|
||||
// Language will be loaded from vault
|
||||
|
||||
// Refresh API config
|
||||
if (customAPICheckbox) {
|
||||
customAPICheckbox.checked = getVaultOrStorage('useCustomAPI') === true || getVaultOrStorage('useCustomAPI') === 'true';
|
||||
apiConfigInputs.style.display = customAPICheckbox.checked ? 'block' : 'none';
|
||||
}
|
||||
if (baseURLInput) {
|
||||
baseURLInput.value = getVaultOrStorage('customBaseURL', '');
|
||||
}
|
||||
if (apiKeyInput) {
|
||||
apiKeyInput.value = getVaultOrStorage('customAPIKey', '');
|
||||
}
|
||||
|
||||
// Refresh unsandbox config
|
||||
if (unsandboxCheckbox) {
|
||||
unsandboxCheckbox.checked = getVaultOrStorage('useUnsandbox') === true || getVaultOrStorage('useUnsandbox') === 'true';
|
||||
unsandboxInputs.style.display = unsandboxCheckbox.checked ? 'block' : 'none';
|
||||
}
|
||||
if (unsandboxPublicKeyInput) {
|
||||
unsandboxPublicKeyInput.value = getVaultOrStorage('unsandboxPublicKey', '');
|
||||
}
|
||||
if (unsandboxSecretKeyInput) {
|
||||
unsandboxSecretKeyInput.value = getVaultOrStorage('unsandboxSecretKey', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize vault UI
|
||||
buildVaultUI();
|
||||
|
||||
// Model selection
|
||||
const modelSection = document.createElement("div");
|
||||
modelSection.className = "uncloseai-section";
|
||||
|
|
@ -306,9 +634,9 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
modelSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Load saved model from localStorage or use current selection
|
||||
const savedModel = localStorage.getItem("selectedModel");
|
||||
const savedEndpoint = localStorage.getItem("selectedEndpoint");
|
||||
// Load saved model from vault or use current selection
|
||||
const savedModel = getVaultOrStorage("selectedModel");
|
||||
const savedEndpoint = getVaultOrStorage("selectedEndpoint");
|
||||
const currentModel = getSelectedModel();
|
||||
|
||||
if (savedModel && savedEndpoint) {
|
||||
|
|
@ -356,9 +684,9 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
(model) => model.uniqueId === selectedUniqueId,
|
||||
);
|
||||
if (selectedModel) {
|
||||
// Update the global model selection
|
||||
localStorage.setItem("selectedModel", selectedModel.modelName);
|
||||
localStorage.setItem("selectedEndpoint", selectedModel.endpointId);
|
||||
// Update the model selection in vault
|
||||
setVaultValue("selectedModel", selectedModel.modelName);
|
||||
setVaultValue("selectedEndpoint", selectedModel.endpointId);
|
||||
console.log("Model changed to:", selectedModel);
|
||||
}
|
||||
};
|
||||
|
|
@ -416,8 +744,8 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
voiceSelect.appendChild(optgroup);
|
||||
});
|
||||
|
||||
// Load saved voice from localStorage or default to first voice
|
||||
const savedVoice = localStorage.getItem("selectedVoice");
|
||||
// Load saved voice from vault or default to first voice
|
||||
const savedVoice = getVaultOrStorage("selectedVoice");
|
||||
if (savedVoice && voiceSelect.querySelector(`option[value="${savedVoice}"]`)) {
|
||||
voiceSelect.value = savedVoice;
|
||||
}
|
||||
|
|
@ -425,9 +753,9 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
console.log("Voices loaded:", loadedVoicesCount, "voices from", models.length, "models");
|
||||
};
|
||||
|
||||
// Save voice selection to localStorage when changed
|
||||
// Save voice selection to vault when changed
|
||||
voiceSelect.onchange = () => {
|
||||
localStorage.setItem("selectedVoice", voiceSelect.value);
|
||||
setVaultValue("selectedVoice", voiceSelect.value);
|
||||
console.log("Voice changed to:", voiceSelect.value);
|
||||
};
|
||||
|
||||
|
|
@ -511,7 +839,7 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
const customAPICheckbox = document.createElement("input");
|
||||
customAPICheckbox.type = "checkbox";
|
||||
customAPICheckbox.id = "custom-api-toggle";
|
||||
customAPICheckbox.checked = localStorage.getItem("useCustomAPI") === "true";
|
||||
customAPICheckbox.checked = getVaultOrStorage("useCustomAPI") === true || getVaultOrStorage("useCustomAPI") === "true";
|
||||
|
||||
const customAPIToggleLabel = document.createElement("label");
|
||||
customAPIToggleLabel.htmlFor = "custom-api-toggle";
|
||||
|
|
@ -528,7 +856,7 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
const baseURLInput = document.createElement("input");
|
||||
baseURLInput.type = "text";
|
||||
baseURLInput.placeholder = "https://api.openai.com/v1";
|
||||
baseURLInput.value = localStorage.getItem("customBaseURL") || "";
|
||||
baseURLInput.value = getVaultOrStorage("customBaseURL", "");
|
||||
baseURLInput.className = "uncloseai-input";
|
||||
|
||||
baseURLContainer.appendChild(baseURLInput);
|
||||
|
|
@ -540,7 +868,7 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
const apiKeyInput = document.createElement("input");
|
||||
apiKeyInput.type = "password";
|
||||
apiKeyInput.placeholder = "API Key";
|
||||
apiKeyInput.value = localStorage.getItem("customAPIKey") || "";
|
||||
apiKeyInput.value = getVaultOrStorage("customAPIKey", "");
|
||||
apiKeyInput.className = "uncloseai-input";
|
||||
|
||||
apiKeyContainer.appendChild(apiKeyInput);
|
||||
|
|
@ -556,16 +884,16 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
// Event handlers
|
||||
customAPICheckbox.onchange = () => {
|
||||
const useCustom = customAPICheckbox.checked;
|
||||
localStorage.setItem("useCustomAPI", useCustom.toString());
|
||||
setVaultValue("useCustomAPI", useCustom);
|
||||
apiConfigInputs.style.display = useCustom ? "block" : "none";
|
||||
};
|
||||
|
||||
baseURLInput.onchange = () => {
|
||||
localStorage.setItem("customBaseURL", baseURLInput.value);
|
||||
setVaultValue("customBaseURL", baseURLInput.value);
|
||||
};
|
||||
|
||||
apiKeyInput.onchange = () => {
|
||||
localStorage.setItem("customAPIKey", apiKeyInput.value);
|
||||
setVaultValue("customAPIKey", apiKeyInput.value);
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -583,7 +911,7 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
const unsandboxCheckbox = document.createElement("input");
|
||||
unsandboxCheckbox.type = "checkbox";
|
||||
unsandboxCheckbox.id = "unsandbox-toggle";
|
||||
unsandboxCheckbox.checked = localStorage.getItem("useUnsandbox") === "true";
|
||||
unsandboxCheckbox.checked = getVaultOrStorage("useUnsandbox") === true || getVaultOrStorage("useUnsandbox") === "true";
|
||||
|
||||
const unsandboxToggleLabel = document.createElement("label");
|
||||
unsandboxToggleLabel.htmlFor = "unsandbox-toggle";
|
||||
|
|
@ -600,7 +928,7 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
const unsandboxPublicKeyInput = document.createElement("input");
|
||||
unsandboxPublicKeyInput.type = "text";
|
||||
unsandboxPublicKeyInput.placeholder = getUIText("unsandboxPublicKey");
|
||||
unsandboxPublicKeyInput.value = localStorage.getItem("unsandboxPublicKey") || "";
|
||||
unsandboxPublicKeyInput.value = getVaultOrStorage("unsandboxPublicKey", "");
|
||||
unsandboxPublicKeyInput.className = "uncloseai-input";
|
||||
|
||||
unsandboxPublicKeyContainer.appendChild(unsandboxPublicKeyInput);
|
||||
|
|
@ -612,7 +940,7 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
const unsandboxSecretKeyInput = document.createElement("input");
|
||||
unsandboxSecretKeyInput.type = "password";
|
||||
unsandboxSecretKeyInput.placeholder = getUIText("unsandboxSecretKey");
|
||||
unsandboxSecretKeyInput.value = localStorage.getItem("unsandboxSecretKey") || "";
|
||||
unsandboxSecretKeyInput.value = getVaultOrStorage("unsandboxSecretKey", "");
|
||||
unsandboxSecretKeyInput.className = "uncloseai-input";
|
||||
|
||||
unsandboxSecretKeyContainer.appendChild(unsandboxSecretKeyInput);
|
||||
|
|
@ -627,16 +955,16 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
// Event handlers for unsandbox
|
||||
unsandboxCheckbox.onchange = () => {
|
||||
const useUnsandbox = unsandboxCheckbox.checked;
|
||||
localStorage.setItem("useUnsandbox", useUnsandbox.toString());
|
||||
setVaultValue("useUnsandbox", useUnsandbox);
|
||||
unsandboxInputs.style.display = useUnsandbox ? "block" : "none";
|
||||
};
|
||||
|
||||
unsandboxPublicKeyInput.onchange = () => {
|
||||
localStorage.setItem("unsandboxPublicKey", unsandboxPublicKeyInput.value);
|
||||
setVaultValue("unsandboxPublicKey", unsandboxPublicKeyInput.value);
|
||||
};
|
||||
|
||||
unsandboxSecretKeyInput.onchange = () => {
|
||||
localStorage.setItem("unsandboxSecretKey", unsandboxSecretKeyInput.value);
|
||||
setVaultValue("unsandboxSecretKey", unsandboxSecretKeyInput.value);
|
||||
};
|
||||
|
||||
unsandboxSection.appendChild(unsandboxToggle);
|
||||
|
|
@ -725,12 +1053,20 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
// Quick actions span both columns
|
||||
actionsSection.className = "uncloseai-section uncloseai-actions-full-width";
|
||||
|
||||
settingsPanel.appendChild(actionsSection);
|
||||
settingsPanel.appendChild(leftColumn);
|
||||
settingsPanel.appendChild(rightColumn);
|
||||
// Wrap settings in settingsContent container (shown/hidden based on vault state)
|
||||
settingsContent.appendChild(actionsSection);
|
||||
settingsContent.appendChild(leftColumn);
|
||||
settingsContent.appendChild(rightColumn);
|
||||
settingsContent.style.display = 'grid';
|
||||
settingsContent.style.gridTemplateColumns = 'repeat(auto-fit, minmax(200px, 1fr))';
|
||||
settingsContent.style.gap = '16px';
|
||||
|
||||
// Add vault section first, then settings content
|
||||
settingsPanel.appendChild(vaultSection);
|
||||
settingsPanel.appendChild(settingsContent);
|
||||
|
||||
// Load and save settings panel state
|
||||
let settingsOpen = localStorage.getItem("hermesSettingsOpen") === "true";
|
||||
let settingsOpen = getVaultOrStorage("hermesSettingsOpen") === true || getVaultOrStorage("hermesSettingsOpen") === "true";
|
||||
if (settingsOpen) {
|
||||
settingsPanel.classList.add("open");
|
||||
}
|
||||
|
|
@ -738,7 +1074,7 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
menuBtn.onclick = () => {
|
||||
settingsOpen = !settingsOpen;
|
||||
settingsPanel.classList.toggle("open", settingsOpen);
|
||||
localStorage.setItem("hermesSettingsOpen", settingsOpen.toString());
|
||||
setVaultValue("hermesSettingsOpen", settingsOpen);
|
||||
};
|
||||
|
||||
// Set close button handler now that settings panel is available
|
||||
|
|
@ -747,10 +1083,10 @@ async function openUncloseaiEmbeddedModalNew() {
|
|||
if (settingsOpen) {
|
||||
settingsOpen = false;
|
||||
settingsPanel.classList.toggle("open", settingsOpen);
|
||||
localStorage.setItem("hermesSettingsOpen", settingsOpen.toString());
|
||||
setVaultValue("hermesSettingsOpen", settingsOpen);
|
||||
return; // Don't close modal yet
|
||||
}
|
||||
|
||||
|
||||
// Settings closed or not open, close the modal
|
||||
modal.close();
|
||||
document.body.removeChild(modal);
|
||||
|
|
|
|||
527
public/src/vault.js
Normal file
527
public/src/vault.js
Normal file
|
|
@ -0,0 +1,527 @@
|
|||
/**
|
||||
* UncloseAI Vault - Encrypted Settings Storage
|
||||
*
|
||||
* All settings are encrypted with a user-chosen password using AES-256.
|
||||
* Session persistence allows staying unlocked across page navigations.
|
||||
*
|
||||
* Requires CryptoJS library.
|
||||
*
|
||||
* Flow:
|
||||
* 1. User creates vault with password (first time)
|
||||
* 2. Settings encrypted with password, stored in localStorage
|
||||
* 3. Session key derived and stored (encrypted) for auto-unlock
|
||||
* 4. On page load, session key auto-unlocks if not expired
|
||||
* 5. User can explicitly lock or session expires after TTL
|
||||
*/
|
||||
|
||||
const UncloseVault = {
|
||||
// localStorage keys
|
||||
VAULT_KEY: 'uncloseai_vault',
|
||||
SALT_KEY: 'uncloseai_device_salt',
|
||||
SESSION_KEY: 'uncloseai_session',
|
||||
|
||||
// Session TTL (7 days in milliseconds)
|
||||
SESSION_TTL_MS: 7 * 24 * 60 * 60 * 1000,
|
||||
|
||||
// In-memory state
|
||||
_settings: null,
|
||||
_password: null,
|
||||
|
||||
/**
|
||||
* Check if CryptoJS is available
|
||||
*/
|
||||
isAvailable() {
|
||||
return typeof CryptoJS !== 'undefined';
|
||||
},
|
||||
|
||||
/**
|
||||
* Get or create device-specific salt (32 bytes)
|
||||
*/
|
||||
getDeviceSalt() {
|
||||
let salt = localStorage.getItem(this.SALT_KEY);
|
||||
if (!salt) {
|
||||
if (!this.isAvailable()) return null;
|
||||
const randomBytes = CryptoJS.lib.WordArray.random(32);
|
||||
salt = randomBytes.toString();
|
||||
localStorage.setItem(this.SALT_KEY, salt);
|
||||
}
|
||||
return salt;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate vault ID from password (deterministic per device)
|
||||
*/
|
||||
getVaultId(password) {
|
||||
if (!this.isAvailable()) return null;
|
||||
const salt = this.getDeviceSalt();
|
||||
return CryptoJS.SHA256(password + salt).toString();
|
||||
},
|
||||
|
||||
/**
|
||||
* Encrypt data with key
|
||||
*/
|
||||
encrypt(data, key) {
|
||||
if (!this.isAvailable()) return null;
|
||||
try {
|
||||
return CryptoJS.AES.encrypt(JSON.stringify(data), key).toString();
|
||||
} catch (e) {
|
||||
console.error('Vault encryption failed:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Decrypt data with key
|
||||
*/
|
||||
decrypt(encryptedData, key) {
|
||||
if (!this.isAvailable()) return null;
|
||||
try {
|
||||
const bytes = CryptoJS.AES.decrypt(encryptedData, key);
|
||||
const decryptedStr = bytes.toString(CryptoJS.enc.Utf8);
|
||||
if (!decryptedStr) return null;
|
||||
return JSON.parse(decryptedStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a vault exists
|
||||
*/
|
||||
hasVault() {
|
||||
const vaultData = localStorage.getItem(this.VAULT_KEY);
|
||||
if (!vaultData) return false;
|
||||
try {
|
||||
const vault = JSON.parse(vaultData);
|
||||
return !!vault.encrypted_settings;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if vault is currently unlocked (in memory)
|
||||
*/
|
||||
isUnlocked() {
|
||||
return this._settings !== null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get vault data from localStorage
|
||||
*/
|
||||
getVault() {
|
||||
try {
|
||||
const vaultJson = localStorage.getItem(this.VAULT_KEY);
|
||||
return vaultJson ? JSON.parse(vaultJson) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save vault data to localStorage
|
||||
*/
|
||||
saveVault(vault) {
|
||||
localStorage.setItem(this.VAULT_KEY, JSON.stringify(vault));
|
||||
},
|
||||
|
||||
/**
|
||||
* Create session for auto-unlock persistence
|
||||
* Stores the password encrypted with a random session key
|
||||
*/
|
||||
createSession(password) {
|
||||
if (!this.isAvailable()) return;
|
||||
|
||||
try {
|
||||
// Generate random session key
|
||||
const sessionKey = CryptoJS.lib.WordArray.random(32).toString();
|
||||
|
||||
// Encrypt password with session key
|
||||
const encryptedPassword = CryptoJS.AES.encrypt(password, sessionKey).toString();
|
||||
|
||||
// Store session data
|
||||
const sessionData = {
|
||||
key: sessionKey,
|
||||
data: encryptedPassword,
|
||||
expires: Date.now() + this.SESSION_TTL_MS,
|
||||
created: Date.now()
|
||||
};
|
||||
|
||||
localStorage.setItem(this.SESSION_KEY, JSON.stringify(sessionData));
|
||||
} catch (e) {
|
||||
console.error('Failed to create session:', e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Try to restore session (auto-unlock)
|
||||
* Returns true if session was valid and vault unlocked
|
||||
*/
|
||||
tryRestoreSession() {
|
||||
if (!this.isAvailable()) return false;
|
||||
if (this.isUnlocked()) return true; // Already unlocked
|
||||
|
||||
try {
|
||||
const sessionJson = localStorage.getItem(this.SESSION_KEY);
|
||||
if (!sessionJson) return false;
|
||||
|
||||
const session = JSON.parse(sessionJson);
|
||||
|
||||
// Check expiry
|
||||
if (Date.now() > session.expires) {
|
||||
this.clearSession();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decrypt password from session
|
||||
const bytes = CryptoJS.AES.decrypt(session.data, session.key);
|
||||
const password = bytes.toString(CryptoJS.enc.Utf8);
|
||||
|
||||
if (!password) {
|
||||
this.clearSession();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to unlock with recovered password
|
||||
const result = this._unlockWithPassword(password, false); // Don't recreate session
|
||||
|
||||
if (result.success) {
|
||||
// Refresh session expiry on successful restore
|
||||
session.expires = Date.now() + this.SESSION_TTL_MS;
|
||||
localStorage.setItem(this.SESSION_KEY, JSON.stringify(session));
|
||||
return true;
|
||||
}
|
||||
|
||||
this.clearSession();
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error('Session restore failed:', e);
|
||||
this.clearSession();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear session data
|
||||
*/
|
||||
clearSession() {
|
||||
localStorage.removeItem(this.SESSION_KEY);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get session info (for UI display)
|
||||
*/
|
||||
getSessionInfo() {
|
||||
try {
|
||||
const sessionJson = localStorage.getItem(this.SESSION_KEY);
|
||||
if (!sessionJson) return null;
|
||||
|
||||
const session = JSON.parse(sessionJson);
|
||||
const now = Date.now();
|
||||
|
||||
if (now > session.expires) return null;
|
||||
|
||||
return {
|
||||
expiresAt: new Date(session.expires),
|
||||
expiresIn: session.expires - now,
|
||||
createdAt: new Date(session.created)
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Internal unlock with password
|
||||
*/
|
||||
_unlockWithPassword(password, createNewSession = true) {
|
||||
if (!this.isAvailable()) {
|
||||
return { success: false, error: 'CryptoJS not available' };
|
||||
}
|
||||
|
||||
const vault = this.getVault();
|
||||
if (!vault || !vault.encrypted_settings) {
|
||||
return { success: false, error: 'No vault found' };
|
||||
}
|
||||
|
||||
const decrypted = this.decrypt(vault.encrypted_settings, password);
|
||||
if (!decrypted) {
|
||||
return { success: false, error: 'Wrong password' };
|
||||
}
|
||||
|
||||
// Store in memory
|
||||
this._settings = decrypted;
|
||||
this._password = password;
|
||||
|
||||
// Create session for persistence
|
||||
if (createNewSession) {
|
||||
this.createSession(password);
|
||||
}
|
||||
|
||||
// Dispatch event
|
||||
window.dispatchEvent(new CustomEvent('uncloseai-vault-unlocked'));
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new vault with password
|
||||
*/
|
||||
create(password) {
|
||||
if (!this.isAvailable()) {
|
||||
return { success: false, error: 'CryptoJS not available. Include crypto-js library.' };
|
||||
}
|
||||
if (!password || password.length < 8) {
|
||||
return { success: false, error: 'Password must be at least 8 characters' };
|
||||
}
|
||||
|
||||
if (this.hasVault()) {
|
||||
return { success: false, error: 'Vault already exists. Use unlock() instead.' };
|
||||
}
|
||||
|
||||
// Create with default settings
|
||||
const defaultSettings = this.getDefaultSettings();
|
||||
const encrypted = this.encrypt(defaultSettings, password);
|
||||
|
||||
if (!encrypted) {
|
||||
return { success: false, error: 'Encryption failed' };
|
||||
}
|
||||
|
||||
const vault = {
|
||||
encrypted_settings: encrypted,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.saveVault(vault);
|
||||
|
||||
// Unlock in memory
|
||||
this._settings = defaultSettings;
|
||||
this._password = password;
|
||||
|
||||
// Create session
|
||||
this.createSession(password);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('uncloseai-vault-created'));
|
||||
window.dispatchEvent(new CustomEvent('uncloseai-vault-unlocked'));
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Unlock vault with password
|
||||
*/
|
||||
unlock(password) {
|
||||
if (!password) {
|
||||
return { success: false, error: 'Password required' };
|
||||
}
|
||||
return this._unlockWithPassword(password, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Lock the vault (clear memory and session)
|
||||
*/
|
||||
lock() {
|
||||
this._settings = null;
|
||||
this._password = null;
|
||||
this.clearSession();
|
||||
window.dispatchEvent(new CustomEvent('uncloseai-vault-locked'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Save current settings to vault
|
||||
*/
|
||||
save() {
|
||||
if (!this.isUnlocked()) {
|
||||
console.error('Cannot save: vault not unlocked');
|
||||
return false;
|
||||
}
|
||||
|
||||
const vault = this.getVault() || {};
|
||||
const encrypted = this.encrypt(this._settings, this._password);
|
||||
|
||||
if (!encrypted) {
|
||||
console.error('Failed to encrypt settings');
|
||||
return false;
|
||||
}
|
||||
|
||||
vault.encrypted_settings = encrypted;
|
||||
vault.updated_at = new Date().toISOString();
|
||||
this.saveVault(vault);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a setting value (returns default if vault locked)
|
||||
*/
|
||||
get(key, defaultValue = null) {
|
||||
if (!this.isUnlocked()) {
|
||||
// Return from defaults if not unlocked
|
||||
const defaults = this.getDefaultSettings();
|
||||
return defaults[key] !== undefined ? defaults[key] : defaultValue;
|
||||
}
|
||||
return this._settings[key] !== undefined ? this._settings[key] : defaultValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a setting value (requires unlocked vault)
|
||||
*/
|
||||
set(key, value) {
|
||||
if (!this.isUnlocked()) {
|
||||
console.warn('Vault locked - setting not saved');
|
||||
return false;
|
||||
}
|
||||
this._settings[key] = value;
|
||||
return this.save();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set multiple settings at once
|
||||
*/
|
||||
setMultiple(settingsObj) {
|
||||
if (!this.isUnlocked()) {
|
||||
console.warn('Vault locked - settings not saved');
|
||||
return false;
|
||||
}
|
||||
Object.assign(this._settings, settingsObj);
|
||||
return this.save();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all settings
|
||||
*/
|
||||
getAll() {
|
||||
if (!this.isUnlocked()) return this.getDefaultSettings();
|
||||
return { ...this._settings };
|
||||
},
|
||||
|
||||
/**
|
||||
* Default settings (used when vault doesn't exist or is locked)
|
||||
*/
|
||||
getDefaultSettings() {
|
||||
return {
|
||||
// Language
|
||||
language: 'en',
|
||||
|
||||
// Model selection
|
||||
selectedModel: null,
|
||||
selectedEndpoint: null,
|
||||
selectedVoice: 'tts-1:onyx',
|
||||
|
||||
// Custom API
|
||||
useCustomAPI: false,
|
||||
customBaseURL: '',
|
||||
customAPIKey: '',
|
||||
|
||||
// Unsandbox code execution
|
||||
useUnsandbox: false,
|
||||
unsandboxPublicKey: '',
|
||||
unsandboxSecretKey: '',
|
||||
|
||||
// UI preferences
|
||||
settingsOpen: false
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete vault entirely (requires password confirmation)
|
||||
*/
|
||||
deleteVault(password) {
|
||||
if (!this.isAvailable()) {
|
||||
return { success: false, error: 'CryptoJS not available' };
|
||||
}
|
||||
|
||||
const vault = this.getVault();
|
||||
if (!vault) {
|
||||
return { success: false, error: 'No vault exists' };
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const decrypted = this.decrypt(vault.encrypted_settings, password);
|
||||
if (!decrypted) {
|
||||
return { success: false, error: 'Wrong password' };
|
||||
}
|
||||
|
||||
// Clear everything
|
||||
localStorage.removeItem(this.VAULT_KEY);
|
||||
this.clearSession();
|
||||
this._settings = null;
|
||||
this._password = null;
|
||||
|
||||
window.dispatchEvent(new CustomEvent('uncloseai-vault-deleted'));
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Change vault password
|
||||
*/
|
||||
changePassword(currentPassword, newPassword) {
|
||||
if (!this.isAvailable()) {
|
||||
return { success: false, error: 'CryptoJS not available' };
|
||||
}
|
||||
if (!newPassword || newPassword.length < 8) {
|
||||
return { success: false, error: 'New password must be at least 8 characters' };
|
||||
}
|
||||
|
||||
const vault = this.getVault();
|
||||
if (!vault) {
|
||||
return { success: false, error: 'No vault exists' };
|
||||
}
|
||||
|
||||
// Decrypt with current password
|
||||
const decrypted = this.decrypt(vault.encrypted_settings, currentPassword);
|
||||
if (!decrypted) {
|
||||
return { success: false, error: 'Wrong current password' };
|
||||
}
|
||||
|
||||
// Re-encrypt with new password
|
||||
const encrypted = this.encrypt(decrypted, newPassword);
|
||||
if (!encrypted) {
|
||||
return { success: false, error: 'Re-encryption failed' };
|
||||
}
|
||||
|
||||
vault.encrypted_settings = encrypted;
|
||||
vault.updated_at = new Date().toISOString();
|
||||
this.saveVault(vault);
|
||||
|
||||
// Update in-memory and session
|
||||
this._password = newPassword;
|
||||
this.createSession(newPassword);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize - try to restore session on page load
|
||||
* Call this when the page loads
|
||||
*/
|
||||
init() {
|
||||
if (!this.isAvailable()) {
|
||||
console.warn('UncloseVault: CryptoJS not available');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.hasVault()) {
|
||||
// Try auto-unlock from session
|
||||
if (this.tryRestoreSession()) {
|
||||
console.log('UncloseVault: Session restored, vault unlocked');
|
||||
return true;
|
||||
}
|
||||
console.log('UncloseVault: Vault exists but locked (no valid session)');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('UncloseVault: No vault exists yet');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Export for ES modules
|
||||
export { UncloseVault };
|
||||
|
||||
// Also make available globally
|
||||
if (typeof window !== 'undefined') {
|
||||
window.UncloseVault = UncloseVault;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue