| … | … | @@ -8,7 +8,7 @@ |
| 8 | 8 | "purpose": "Seeding pear://18x3enb54ttcr6uuhg5hh1cqzwi8ydnm658fmgyyjkzoqxd1ybso", |
| 9 | 9 | "command": "pear seed pear://18x3enb54ttcr6uuhg5hh1cqzwi8ydnm658fmgyyjkzoqxd1ybso .", |
| 10 | 10 | "persistent": true, |
| 11 | "latest_version": "11468", | |
| 11 | "latest_version": "11472", | |
| 12 | 12 | "last_deployed": "2026-01-27", |
| 13 | 13 | "features": "Phase 2 COMPLETE: Auth + Encryption + Validation + Rate Limiting + CORS + Signed Invites + Password Unlock UI ✅" |
| 14 | 14 | }, |
| … | … | @@ -2423,6 +2423,164 @@ async function promptForPassword() { |
| 2423 | 2423 | }) |
| 2424 | 2424 | } |
| 2425 | 2425 | |
| 2426 | // Show change password dialog | |
| 2427 | function showChangePassword() { | |
| 2428 | const dialog = document.getElementById('change-password-dialog') | |
| 2429 | const currentInput = document.getElementById('current-password-input') | |
| 2430 | const newInput = document.getElementById('new-password-input') | |
| 2431 | const confirmInput = document.getElementById('new-password-confirm-input') | |
| 2432 | const matchError = document.getElementById('change-password-match-error') | |
| 2433 | const strengthDiv = document.getElementById('change-password-strength') | |
| 2434 | const strengthText = document.getElementById('change-password-strength-text') | |
| 2435 | const strengthBar = document.getElementById('change-password-strength-bar') | |
| 2436 | const submitBtn = document.getElementById('change-password-submit-btn') | |
| 2437 | const cancelBtn = document.getElementById('change-password-cancel-btn') | |
| 2438 | const errorDiv = document.getElementById('change-password-error') | |
| 2439 | ||
| 2440 | // Password strength calculator (reuse from unlock dialog) | |
| 2441 | function checkPasswordStrength(password) { | |
| 2442 | if (!password) return { strength: 0, text: 'Too short', color: '#d32f2f' } | |
| 2443 | ||
| 2444 | let strength = 0 | |
| 2445 | if (password.length >= 8) strength += 25 | |
| 2446 | if (password.length >= 12) strength += 25 | |
| 2447 | if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength += 20 | |
| 2448 | if (/\d/.test(password)) strength += 15 | |
| 2449 | if (/[^a-zA-Z0-9]/.test(password)) strength += 15 | |
| 2450 | ||
| 2451 | if (strength < 40) return { strength, text: 'Weak', color: '#d32f2f' } | |
| 2452 | if (strength < 60) return { strength, text: 'Fair', color: '#ff9800' } | |
| 2453 | if (strength < 80) return { strength, text: 'Good', color: '#2196f3' } | |
| 2454 | return { strength, text: 'Strong', color: '#4caf50' } | |
| 2455 | } | |
| 2456 | ||
| 2457 | // Validation function | |
| 2458 | function validateInputs() { | |
| 2459 | const current = currentInput.value | |
| 2460 | const newPassword = newInput.value | |
| 2461 | const confirm = confirmInput.value | |
| 2462 | ||
| 2463 | // Update strength indicator | |
| 2464 | const { strength, text, color } = checkPasswordStrength(newPassword) | |
| 2465 | strengthText.textContent = text | |
| 2466 | strengthText.style.color = color | |
| 2467 | strengthBar.style.width = `${strength}%` | |
| 2468 | strengthBar.style.background = color | |
| 2469 | ||
| 2470 | // Check all fields filled | |
| 2471 | if (!current || newPassword.length < 8 || !confirm) { | |
| 2472 | submitBtn.disabled = true | |
| 2473 | return | |
| 2474 | } | |
| 2475 | ||
| 2476 | // Check password match | |
| 2477 | if (newPassword !== confirm) { | |
| 2478 | matchError.style.display = 'block' | |
| 2479 | submitBtn.disabled = true | |
| 2480 | return | |
| 2481 | } else { | |
| 2482 | matchError.style.display = 'none' | |
| 2483 | } | |
| 2484 | ||
| 2485 | // Check new password different from current | |
| 2486 | if (newPassword === current) { | |
| 2487 | matchError.textContent = '❌ New password must be different from current' | |
| 2488 | matchError.style.display = 'block' | |
| 2489 | submitBtn.disabled = true | |
| 2490 | return | |
| 2491 | } | |
| 2492 | ||
| 2493 | matchError.textContent = '❌ New passwords do not match' | |
| 2494 | matchError.style.display = 'none' | |
| 2495 | ||
| 2496 | // All validations passed | |
| 2497 | submitBtn.disabled = false | |
| 2498 | } | |
| 2499 | ||
| 2500 | // Add input listeners | |
| 2501 | currentInput.addEventListener('input', validateInputs) | |
| 2502 | newInput.addEventListener('input', validateInputs) | |
| 2503 | confirmInput.addEventListener('input', validateInputs) | |
| 2504 | ||
| 2505 | // Handle form submission | |
| 2506 | async function handleSubmit() { | |
| 2507 | const currentPassword = currentInput.value | |
| 2508 | const newPassword = newInput.value | |
| 2509 | ||
| 2510 | try { | |
| 2511 | submitBtn.disabled = true | |
| 2512 | submitBtn.textContent = 'Changing Password...' | |
| 2513 | errorDiv.style.display = 'none' | |
| 2514 | ||
| 2515 | // Call change password endpoint | |
| 2516 | const res = await authFetch(`${API_BASE}/change-password`, { | |
| 2517 | method: 'POST', | |
| 2518 | headers: { 'Content-Type': 'application/json' }, | |
| 2519 | body: JSON.stringify({ | |
| 2520 | currentPassword, | |
| 2521 | newPassword | |
| 2522 | }) | |
| 2523 | }) | |
| 2524 | ||
| 2525 | if (!res.ok) { | |
| 2526 | const error = await res.json() | |
| 2527 | throw new Error(error.error || 'Password change failed') | |
| 2528 | } | |
| 2529 | ||
| 2530 | const data = await res.json() | |
| 2531 | ||
| 2532 | // Close dialog | |
| 2533 | dialog.close() | |
| 2534 | ||
| 2535 | // Show success | |
| 2536 | alert('✅ Password changed successfully!\n\nYour mnemonics have been re-encrypted with the new password.\n\nYou\'ll need the new password on next login.') | |
| 2537 | showSuccess('Password changed successfully') | |
| 2538 | ||
| 2539 | } catch (err) { | |
| 2540 | console.error('Change password error:', err) | |
| 2541 | errorDiv.textContent = err.message | |
| 2542 | errorDiv.style.display = 'block' | |
| 2543 | submitBtn.textContent = 'Change Password' | |
| 2544 | submitBtn.disabled = false | |
| 2545 | } | |
| 2546 | } | |
| 2547 | ||
| 2548 | // Button handlers | |
| 2549 | submitBtn.onclick = handleSubmit | |
| 2550 | cancelBtn.onclick = () => { | |
| 2551 | dialog.close() | |
| 2552 | } | |
| 2553 | ||
| 2554 | // Enter key submits | |
| 2555 | currentInput.addEventListener('keydown', (e) => { | |
| 2556 | if (e.key === 'Enter' && !submitBtn.disabled) { | |
| 2557 | handleSubmit() | |
| 2558 | } | |
| 2559 | }) | |
| 2560 | newInput.addEventListener('keydown', (e) => { | |
| 2561 | if (e.key === 'Enter' && !submitBtn.disabled) { | |
| 2562 | handleSubmit() | |
| 2563 | } | |
| 2564 | }) | |
| 2565 | confirmInput.addEventListener('keydown', (e) => { | |
| 2566 | if (e.key === 'Enter' && !submitBtn.disabled) { | |
| 2567 | handleSubmit() | |
| 2568 | } | |
| 2569 | }) | |
| 2570 | ||
| 2571 | // Reset and show dialog | |
| 2572 | currentInput.value = '' | |
| 2573 | newInput.value = '' | |
| 2574 | confirmInput.value = '' | |
| 2575 | matchError.style.display = 'none' | |
| 2576 | errorDiv.style.display = 'none' | |
| 2577 | submitBtn.textContent = 'Change Password' | |
| 2578 | submitBtn.disabled = true | |
| 2579 | ||
| 2580 | dialog.showModal() | |
| 2581 | currentInput.focus() | |
| 2582 | } | |
| 2583 | ||
| 2426 | 2584 | // Initialize on page load |
| 2427 | 2585 | async function init() { |
| 2428 | 2586 | console.log('🚀 Hyper Wallet UI initialized') |
| … | … | @@ -421,6 +421,30 @@ |
| 421 | 421 | </div> |
| 422 | 422 | </section> |
| 423 | 423 | |
| 424 | <!-- Security Section --> | |
| 425 | <section class="settings-section"> | |
| 426 | <h4>🔒 Security</h4> | |
| 427 | <div class="settings-card"> | |
| 428 | <div class="settings-info"> | |
| 429 | <p>Your wallet mnemonics are encrypted with a password.</p> | |
| 430 | <p>You can change your password below. You'll need your current password to make changes.</p> | |
| 431 | </div> | |
| 432 | <div class="settings-row"> | |
| 433 | <span class="settings-label">Encryption:</span> | |
| 434 | <span class="settings-value"> | |
| 435 | <span class="status-badge status-active">Enabled</span> | |
| 436 | </span> | |
| 437 | </div> | |
| 438 | <div class="settings-row"> | |
| 439 | <span class="settings-label">Password:</span> | |
| 440 | <span class="settings-value monospace">••••••••</span> | |
| 441 | </div> | |
| 442 | </div> | |
| 443 | <div class="settings-actions"> | |
| 444 | <button class="btn-secondary btn-small" onclick="showChangePassword()">🔑 Change Password</button> | |
| 445 | </div> | |
| 446 | </section> | |
| 447 | ||
| 424 | 448 | <!-- Wallet Backup Section --> |
| 425 | 449 | <section class="settings-section"> |
| 426 | 450 | <h4>💾 Backup & Recovery</h4> |
| … | … | @@ -800,6 +824,81 @@ |
| 800 | 824 | </div> |
| 801 | 825 | </dialog> |
| 802 | 826 | |
| 827 | <!-- Change Password Dialog --> | |
| 828 | <dialog id="change-password-dialog"> | |
| 829 | <div class="modal-header"> | |
| 830 | <h3>🔑 Change Password</h3> | |
| 831 | </div> | |
| 832 | <div class="modal-body"> | |
| 833 | <div class="warning-box" style="background: #fff3cd; border: 2px solid #ffc107; padding: 15px; border-radius: 8px; margin-bottom: 20px;"> | |
| 834 | <strong style="color: #856404;">⚠️ IMPORTANT:</strong> | |
| 835 | <ul style="margin: 10px 0; padding-left: 20px; text-align: left; color: #856404;"> | |
| 836 | <li>You'll need your <strong>current password</strong> to change it</li> | |
| 837 | <li><strong>Remember your new password</strong> - it cannot be recovered</li> | |
| 838 | <li>All devices will need the new password on next access</li> | |
| 839 | </ul> | |
| 840 | </div> | |
| 841 | ||
| 842 | <div class="form-group"> | |
| 843 | <label for="current-password-input">Current Password:</label> | |
| 844 | <input | |
| 845 | type="password" | |
| 846 | id="current-password-input" | |
| 847 | class="input-field" | |
| 848 | placeholder="Enter current password" | |
| 849 | autocomplete="current-password" | |
| 850 | style="width: 100%; padding: 10px; font-size: 16px; border: 2px solid #ddd; border-radius: 6px;" | |
| 851 | /> | |
| 852 | </div> | |
| 853 | ||
| 854 | <div class="form-group"> | |
| 855 | <label for="new-password-input">New Password (min 8 characters):</label> | |
| 856 | <input | |
| 857 | type="password" | |
| 858 | id="new-password-input" | |
| 859 | class="input-field" | |
| 860 | placeholder="Enter new password" | |
| 861 | autocomplete="new-password" | |
| 862 | style="width: 100%; padding: 10px; font-size: 16px; border: 2px solid #ddd; border-radius: 6px;" | |
| 863 | /> | |
| 864 | </div> | |
| 865 | ||
| 866 | <div class="form-group"> | |
| 867 | <label for="new-password-confirm-input">Confirm New Password:</label> | |
| 868 | <input | |
| 869 | type="password" | |
| 870 | id="new-password-confirm-input" | |
| 871 | class="input-field" | |
| 872 | placeholder="Re-enter new password" | |
| 873 | autocomplete="new-password" | |
| 874 | style="width: 100%; padding: 10px; font-size: 16px; border: 2px solid #ddd; border-radius: 6px;" | |
| 875 | /> | |
| 876 | </div> | |
| 877 | ||
| 878 | <div id="change-password-match-error" style="color: #d32f2f; font-weight: bold; display: none; margin-top: 10px;"> | |
| 879 | ❌ New passwords do not match | |
| 880 | </div> | |
| 881 | ||
| 882 | <div id="change-password-strength" style="margin-top: 15px;"> | |
| 883 | <div style="display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 14px;"> | |
| 884 | <span>Strength:</span> | |
| 885 | <span id="change-password-strength-text" style="font-weight: bold;">Weak</span> | |
| 886 | </div> | |
| 887 | <div style="height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden;"> | |
| 888 | <div id="change-password-strength-bar" style="height: 100%; width: 0%; transition: width 0.3s, background 0.3s;"></div> | |
| 889 | </div> | |
| 890 | </div> | |
| 891 | ||
| 892 | <div class="button-row" style="margin-top: 25px; display: flex; gap: 10px; justify-content: flex-end;"> | |
| 893 | <button id="change-password-cancel-btn" class="secondary-btn" style="padding: 10px 20px; font-size: 16px;">Cancel</button> | |
| 894 | <button id="change-password-submit-btn" class="primary-btn" disabled style="padding: 10px 20px; font-size: 16px;">Change Password</button> | |
| 895 | </div> | |
| 896 | ||
| 897 | <div id="change-password-error" style="margin-top: 15px; color: #d32f2f; font-weight: bold; text-align: center; display: none;"> | |
| 898 | </div> | |
| 899 | </div> | |
| 900 | </dialog> | |
| 901 | ||
| 803 | 902 | <!-- Error Toast --> |
| 804 | 903 | <div id="error-toast" class="toast"></div> |
| 805 | 904 |
| … | … | @@ -1397,6 +1397,53 @@ const routes = { |
| 1397 | 1397 | } |
| 1398 | 1398 | }, |
| 1399 | 1399 | |
| 1400 | // POST /api/change-password - Change wallet encryption password | |
| 1401 | 'POST /api/change-password': requireAuth(async (req, res) => { | |
| 1402 | try { | |
| 1403 | const body = await parseBody(req) | |
| 1404 | const { currentPassword, newPassword } = body | |
| 1405 | ||
| 1406 | // Validate inputs | |
| 1407 | if (!currentPassword || !newPassword) { | |
| 1408 | return sendError(res, 'Both current and new passwords required', 400) | |
| 1409 | } | |
| 1410 | ||
| 1411 | if (newPassword.length < 8) { | |
| 1412 | return sendError(res, 'New password must be at least 8 characters', 400) | |
| 1413 | } | |
| 1414 | ||
| 1415 | if (currentPassword === newPassword) { | |
| 1416 | return sendError(res, 'New password must be different from current password', 400) | |
| 1417 | } | |
| 1418 | ||
| 1419 | // Verify current password by trying to decrypt | |
| 1420 | let identityMnemonic, walletMnemonic | |
| 1421 | try { | |
| 1422 | identityMnemonic = loadIdentityMnemonic(currentPassword) | |
| 1423 | walletMnemonic = loadWalletMnemonic(currentPassword) | |
| 1424 | } catch (err) { | |
| 1425 | return sendError(res, 'Current password is incorrect', 401) | |
| 1426 | } | |
| 1427 | ||
| 1428 | // Re-encrypt with new password | |
| 1429 | saveIdentityMnemonic(identityMnemonic, newPassword) | |
| 1430 | saveWalletMnemonic(walletMnemonic, newPassword) | |
| 1431 | ||
| 1432 | // Update cached password | |
| 1433 | setCachedPassword(newPassword) | |
| 1434 | ||
| 1435 | console.log('🔑 Password changed successfully') | |
| 1436 | ||
| 1437 | sendJSON(res, { | |
| 1438 | success: true, | |
| 1439 | message: 'Password changed successfully. Mnemonics re-encrypted.' | |
| 1440 | }) | |
| 1441 | } catch (err) { | |
| 1442 | console.error('Change password error:', err) | |
| 1443 | sendError(res, err.message) | |
| 1444 | } | |
| 1445 | }), | |
| 1446 | ||
| 1400 | 1447 | // POST /api/unlock - Unlock existing wallet with password (caches password in memory) |
| 1401 | 1448 | 'POST /api/unlock': async (req, res) => { |
| 1402 | 1449 | try { |