OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet commits / e29b93e

Phase 2: Add password change functionality in settings
- Added Security section to Settings modal - Change Password button with professional dialog - Current password verification required - New password with strength indicator and confirmation - Backend endpoint POST /api/change-password - Re-encrypts mnemonics with new password - Updates cached password in server memory - Complete password management UX Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
e29b93e6fc6b0eecfe8653e676e9783d30f7dbf9
by plankatron <fleeky@gmail.com> · 2026-01-27T21:08:59.000Z

Changed files (4) +305 -1

.claude/tmux-registry.json +1 -1
……@@ -8,7 +8,7 @@
88 "purpose": "Seeding pear://18x3enb54ttcr6uuhg5hh1cqzwi8ydnm658fmgyyjkzoqxd1ybso",
99 "command": "pear seed pear://18x3enb54ttcr6uuhg5hh1cqzwi8ydnm658fmgyyjkzoqxd1ybso .",
1010 "persistent": true,
11 "latest_version": "11468",
11 "latest_version": "11472",
1212 "last_deployed": "2026-01-27",
1313 "features": "Phase 2 COMPLETE: Auth + Encryption + Validation + Rate Limiting + CORS + Signed Invites + Password Unlock UI ✅"
1414 },
ui/app.js +158 -0
……@@ -2423,6 +2423,164 @@ async function promptForPassword() {
24232423 })
24242424}
24252425
2426// Show change password dialog
2427function 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
24262584// Initialize on page load
24272585async function init() {
24282586 console.log('🚀 Hyper Wallet UI initialized')
ui/index.html +99 -0
……@@ -421,6 +421,30 @@
421421 </div>
422422 </section>
423423
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
424448 <!-- Wallet Backup Section -->
425449 <section class="settings-section">
426450 <h4>💾 Backup & Recovery</h4>
……@@ -800,6 +824,81 @@
800824 </div>
801825 </dialog>
802826
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
803902 <!-- Error Toast -->
804903 <div id="error-toast" class="toast"></div>
805904
wallet-server.cjs +47 -0
……@@ -1397,6 +1397,53 @@ const routes = {
13971397 }
13981398 },
13991399
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
14001447 // POST /api/unlock - Unlock existing wallet with password (caches password in memory)
14011448 'POST /api/unlock': async (req, res) => {
14021449 try {