OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet commits / fd2f326

Phase 2.2 (Part 2): Complete mnemonic encryption integration
COMPLETE: Password-based encryption for mnemonics at rest Web UI Changes (index.html): - Added password input section to create wallet screen - Added password confirmation fields - Added password input section to restore wallet screen - Added encryption warnings and help text - Password section shown before mnemonic generation Web UI Changes (app.js): - Updated generateNewWallet() to validate password and send to API - Updated restoreWallet() to validate password and send to API - Updated exportIdentity() to prompt for password (changed to POST) - Updated exportWalletMnemonic() to prompt for password (changed to POST) - Password validation: min 8 chars, must match confirmation - User-friendly error messages for password mismatch Backend Changes (wallet-server.cjs): - Updated all loadIdentityMnemonic() calls to use Cached version - Updated all loadWalletMnemonic() calls to use Cached version - Server startup handles encrypted mnemonics gracefully - Password cached in memory after successful init/restore Flow: 1. User sets password during init/restore 2. Password sent to backend in request body 3. Backend encrypts mnemonics with password before saving 4. Backend caches password in memory for session 5. Subsequent operations use cached password 6. Export operations require password re-entry Security: - Mnemonics encrypted at rest with AES-256-GCM - Password derived key using scrypt (N=16384) - Password cached in memory only (never persisted) - Password cleared on server restart - Legacy plaintext detection for migration Status: - Phase 2.2 (Mnemonic Encryption): COMPLETE ✅ - Ready for deployment and testing - Next: Phase 2.3 (Secure Device Pairing) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
fd2f32664574f2d2d424ec2d26cb2751df20f957
by plankatron <fleeky@gmail.com> · 2026-01-27T19:30:20.000Z

Changed files (3) +103 -17

ui/app.js +59 -4
……@@ -93,13 +93,33 @@ function showRestoreWallet() {
9393
9494async function generateNewWallet() {
9595 try {
96 // Validate password
97 const password = document.getElementById('create-password').value
98 const passwordConfirm = document.getElementById('create-password-confirm').value
99 const errorDiv = document.getElementById('create-password-error')
100
101 errorDiv.style.display = 'none'
102
103 if (!password || password.length < 8) {
104 errorDiv.textContent = 'Password must be at least 8 characters'
105 errorDiv.style.display = 'block'
106 return
107 }
108
109 if (password !== passwordConfirm) {
110 errorDiv.textContent = 'Passwords do not match'
111 errorDiv.style.display = 'block'
112 return
113 }
114
96115 const btn = document.getElementById('btn-generate-mnemonic')
97116 btn.disabled = true
98117 btn.textContent = 'Generating...'
99118
100119 const res = await authFetch(`${API_BASE}/init`, {
101120 method: 'POST',
102 headers: { 'Content-Type': 'application/json' }
121 headers: { 'Content-Type': 'application/json' },
122 body: JSON.stringify({ password })
103123 })
104124
105125 const data = await res.json()
……@@ -185,6 +205,8 @@ function confirmMnemonicBackup() {
185205async function restoreWallet() {
186206 const identityInput = document.getElementById('restore-identity-mnemonic')
187207 const walletInput = document.getElementById('restore-wallet-mnemonic')
208 const password = document.getElementById('restore-password').value
209 const passwordConfirm = document.getElementById('restore-password-confirm').value
188210 const errorDiv = document.getElementById('restore-error')
189211
190212 const identityMnemonic = identityInput.value.trim()
……@@ -197,13 +219,24 @@ async function restoreWallet() {
197219 return
198220 }
199221
222 if (!password || password.length < 8) {
223 errorDiv.textContent = 'Password must be at least 8 characters'
224 return
225 }
226
227 if (password !== passwordConfirm) {
228 errorDiv.textContent = 'Passwords do not match'
229 return
230 }
231
200232 try {
201233 const res = await authFetch(`${API_BASE}/restore`, {
202234 method: 'POST',
203235 headers: { 'Content-Type': 'application/json' },
204236 body: JSON.stringify({
205237 identityMnemonic,
206 walletMnemonic
238 walletMnemonic,
239 password
207240 })
208241 })
209242
……@@ -547,8 +580,19 @@ async function exportIdentity() {
547580 return
548581 }
549582
583 // Prompt for password
584 const password = prompt('🔒 Enter your encryption password:')
585 if (!password) {
586 showError('Password required to export mnemonic')
587 return
588 }
589
550590 try {
551 const res = await authFetch(`${API_BASE}/export-identity`)
591 const res = await authFetch(`${API_BASE}/export-identity`, {
592 method: 'POST',
593 headers: { 'Content-Type': 'application/json' },
594 body: JSON.stringify({ password })
595 })
552596 const data = await res.json()
553597
554598 if (data.error) {
……@@ -568,8 +612,19 @@ async function exportWalletMnemonic() {
568612 return
569613 }
570614
615 // Prompt for password
616 const password = prompt('🔒 Enter your encryption password:')
617 if (!password) {
618 showError('Password required to export mnemonic')
619 return
620 }
621
571622 try {
572 const res = await authFetch(`${API_BASE}/export-wallet`)
623 const res = await authFetch(`${API_BASE}/export-wallet`, {
624 method: 'POST',
625 headers: { 'Content-Type': 'application/json' },
626 body: JSON.stringify({ password })
627 })
573628 const data = await res.json()
574629
575630 if (data.error) {
ui/index.html +31 -0
……@@ -63,6 +63,24 @@
6363 </label>
6464 </div>
6565 </div>
66
67 <div id="password-section" class="password-section">
68 <h3>🔒 Set Encryption Password</h3>
69 <p class="password-hint">Your recovery phrases will be encrypted with this password. You'll need it to access your wallet.</p>
70 <div class="form-group">
71 <label for="create-password">Password (min 8 characters)</label>
72 <input type="password" id="create-password" placeholder="Enter strong password" minlength="8">
73 </div>
74 <div class="form-group">
75 <label for="create-password-confirm">Confirm Password</label>
76 <input type="password" id="create-password-confirm" placeholder="Re-enter password" minlength="8">
77 </div>
78 <div id="create-password-error" class="form-error" style="display: none;"></div>
79 <div class="password-warning">
80 <p>⚠️ <strong>IMPORTANT</strong>: If you forget this password, you can only restore using your recovery phrases!</p>
81 </div>
82 </div>
83
6684 <div class="setup-actions">
6785 <button class="btn-secondary" onclick="showSetupWelcome()">← Back</button>
6886 <button id="btn-generate-mnemonic" class="btn-primary" onclick="generateNewWallet()">Generate Recovery Phrases</button>
……@@ -89,6 +107,19 @@
89107 <textarea id="restore-wallet-mnemonic" class="mnemonic-input" placeholder="Enter your 12-word wallet mnemonic (separated by spaces)" rows="3"></textarea>
90108 </div>
91109
110 <div class="password-section">
111 <h3>🔒 Set Encryption Password</h3>
112 <p class="password-hint">Set a password to encrypt your restored mnemonics.</p>
113 <div class="form-group">
114 <label for="restore-password">Password (min 8 characters)</label>
115 <input type="password" id="restore-password" placeholder="Enter strong password" minlength="8">
116 </div>
117 <div class="form-group">
118 <label for="restore-password-confirm">Confirm Password</label>
119 <input type="password" id="restore-password-confirm" placeholder="Re-enter password" minlength="8">
120 </div>
121 </div>
122
92123 <div id="restore-error" class="form-error"></div>
93124
94125 <div class="setup-actions">
wallet-server.cjs +13 -13
……@@ -189,7 +189,7 @@ function generateIdentityMnemonic() {
189189
190190// Load identity from mnemonic
191191async function loadIdentity() {
192 const mnemonic = loadIdentityMnemonic()
192 const mnemonic = loadIdentityMnemonicCached()
193193 if (!mnemonic) {
194194 throw new Error('Identity not initialized')
195195 }
……@@ -283,7 +283,7 @@ const routes = {
283283 // GET /api/balance/:chain (protected)
284284 'GET /api/balance/': requireAuth(async (req, res, chain) => {
285285 try {
286 const walletMnemonic = loadWalletMnemonic()
286 const walletMnemonic = loadWalletMnemonicCached()
287287 if (!walletMnemonic) {
288288 return sendError(res, 'Wallet not initialized', 400)
289289 }
……@@ -309,7 +309,7 @@ const routes = {
309309 // GET /api/address/:chain
310310 'GET /api/address/': requireAuth(async (req, res, chain) => {
311311 try {
312 const walletMnemonic = loadWalletMnemonic()
312 const walletMnemonic = loadWalletMnemonicCached()
313313 if (!walletMnemonic) {
314314 return sendError(res, 'Wallet not initialized', 400)
315315 }
……@@ -335,7 +335,7 @@ const routes = {
335335 // GET /api/qrcode/:chain
336336 'GET /api/qrcode/': requireAuth(async (req, res, chain) => {
337337 try {
338 const walletMnemonic = loadWalletMnemonic()
338 const walletMnemonic = loadWalletMnemonicCached()
339339 if (!walletMnemonic) {
340340 return sendError(res, 'Wallet not initialized', 400)
341341 }
……@@ -412,7 +412,7 @@ const routes = {
412412 await initializeStorage(identity)
413413
414414 // Initialize WDK
415 const walletMnemonic = loadWalletMnemonic()
415 const walletMnemonic = loadWalletMnemonicCached()
416416 if (!walletMnemonic) {
417417 return sendError(res, 'Wallet not initialized', 400)
418418 }
……@@ -442,7 +442,7 @@ const routes = {
442442 // GET /api/whoami
443443 'GET /api/whoami': requireAuth(async (req, res) => {
444444 try {
445 const identityMnemonic = loadIdentityMnemonic()
445 const identityMnemonic = loadIdentityMnemonicCached()
446446 if (!identityMnemonic) {
447447 return sendError(res, 'Identity not initialized', 400)
448448 }
……@@ -565,8 +565,8 @@ const routes = {
565565 'POST /api/device/add': requireAuth(async (req, res) => {
566566 try {
567567 const identity = await loadIdentity()
568 const identityMnemonic = loadIdentityMnemonic()
569 const walletMnemonic = loadWalletMnemonic()
568 const identityMnemonic = loadIdentityMnemonicCached()
569 const walletMnemonic = loadWalletMnemonicCached()
570570
571571 if (!walletMnemonic) {
572572 return sendError(res, 'Wallet not initialized', 400)
……@@ -1009,7 +1009,7 @@ const routes = {
10091009 return sendError(res, 'Missing required fields', 400)
10101010 }
10111011
1012 const walletMnemonic = loadWalletMnemonic()
1012 const walletMnemonic = loadWalletMnemonicCached()
10131013 if (!walletMnemonic) {
10141014 return sendError(res, 'Wallet not initialized', 400)
10151015 }
……@@ -1043,7 +1043,7 @@ const routes = {
10431043 return sendError(res, 'Missing required fields', 400)
10441044 }
10451045
1046 const walletMnemonic = loadWalletMnemonic()
1046 const walletMnemonic = loadWalletMnemonicCached()
10471047 if (!walletMnemonic) {
10481048 return sendError(res, 'Wallet not initialized', 400)
10491049 }
……@@ -1070,7 +1070,7 @@ const routes = {
10701070 // POST /api/sync-transactions - Manual transaction sync from blockchain
10711071 'POST /api/sync-transactions': requireAuth(async (req, res) => {
10721072 try {
1073 const walletMnemonic = loadWalletMnemonic()
1073 const walletMnemonic = loadWalletMnemonicCached()
10741074 if (!walletMnemonic) {
10751075 return sendError(res, 'Wallet not initialized', 400)
10761076 }
……@@ -1237,7 +1237,7 @@ const routes = {
12371237 'POST /api/wallet/init': async (req, res) => {
12381238 try {
12391239 // Check if wallet already exists
1240 const existing = loadWalletMnemonic()
1240 const existing = loadWalletMnemonicCached()
12411241 if (existing) {
12421242 return sendError(res, 'Wallet already initialized', 400)
12431243 }
……@@ -1347,7 +1347,7 @@ const routes = {
13471347 }
13481348
13491349 // Check if wallet already exists
1350 const existing = loadWalletMnemonic()
1350 const existing = loadWalletMnemonicCached()
13511351 if (existing) {
13521352 return sendError(res, 'Wallet already initialized. Delete ~/.hyper-wallet/.wallet-mnemonic to restore.', 400)
13531353 }