w3pk 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1887 @@
1
+ # QR Code Backup System
2
+
3
+ > **Secure, Scannable, Offline Wallet Backups**
4
+ >
5
+ > Generate encrypted QR codes for wallet recovery with 30% damage tolerance and military-grade encryption.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Overview](#overview)
12
+ - [Security Architecture](#security-architecture)
13
+ - [Best Practices](#best-practices)
14
+ - [For Developers](#for-developers)
15
+ - [Using with React and Next.js](#using-with-react-and-nextjs)
16
+ - [For End Users](#for-end-users)
17
+ - [Technical Specifications](#technical-specifications)
18
+ - [Troubleshooting](#troubleshooting)
19
+ - [FAQ](#faq)
20
+
21
+ ---
22
+
23
+ ## Overview
24
+
25
+ The w3pk QR Code backup system allows users to backup their wallet as a scannable QR code that can be:
26
+
27
+ - ✅ **Printed and stored offline** (paper, safe deposit box)
28
+ - ✅ **Password-protected** with AES-256-GCM encryption
29
+ - ✅ **Damage-resistant** with 30% error correction
30
+ - ✅ **Cross-platform** compatible (scannable from any device)
31
+ - ✅ **Secure** with address checksum verification
32
+
33
+ ### Key Features
34
+
35
+ | Feature | Details |
36
+ |---------|---------|
37
+ | **Encryption** | AES-256-GCM with PBKDF2-SHA256 (310,000 iterations) |
38
+ | **Error Correction** | Reed-Solomon Level H (30% damage tolerance) |
39
+ | **Size** | 512×512 pixels (optimized for scanning) |
40
+ | **Format** | PNG data URL (embedded base64) |
41
+ | **Verification** | Address checksum prevents corrupted restores |
42
+ | **Fallback** | Canvas-based placeholder if library unavailable |
43
+
44
+ ---
45
+
46
+ ## Security Architecture
47
+
48
+ ### Encryption Flow
49
+
50
+ ```typescript
51
+ ┌─────────────────────────────────────────────────────────┐
52
+ │ QR Code Creation Flow │
53
+ ├─────────────────────────────────────────────────────────┤
54
+ │ │
55
+ │ 1. User Input │
56
+ │ ├─ Mnemonic: "word1 word2 ... word12" │
57
+ │ ├─ Password: "MyS3cur3!Password@2024" │
58
+ │ └─ Options: { errorCorrection: 'H' } │
59
+ │ │
60
+ │ 2. Key Derivation (PBKDF2-SHA256) │
61
+ │ ├─ Salt: Random 32 bytes │
62
+ │ ├─ Iterations: 310,000 (OWASP 2025) │
63
+ │ └─ Output: 256-bit AES key │
64
+ │ │
65
+ │ 3. Encryption (AES-256-GCM) │
66
+ │ ├─ Algorithm: AES-GCM │
67
+ │ ├─ Key Size: 256 bits │
68
+ │ ├─ IV: Random 12 bytes │
69
+ │ ├─ Input: Mnemonic plaintext │
70
+ │ └─ Output: Encrypted ciphertext + auth tag │
71
+ │ │
72
+ │ 4. Checksum Generation │
73
+ │ ├─ Derive Ethereum address from mnemonic │
74
+ │ ├─ Hash: SHA-256(address) │
75
+ │ └─ Store: First 8 bytes as checksum │
76
+ │ │
77
+ │ 5. QR Code Data Structure │
78
+ │ { │
79
+ │ version: 1, │
80
+ │ type: "encrypted", │
81
+ │ data: "<base64_encrypted_mnemonic>", │
82
+ │ salt: "<base64_salt>", │
83
+ │ iv: "<base64_iv>", │
84
+ │ iterations: 310000, │
85
+ │ checksum: "<hex_address_checksum>" │
86
+ │ } │
87
+ │ │
88
+ │ 6. QR Code Generation │
89
+ │ ├─ Library: qrcode (npm) │
90
+ │ ├─ Error Correction: Level H (30%) │
91
+ │ ├─ Size: 512×512 pixels │
92
+ │ ├─ Margin: 2 modules │
93
+ │ └─ Output: PNG data URL │
94
+ │ │
95
+ │ 7. Recovery Instructions │
96
+ │ ├─ Storage recommendations │
97
+ │ ├─ Recovery steps │
98
+ │ ├─ Security warnings │
99
+ │ └─ Verification guidance │
100
+ │ │
101
+ └─────────────────────────────────────────────────────────┘
102
+ ```
103
+
104
+ ### Decryption Flow
105
+
106
+ ```typescript
107
+ ┌─────────────────────────────────────────────────────────┐
108
+ │ QR Code Recovery Flow │
109
+ ├─────────────────────────────────────────────────────────┤
110
+ │ │
111
+ │ 1. Scan QR Code │
112
+ │ └─ Extract JSON data from QR │
113
+ │ │
114
+ │ 2. Parse and Validate │
115
+ │ ├─ Verify version = 1 │
116
+ │ ├─ Verify type = "encrypted" │
117
+ │ └─ Extract: data, salt, iv, iterations, checksum │
118
+ │ │
119
+ │ 3. Key Derivation │
120
+ │ ├─ User enters password │
121
+ │ ├─ PBKDF2-SHA256(password, salt, 310000) │
122
+ │ └─ Derive 256-bit AES key │
123
+ │ │
124
+ │ 4. Decryption │
125
+ │ ├─ AES-256-GCM decrypt │
126
+ │ ├─ Verify authentication tag │
127
+ │ └─ Output: Plaintext mnemonic │
128
+ │ │
129
+ │ 5. Verification │
130
+ │ ├─ Derive Ethereum address from mnemonic │
131
+ │ ├─ Calculate SHA-256(address) │
132
+ │ ├─ Compare with stored checksum │
133
+ │ └─ Match? → Success | Mismatch → Error │
134
+ │ │
135
+ │ 6. Wallet Restoration │
136
+ │ ├─ Display recovered address │
137
+ │ ├─ User verifies address │
138
+ │ └─ Re-register with new passkey │
139
+ │ │
140
+ └─────────────────────────────────────────────────────────┘
141
+ ```
142
+
143
+ ### Error Correction Details
144
+
145
+ QR codes use **Reed-Solomon error correction** to handle damage:
146
+
147
+ | Level | Recovery Capacity | w3pk Usage |
148
+ |-------|------------------|------------|
149
+ | **L** (Low) | 7% data loss | ❌ Not used |
150
+ | **M** (Medium) | 15% data loss | ❌ Not used |
151
+ | **Q** (Quartile) | 25% data loss | ❌ Not used |
152
+ | **H** (High) | **30% data loss** | ✅ **Default** |
153
+
154
+ **Level H means:**
155
+ - Up to 30% of QR code can be damaged/obscured
156
+ - Still fully scannable and recoverable
157
+ - Ideal for physical storage (folding, wear, water damage)
158
+ - Slightly larger QR code size (acceptable tradeoff)
159
+
160
+ ---
161
+
162
+ ## Best Practices
163
+
164
+ ### For QR Code Generation
165
+
166
+ #### ✅ Always Use Encryption
167
+
168
+ ```typescript
169
+ // ✅ RECOMMENDED: Password-protected QR
170
+ const backup = await sdk.createQRBackup('MyS3cur3!Password@2024', {
171
+ errorCorrection: 'H' // 30% damage tolerance
172
+ });
173
+
174
+ // ❌ DANGEROUS: Plain QR code (anyone with QR can steal wallet)
175
+ const backup = await sdk.createQRBackup(undefined, {
176
+ errorCorrection: 'H'
177
+ });
178
+ ```
179
+
180
+ #### ✅ Use Strong Passwords
181
+
182
+ w3pk enforces strong password requirements:
183
+
184
+ - **Minimum 12 characters**
185
+ - At least 1 uppercase letter (A-Z)
186
+ - At least 1 lowercase letter (a-z)
187
+ - At least 1 number (0-9)
188
+ - At least 1 special character (!@#$%^&*)
189
+ - Not in common password dictionary
190
+
191
+ **Good passwords:**
192
+ ```
193
+ ✅ correct horse battery staple (passphrase)
194
+ ✅ MyS3cur3!Backup@December2024 (long with variety)
195
+ ✅ x8$mK9#nP2@vQ5!zR7 (password manager generated)
196
+ ```
197
+
198
+ **Bad passwords:**
199
+ ```
200
+ ❌ password123 (too common)
201
+ ❌ MyPassword (too simple)
202
+ ❌ 12345678 (sequential)
203
+ ❌ qwerty123 (keyboard pattern)
204
+ ```
205
+
206
+ #### ✅ Test Scannability
207
+
208
+ Before printing final copies, test that the QR code is scannable:
209
+
210
+ 1. Display QR on screen
211
+ 2. Scan with phone camera (iOS/Android native camera app)
212
+ 3. Verify data is readable
213
+ 4. Test at different angles and lighting
214
+ 5. Test with slightly damaged/obscured QR (cover 10-20%)
215
+
216
+ #### ✅ Store Securely
217
+
218
+ **Recommended storage locations:**
219
+
220
+ | Location | Security | Accessibility | Cost |
221
+ |----------|----------|---------------|------|
222
+ | **Safe deposit box** | ⭐⭐⭐⭐⭐ | ⭐⭐ | $$ |
223
+ | **Home safe** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | $$$ |
224
+ | **Trusted family member** | ⭐⭐⭐ | ⭐⭐⭐⭐ | Free |
225
+ | **Multiple locations** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | $ |
226
+
227
+ **DO:**
228
+ - ✅ Print on quality paper (acid-free for longevity)
229
+ - ✅ Store in protective sleeve/envelope
230
+ - ✅ Keep multiple copies in different locations
231
+ - ✅ Test QR code readability periodically
232
+ - ✅ Use tamper-evident storage (seal, signature)
233
+
234
+ **DON'T:**
235
+ - ❌ Take screenshots or digital photos
236
+ - ❌ Store in cloud unencrypted
237
+ - ❌ Email or message to anyone
238
+ - ❌ Display publicly or share online
239
+ - ❌ Laminate (can cause scanning glare issues)
240
+
241
+ ### Security Considerations
242
+
243
+ #### 🔒 Password Security
244
+
245
+ Your QR backup is **only as secure as your password**.
246
+
247
+ **Brute-force resistance:**
248
+
249
+ | Password Type | Entropy | Time to Crack (GPU) |
250
+ |--------------|---------|---------------------|
251
+ | `password123` | ~20 bits | **Seconds** ⚠️ |
252
+ | `MyPassword123!` | ~35 bits | **Hours** ⚠️ |
253
+ | `MyS3cur3!Pass@2024` | ~50 bits | **Months** ✅ |
254
+ | `correct horse battery staple` | ~80 bits | **Centuries** ✅ |
255
+ | Random 16 chars | ~100 bits | **Universe lifetime** ✅ |
256
+
257
+ **Recommendation:** Use a password manager to generate and store strong passwords.
258
+
259
+ #### 🔒 Physical Security
260
+
261
+ Even encrypted QR codes need physical security:
262
+
263
+ **Threats:**
264
+ 1. **QR Swapping Attack** - Attacker replaces your QR with theirs
265
+ - **Mitigation:** Include visual identifiers (photo, unique icon, partial address)
266
+
267
+ 2. **Camera Malware** - Compromised scanner steals QR data
268
+ - **Mitigation:** Use trusted devices, scan in private locations
269
+
270
+ 3. **Over-the-shoulder Scanning** - Someone photographs your QR while scanning
271
+ - **Mitigation:** Scan in private, cover surroundings
272
+
273
+ 4. **Interception During Recovery** - Network attacks during restoration
274
+ - **Mitigation:** Use offline recovery tools, air-gapped devices
275
+
276
+ #### 🔒 Storage Best Practices
277
+
278
+ **Multi-location strategy:**
279
+
280
+ ```
281
+ Primary Copy
282
+ ├─ Location: Home safe
283
+ ├─ Format: Printed + sealed envelope
284
+ └─ Access: Immediate
285
+
286
+ Backup Copy 1
287
+ ├─ Location: Bank safe deposit box
288
+ ├─ Format: Printed + instructions
289
+ └─ Access: Within 24 hours
290
+
291
+ Backup Copy 2
292
+ ├─ Location: Trusted family member
293
+ ├─ Format: Printed + sealed tamper-evident envelope
294
+ └─ Access: Within 48 hours (requires contact)
295
+
296
+ Backup Copy 3 (optional)
297
+ ├─ Location: Encrypted cloud storage
298
+ ├─ Format: Encrypted file + password in separate location
299
+ └─ Access: Anywhere with internet
300
+ ```
301
+
302
+ ---
303
+
304
+ ## For Developers
305
+
306
+ ### Installation
307
+
308
+ ```bash
309
+ # Install w3pk
310
+ npm install w3pk ethers
311
+
312
+ # Optional: Install qrcode for QR generation
313
+ npm install qrcode
314
+
315
+ # Optional: Install types for better IDE support
316
+ npm install --save-dev @types/qrcode
317
+ ```
318
+
319
+ ### Basic Usage
320
+
321
+ ```typescript
322
+ import { Web3Passkey } from 'w3pk';
323
+
324
+ const sdk = new Web3Passkey();
325
+
326
+ // 1. Create encrypted QR backup
327
+ const { qrCodeDataURL, instructions, rawData } = await sdk.createQRBackup(
328
+ 'MyS3cur3!Password@2024',
329
+ { errorCorrection: 'H' } // Level H = 30% damage tolerance
330
+ );
331
+
332
+ // 2. Display QR code in UI
333
+ document.getElementById('qr-image').src = qrCodeDataURL;
334
+
335
+ // 3. Show instructions to user
336
+ document.getElementById('instructions').textContent = instructions;
337
+
338
+ // 4. Optional: Download as image
339
+ const link = document.createElement('a');
340
+ link.href = qrCodeDataURL;
341
+ link.download = 'wallet-backup-qr.png';
342
+ link.click();
343
+ ```
344
+
345
+ ### Recovery Implementation
346
+
347
+ ```typescript
348
+ // Recovery page - scan QR and restore
349
+ async function recoverFromQR(scannedData: string, password: string) {
350
+ try {
351
+ // Restore wallet from QR
352
+ const { mnemonic, ethereumAddress } = await sdk.restoreFromQR(
353
+ scannedData,
354
+ password
355
+ );
356
+
357
+ // Show success with address verification
358
+ showSuccess(`
359
+ ✅ Wallet recovered successfully!
360
+
361
+ Address: ${ethereumAddress}
362
+
363
+ ⚠️ Please verify this matches your expected address.
364
+ `);
365
+
366
+ // Re-register with new passkey
367
+ await sdk.register({
368
+ username: 'recovered-wallet',
369
+ mnemonic // Use recovered mnemonic
370
+ });
371
+
372
+ return { success: true, address: ethereumAddress };
373
+
374
+ } catch (error) {
375
+ if (error.message.includes('checksum mismatch')) {
376
+ showError('❌ Incorrect password or corrupted QR code. Please try again.');
377
+ } else if (error.message.includes('Unsupported')) {
378
+ showError('❌ This QR code format is not supported by this version.');
379
+ } else {
380
+ showError(`❌ Recovery failed: ${error.message}`);
381
+ }
382
+
383
+ return { success: false, error: error.message };
384
+ }
385
+ }
386
+ ```
387
+
388
+ ### Advanced: Custom UI
389
+
390
+ ```typescript
391
+ // Create QR backup with custom UI
392
+ async function createQRBackupWithUI(sdk: Web3Passkey) {
393
+ // 1. Prompt for password
394
+ const password = await promptSecurePassword({
395
+ minLength: 12,
396
+ requireUppercase: true,
397
+ requireLowercase: true,
398
+ requireNumbers: true,
399
+ requireSpecialChars: true
400
+ });
401
+
402
+ // 2. Show loading indicator
403
+ showLoading('Generating encrypted QR code...');
404
+
405
+ try {
406
+ // 3. Generate QR backup
407
+ const backup = await sdk.createQRBackup(password, {
408
+ errorCorrection: 'H'
409
+ });
410
+
411
+ hideLoading();
412
+
413
+ // 4. Display QR with visual verification
414
+ displayQRBackup({
415
+ qrCodeDataURL: backup.qrCodeDataURL,
416
+ ethereumAddress: await sdk.getAddress(),
417
+ instructions: backup.instructions,
418
+ createdAt: new Date().toISOString()
419
+ });
420
+
421
+ // 5. Offer print and download options
422
+ showActionButtons([
423
+ { label: 'Print QR Code', action: () => printQR(backup.qrCodeDataURL) },
424
+ { label: 'Download PNG', action: () => downloadQR(backup.qrCodeDataURL) },
425
+ { label: 'Test Scan', action: () => testScanQR() }
426
+ ]);
427
+
428
+ } catch (error) {
429
+ hideLoading();
430
+
431
+ if (error.message.includes('qrcode')) {
432
+ showError(`
433
+ QR code library not installed.
434
+
435
+ To enable QR backups, run:
436
+ npm install qrcode
437
+
438
+ Alternative: Use encrypted ZIP backup instead.
439
+ `);
440
+ } else {
441
+ showError(`Failed to create QR backup: ${error.message}`);
442
+ }
443
+ }
444
+ }
445
+
446
+ // Display QR with verification info
447
+ function displayQRBackup(info: {
448
+ qrCodeDataURL: string;
449
+ ethereumAddress: string;
450
+ instructions: string;
451
+ createdAt: string;
452
+ }) {
453
+ return `
454
+ <div class="qr-backup-container">
455
+ <div class="qr-header">
456
+ <h2>🔐 Wallet Backup QR Code</h2>
457
+ <p class="timestamp">Created: ${new Date(info.createdAt).toLocaleString()}</p>
458
+ </div>
459
+
460
+ <div class="qr-display">
461
+ <img
462
+ src="${info.qrCodeDataURL}"
463
+ alt="Wallet Backup QR Code"
464
+ class="qr-image"
465
+ />
466
+ </div>
467
+
468
+ <div class="verification">
469
+ <h3>Verification</h3>
470
+ <p class="address">
471
+ <strong>Wallet Address:</strong><br>
472
+ <code>${info.ethereumAddress}</code>
473
+ </p>
474
+ <p class="verify-note">
475
+ ✓ After recovery, verify this address matches
476
+ </p>
477
+ </div>
478
+
479
+ <div class="security-warning">
480
+ <h3>⚠️ Security Reminder</h3>
481
+ <ul>
482
+ <li>This QR code is encrypted with your password</li>
483
+ <li>Store in a secure physical location</li>
484
+ <li>Never share or photograph</li>
485
+ <li>Test scannability before final storage</li>
486
+ </ul>
487
+ </div>
488
+
489
+ <details class="instructions">
490
+ <summary>📋 Full Instructions</summary>
491
+ <pre>${info.instructions}</pre>
492
+ </details>
493
+ </div>
494
+ `;
495
+ }
496
+ ```
497
+
498
+ ### Print Styling
499
+
500
+ Add CSS for optimal printing:
501
+
502
+ ```css
503
+ /* Print-optimized QR code display */
504
+ @media print {
505
+ .qr-backup-container {
506
+ page-break-inside: avoid;
507
+ page-break-after: always;
508
+ }
509
+
510
+ .qr-image {
511
+ width: 4in; /* Physical size for easy scanning */
512
+ height: 4in;
513
+ display: block;
514
+ margin: 0.5in auto;
515
+ }
516
+
517
+ .address {
518
+ font-size: 10pt;
519
+ word-break: break-all;
520
+ }
521
+
522
+ .instructions {
523
+ font-size: 9pt;
524
+ line-height: 1.4;
525
+ margin-top: 0.25in;
526
+ }
527
+
528
+ /* Hide non-essential elements when printing */
529
+ .action-buttons,
530
+ .qr-header .timestamp {
531
+ display: none;
532
+ }
533
+ }
534
+
535
+ /* Screen display */
536
+ @media screen {
537
+ .qr-backup-container {
538
+ max-width: 600px;
539
+ margin: 2rem auto;
540
+ padding: 2rem;
541
+ border: 1px solid #ddd;
542
+ border-radius: 8px;
543
+ }
544
+
545
+ .qr-image {
546
+ width: 100%;
547
+ max-width: 512px;
548
+ height: auto;
549
+ border: 2px solid #333;
550
+ padding: 1rem;
551
+ background: white;
552
+ }
553
+
554
+ .address code {
555
+ display: block;
556
+ padding: 0.5rem;
557
+ background: #f5f5f5;
558
+ border-radius: 4px;
559
+ font-family: 'Monaco', 'Courier New', monospace;
560
+ font-size: 12px;
561
+ word-break: break-all;
562
+ }
563
+
564
+ .security-warning {
565
+ background: #fff3cd;
566
+ border-left: 4px solid #ffc107;
567
+ padding: 1rem;
568
+ margin: 1rem 0;
569
+ }
570
+ }
571
+ ```
572
+
573
+ ### Error Handling
574
+
575
+ ```typescript
576
+ // Comprehensive error handling for QR operations
577
+ enum QRErrorType {
578
+ LIBRARY_MISSING = 'QR_LIBRARY_MISSING',
579
+ INVALID_PASSWORD = 'INVALID_PASSWORD',
580
+ CHECKSUM_MISMATCH = 'CHECKSUM_MISMATCH',
581
+ CORRUPTED_QR = 'CORRUPTED_QR',
582
+ UNSUPPORTED_VERSION = 'UNSUPPORTED_VERSION',
583
+ GENERATION_FAILED = 'GENERATION_FAILED'
584
+ }
585
+
586
+ function handleQRError(error: Error): { type: QRErrorType; message: string; solution: string } {
587
+ if (error.message.includes('qrcode')) {
588
+ return {
589
+ type: QRErrorType.LIBRARY_MISSING,
590
+ message: 'QR code library not installed',
591
+ solution: 'Run: npm install qrcode'
592
+ };
593
+ }
594
+
595
+ if (error.message.includes('checksum mismatch')) {
596
+ return {
597
+ type: QRErrorType.CHECKSUM_MISMATCH,
598
+ message: 'Wrong password or corrupted QR code',
599
+ solution: 'Double-check your password or re-scan the QR code'
600
+ };
601
+ }
602
+
603
+ if (error.message.includes('Unsupported')) {
604
+ return {
605
+ type: QRErrorType.UNSUPPORTED_VERSION,
606
+ message: 'QR code version not supported',
607
+ solution: 'Update w3pk to the latest version'
608
+ };
609
+ }
610
+
611
+ if (error.message.includes('Password required')) {
612
+ return {
613
+ type: QRErrorType.INVALID_PASSWORD,
614
+ message: 'Password required for encrypted QR',
615
+ solution: 'Enter the password used when creating the backup'
616
+ };
617
+ }
618
+
619
+ return {
620
+ type: QRErrorType.GENERATION_FAILED,
621
+ message: error.message,
622
+ solution: 'Check console for details or contact support'
623
+ };
624
+ }
625
+ ```
626
+
627
+ ### Testing Checklist
628
+
629
+ ```typescript
630
+ // Comprehensive test suite for QR functionality
631
+ describe('QR Code Backup System', () => {
632
+
633
+ test('1. QR generation with encryption', async () => {
634
+ const backup = await sdk.createQRBackup('test-password', {
635
+ errorCorrection: 'H'
636
+ });
637
+
638
+ expect(backup.qrCodeDataURL).toMatch(/^data:image\/png;base64,/);
639
+ expect(backup.rawData).toBeDefined();
640
+ expect(backup.instructions).toContain('RECOVERY STEPS');
641
+ });
642
+
643
+ test('2. Round-trip: create → restore → verify', async () => {
644
+ const originalMnemonic = 'test test test test test test test test test test test junk';
645
+
646
+ // Create QR
647
+ const { rawData } = await sdk.createQRBackup('password123');
648
+
649
+ // Restore QR
650
+ const { mnemonic, ethereumAddress } = await sdk.restoreFromQR(
651
+ rawData,
652
+ 'password123'
653
+ );
654
+
655
+ expect(mnemonic).toBe(originalMnemonic);
656
+ expect(ethereumAddress).toMatch(/^0x[a-fA-F0-9]{40}$/);
657
+ });
658
+
659
+ test('3. Wrong password fails gracefully', async () => {
660
+ const { rawData } = await sdk.createQRBackup('correct-password');
661
+
662
+ await expect(
663
+ sdk.restoreFromQR(rawData, 'wrong-password')
664
+ ).rejects.toThrow('checksum mismatch');
665
+ });
666
+
667
+ test('4. Corrupted QR data fails verification', async () => {
668
+ const { rawData } = await sdk.createQRBackup('password');
669
+ const corrupted = rawData.slice(0, -20) + 'CORRUPTED_DATA';
670
+
671
+ await expect(
672
+ sdk.restoreFromQR(corrupted, 'password')
673
+ ).rejects.toThrow();
674
+ });
675
+
676
+ test('5. Error correction level H is used', async () => {
677
+ const { rawData } = await sdk.createQRBackup('password', {
678
+ errorCorrection: 'H'
679
+ });
680
+
681
+ // Parse QR data
682
+ const data = JSON.parse(rawData);
683
+
684
+ // Verify high error correction (30% damage tolerance)
685
+ expect(data.version).toBe(1);
686
+ expect(data.type).toBe('encrypted');
687
+ });
688
+
689
+ test('6. Checksum verification works', async () => {
690
+ const { rawData } = await sdk.createQRBackup('password');
691
+ const data = JSON.parse(rawData);
692
+
693
+ // Checksum should be present
694
+ expect(data.checksum).toBeDefined();
695
+ expect(data.checksum).toHaveLength(16); // 8 bytes hex
696
+ });
697
+
698
+ test('7. Fallback works when qrcode not installed', async () => {
699
+ // Mock qrcode import failure
700
+ jest.mock('qrcode', () => {
701
+ throw new Error('Cannot find module qrcode');
702
+ });
703
+
704
+ const backup = await sdk.createQRBackup('password');
705
+
706
+ // Should still return a data URL (canvas fallback)
707
+ expect(backup.qrCodeDataURL).toMatch(/^data:/);
708
+ });
709
+
710
+ test('8. QR code is scannable at different sizes', async () => {
711
+ const { qrCodeDataURL } = await sdk.createQRBackup('password');
712
+
713
+ // Test at multiple resolutions
714
+ const sizes = [256, 512, 1024];
715
+
716
+ for (const size of sizes) {
717
+ const resized = await resizeDataURL(qrCodeDataURL, size);
718
+ expect(resized).toBeDefined();
719
+ // Manual verification: scan with phone
720
+ }
721
+ });
722
+
723
+ test('9. Instructions are comprehensive', async () => {
724
+ const { instructions } = await sdk.createQRBackup('password');
725
+
726
+ // Verify instructions contain key sections
727
+ expect(instructions).toContain('STORAGE INSTRUCTIONS');
728
+ expect(instructions).toContain('RECOVERY STEPS');
729
+ expect(instructions).toContain('SECURITY NOTES');
730
+ expect(instructions).toContain('ERROR CORRECTION');
731
+ expect(instructions).toContain('VERIFICATION');
732
+ });
733
+
734
+ test('10. Plain QR (unencrypted) works but warns', async () => {
735
+ const { rawData } = await sdk.createQRBackup(undefined);
736
+ const data = JSON.parse(rawData);
737
+
738
+ expect(data.type).toBe('plain');
739
+ expect(data.data).toBeDefined(); // Mnemonic in plain text
740
+
741
+ // Should have warning in instructions
742
+ const { instructions } = await sdk.createQRBackup(undefined);
743
+ expect(instructions).toContain('NOT ENCRYPTED');
744
+ });
745
+ });
746
+ ```
747
+
748
+ ---
749
+
750
+ ## Using with React and Next.js
751
+
752
+ ### Option 1: Use w3pk's Built-in QR Generation
753
+
754
+ If you're using w3pk in a React/Next.js app, the simplest approach is to use w3pk's built-in QR generation and display the data URL directly:
755
+
756
+ ```tsx
757
+ 'use client'; // Next.js 13+ App Router
758
+
759
+ import { Web3Passkey } from 'w3pk';
760
+ import { useState } from 'react';
761
+ import Image from 'next/image';
762
+
763
+ export default function WalletBackup() {
764
+ const [sdk] = useState(() => new Web3Passkey());
765
+ const [qrData, setQrData] = useState<string | null>(null);
766
+ const [loading, setLoading] = useState(false);
767
+
768
+ const handleCreateBackup = async (password: string) => {
769
+ setLoading(true);
770
+ try {
771
+ const backup = await sdk.createQRBackup(password, {
772
+ errorCorrection: 'H'
773
+ });
774
+
775
+ setQrData(backup.qrCodeDataURL);
776
+ } catch (error) {
777
+ console.error('Backup failed:', error);
778
+ } finally {
779
+ setLoading(false);
780
+ }
781
+ };
782
+
783
+ return (
784
+ <div className="wallet-backup">
785
+ {!qrData ? (
786
+ <button
787
+ onClick={() => handleCreateBackup('user-password')}
788
+ disabled={loading}
789
+ >
790
+ {loading ? 'Generating...' : 'Create QR Backup'}
791
+ </button>
792
+ ) : (
793
+ <>
794
+ {/* Next.js Image component */}
795
+ <Image
796
+ src={qrData}
797
+ alt="Wallet Backup QR Code"
798
+ width={512}
799
+ height={512}
800
+ priority
801
+ />
802
+
803
+ {/* Or standard img tag */}
804
+ <img src={qrData} alt="Wallet Backup" />
805
+
806
+ <button onClick={() => window.print()}>
807
+ Print QR Code
808
+ </button>
809
+ </>
810
+ )}
811
+ </div>
812
+ );
813
+ }
814
+ ```
815
+
816
+ **Pros:**
817
+ - ✅ No additional dependencies
818
+ - ✅ Works with w3pk's encryption out of the box
819
+ - ✅ Consistent with w3pk's error correction settings
820
+ - ✅ Works in both SSR and client-side rendering
821
+
822
+ **Cons:**
823
+ - ⚠️ Requires optional `qrcode` package installed
824
+ - ⚠️ Less customization of QR appearance
825
+
826
+ ---
827
+
828
+ ### Option 2: Use `qrcode.react` with w3pk's Encrypted Data
829
+
830
+ If you're already using `qrcode.react` in your Next.js app, you can use it to render w3pk's encrypted QR data:
831
+
832
+ ```bash
833
+ npm install qrcode.react
834
+ npm install @types/qrcode.react --save-dev
835
+ ```
836
+
837
+ ```tsx
838
+ 'use client';
839
+
840
+ import { Web3Passkey } from 'w3pk';
841
+ import { QRCodeSVG, QRCodeCanvas } from 'qrcode.react';
842
+ import { useState, useEffect } from 'react';
843
+
844
+ export default function WalletBackupWithReact() {
845
+ const [sdk] = useState(() => new Web3Passkey());
846
+ const [qrData, setQrData] = useState<string>('');
847
+ const [loading, setLoading] = useState(false);
848
+
849
+ const handleCreateBackup = async (password: string) => {
850
+ setLoading(true);
851
+ try {
852
+ // Get encrypted QR data from w3pk (not the image, just the data)
853
+ const backup = await sdk.createQRBackup(password, {
854
+ errorCorrection: 'H'
855
+ });
856
+
857
+ // w3pk returns { qrCodeDataURL, rawData, instructions }
858
+ // Use rawData for qrcode.react
859
+ setQrData(backup.rawData);
860
+
861
+ } catch (error) {
862
+ console.error('Backup failed:', error);
863
+ } finally {
864
+ setLoading(false);
865
+ }
866
+ };
867
+
868
+ return (
869
+ <div className="wallet-backup">
870
+ {!qrData ? (
871
+ <button
872
+ onClick={() => handleCreateBackup('user-password')}
873
+ disabled={loading}
874
+ >
875
+ {loading ? 'Generating...' : 'Create QR Backup'}
876
+ </button>
877
+ ) : (
878
+ <div className="qr-display">
879
+ {/* SVG QR Code (recommended for web) */}
880
+ <QRCodeSVG
881
+ value={qrData}
882
+ size={512}
883
+ level="H" // 30% error correction (matches w3pk)
884
+ includeMargin={true}
885
+ marginSize={2}
886
+ />
887
+
888
+ {/* Or Canvas QR Code (for downloading) */}
889
+ <QRCodeCanvas
890
+ value={qrData}
891
+ size={512}
892
+ level="H"
893
+ includeMargin={true}
894
+ marginSize={2}
895
+ />
896
+ </div>
897
+ )}
898
+ </div>
899
+ );
900
+ }
901
+ ```
902
+
903
+ **Pros:**
904
+ - ✅ Full React component
905
+ - ✅ SVG format (scalable, smaller file size)
906
+ - ✅ More customization options
907
+ - ✅ Better for responsive designs
908
+
909
+ **Cons:**
910
+ - ⚠️ Additional dependency (`qrcode.react`)
911
+ - ⚠️ Need to ensure error correction level matches
912
+
913
+ ---
914
+
915
+ ### Option 3: Hybrid Approach (Recommended for Next.js)
916
+
917
+ Use w3pk for encryption and `qrcode.react` for rendering:
918
+
919
+ ```tsx
920
+ 'use client';
921
+
922
+ import { Web3Passkey } from 'w3pk';
923
+ import { QRCodeSVG } from 'qrcode.react';
924
+ import { useState } from 'react';
925
+
926
+ interface QRBackupProps {
927
+ onBackupCreated?: (data: string) => void;
928
+ }
929
+
930
+ export default function QRBackupComponent({ onBackupCreated }: QRBackupProps) {
931
+ const [sdk] = useState(() => new Web3Passkey());
932
+ const [backupData, setBackupData] = useState<{
933
+ rawData: string;
934
+ address: string;
935
+ instructions: string;
936
+ } | null>(null);
937
+ const [password, setPassword] = useState('');
938
+ const [error, setError] = useState<string | null>(null);
939
+
940
+ const validatePassword = (pwd: string): boolean => {
941
+ // w3pk's password requirements
942
+ if (pwd.length < 12) return false;
943
+ if (!/[A-Z]/.test(pwd)) return false;
944
+ if (!/[a-z]/.test(pwd)) return false;
945
+ if (!/[0-9]/.test(pwd)) return false;
946
+ if (!/[!@#$%^&*]/.test(pwd)) return false;
947
+ return true;
948
+ };
949
+
950
+ const handleCreateBackup = async () => {
951
+ if (!validatePassword(password)) {
952
+ setError('Password must be 12+ chars with uppercase, lowercase, numbers, and symbols');
953
+ return;
954
+ }
955
+
956
+ try {
957
+ const backup = await sdk.createQRBackup(password, {
958
+ errorCorrection: 'H'
959
+ });
960
+
961
+ const address = await sdk.getAddress();
962
+
963
+ setBackupData({
964
+ rawData: backup.rawData,
965
+ address,
966
+ instructions: backup.instructions
967
+ });
968
+
969
+ onBackupCreated?.(backup.rawData);
970
+
971
+ } catch (err: any) {
972
+ if (err.message.includes('qrcode')) {
973
+ setError('Install qrcode package: npm install qrcode');
974
+ } else {
975
+ setError(`Backup failed: ${err.message}`);
976
+ }
977
+ }
978
+ };
979
+
980
+ const handleDownloadQR = () => {
981
+ if (!backupData) return;
982
+
983
+ // Get canvas from QRCodeCanvas component
984
+ const canvas = document.querySelector('canvas');
985
+ if (canvas) {
986
+ const url = canvas.toDataURL('image/png');
987
+ const a = document.createElement('a');
988
+ a.href = url;
989
+ a.download = `wallet-backup-${Date.now()}.png`;
990
+ a.click();
991
+ }
992
+ };
993
+
994
+ return (
995
+ <div className="max-w-2xl mx-auto p-6">
996
+ {!backupData ? (
997
+ <div className="space-y-4">
998
+ <h2 className="text-2xl font-bold">Create QR Backup</h2>
999
+
1000
+ <div>
1001
+ <label className="block text-sm font-medium mb-2">
1002
+ Password (12+ characters)
1003
+ </label>
1004
+ <input
1005
+ type="password"
1006
+ value={password}
1007
+ onChange={(e) => setPassword(e.target.value)}
1008
+ className="w-full px-3 py-2 border rounded-lg"
1009
+ placeholder="Enter strong password"
1010
+ />
1011
+ </div>
1012
+
1013
+ {error && (
1014
+ <div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700">
1015
+ {error}
1016
+ </div>
1017
+ )}
1018
+
1019
+ <button
1020
+ onClick={handleCreateBackup}
1021
+ className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
1022
+ >
1023
+ Generate Encrypted QR Code
1024
+ </button>
1025
+ </div>
1026
+ ) : (
1027
+ <div className="space-y-6">
1028
+ <h2 className="text-2xl font-bold">Your Wallet Backup</h2>
1029
+
1030
+ {/* QR Code Display */}
1031
+ <div className="flex flex-col items-center space-y-4">
1032
+ <div className="p-4 bg-white border-4 border-gray-800 rounded-lg">
1033
+ <QRCodeSVG
1034
+ value={backupData.rawData}
1035
+ size={512}
1036
+ level="H"
1037
+ includeMargin={true}
1038
+ marginSize={2}
1039
+ />
1040
+ </div>
1041
+
1042
+ {/* Address Verification */}
1043
+ <div className="w-full p-4 bg-gray-50 rounded-lg">
1044
+ <p className="text-sm font-medium text-gray-700">Wallet Address:</p>
1045
+ <p className="font-mono text-xs break-all mt-1">
1046
+ {backupData.address}
1047
+ </p>
1048
+ <p className="text-xs text-gray-500 mt-2">
1049
+ ✓ Verify this address matches after recovery
1050
+ </p>
1051
+ </div>
1052
+ </div>
1053
+
1054
+ {/* Action Buttons */}
1055
+ <div className="flex gap-3">
1056
+ <button
1057
+ onClick={() => window.print()}
1058
+ className="flex-1 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
1059
+ >
1060
+ 🖨️ Print QR Code
1061
+ </button>
1062
+ <button
1063
+ onClick={handleDownloadQR}
1064
+ className="flex-1 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
1065
+ >
1066
+ 📥 Download PNG
1067
+ </button>
1068
+ </div>
1069
+
1070
+ {/* Security Warning */}
1071
+ <div className="p-4 bg-yellow-50 border-l-4 border-yellow-400">
1072
+ <h3 className="font-bold text-yellow-800 mb-2">⚠️ Security Reminder</h3>
1073
+ <ul className="text-sm text-yellow-700 space-y-1">
1074
+ <li>• This QR code is encrypted with your password</li>
1075
+ <li>• Store in a secure physical location</li>
1076
+ <li>• Never share or photograph</li>
1077
+ <li>• Keep password separate from QR code</li>
1078
+ </ul>
1079
+ </div>
1080
+
1081
+ {/* Instructions (collapsible) */}
1082
+ <details className="border rounded-lg p-4">
1083
+ <summary className="cursor-pointer font-medium">
1084
+ 📋 Full Recovery Instructions
1085
+ </summary>
1086
+ <pre className="mt-3 text-xs whitespace-pre-wrap overflow-auto">
1087
+ {backupData.instructions}
1088
+ </pre>
1089
+ </details>
1090
+ </div>
1091
+ )}
1092
+ </div>
1093
+ );
1094
+ }
1095
+ ```
1096
+
1097
+ ---
1098
+
1099
+ ### Next.js 13+ App Router Considerations
1100
+
1101
+ ```tsx
1102
+ // app/wallet/backup/page.tsx
1103
+ 'use client';
1104
+
1105
+ import dynamic from 'next/dynamic';
1106
+
1107
+ // Dynamic import to avoid SSR issues with Web3Passkey
1108
+ const QRBackupComponent = dynamic(
1109
+ () => import('@/components/QRBackupComponent'),
1110
+ { ssr: false }
1111
+ );
1112
+
1113
+ export default function BackupPage() {
1114
+ return (
1115
+ <main className="container mx-auto py-8">
1116
+ <QRBackupComponent />
1117
+ </main>
1118
+ );
1119
+ }
1120
+ ```
1121
+
1122
+ **Why use dynamic import?**
1123
+ - Web3Passkey uses browser APIs (WebAuthn, Crypto)
1124
+ - These aren't available during SSR
1125
+ - `ssr: false` ensures component only renders on client
1126
+
1127
+ ---
1128
+
1129
+ ### Print Styling for Next.js
1130
+
1131
+ ```tsx
1132
+ // app/globals.css or component-specific CSS module
1133
+
1134
+ @media print {
1135
+ /* Hide everything except QR code when printing */
1136
+ body * {
1137
+ visibility: hidden;
1138
+ }
1139
+
1140
+ .qr-display,
1141
+ .qr-display * {
1142
+ visibility: visible;
1143
+ }
1144
+
1145
+ .qr-display {
1146
+ position: absolute;
1147
+ left: 0;
1148
+ top: 0;
1149
+ width: 100%;
1150
+ }
1151
+
1152
+ /* Optimal QR code size for printing */
1153
+ .qr-display svg,
1154
+ .qr-display canvas {
1155
+ width: 4in !important;
1156
+ height: 4in !important;
1157
+ display: block;
1158
+ margin: 0.5in auto;
1159
+ }
1160
+
1161
+ /* Include address for verification */
1162
+ .address-verification {
1163
+ display: block !important;
1164
+ page-break-inside: avoid;
1165
+ margin-top: 0.5in;
1166
+ font-size: 10pt;
1167
+ }
1168
+
1169
+ /* Hide buttons when printing */
1170
+ button,
1171
+ .no-print {
1172
+ display: none !important;
1173
+ }
1174
+ }
1175
+ ```
1176
+
1177
+ ---
1178
+
1179
+ ### Recovery Component (Scanning QR)
1180
+
1181
+ ```tsx
1182
+ 'use client';
1183
+
1184
+ import { Web3Passkey } from 'w3pk';
1185
+ import { useState } from 'react';
1186
+ import { QrReader } from 'react-qr-reader'; // Optional: for camera scanning
1187
+
1188
+ export default function QRRecoveryComponent() {
1189
+ const [sdk] = useState(() => new Web3Passkey());
1190
+ const [scannedData, setScannedData] = useState('');
1191
+ const [password, setPassword] = useState('');
1192
+ const [recovered, setRecovered] = useState<{
1193
+ address: string;
1194
+ success: boolean;
1195
+ } | null>(null);
1196
+ const [error, setError] = useState<string | null>(null);
1197
+
1198
+ const handleRecover = async () => {
1199
+ try {
1200
+ setError(null);
1201
+
1202
+ const { mnemonic, ethereumAddress } = await sdk.restoreFromQR(
1203
+ scannedData,
1204
+ password
1205
+ );
1206
+
1207
+ // Re-register with recovered mnemonic
1208
+ await sdk.register({
1209
+ username: 'recovered-wallet',
1210
+ mnemonic
1211
+ });
1212
+
1213
+ setRecovered({
1214
+ address: ethereumAddress,
1215
+ success: true
1216
+ });
1217
+
1218
+ } catch (err: any) {
1219
+ if (err.message.includes('checksum mismatch')) {
1220
+ setError('❌ Incorrect password or corrupted QR code');
1221
+ } else if (err.message.includes('Unsupported')) {
1222
+ setError('❌ QR code version not supported. Update w3pk.');
1223
+ } else {
1224
+ setError(`❌ Recovery failed: ${err.message}`);
1225
+ }
1226
+ }
1227
+ };
1228
+
1229
+ return (
1230
+ <div className="max-w-2xl mx-auto p-6 space-y-6">
1231
+ <h2 className="text-2xl font-bold">Recover Wallet from QR</h2>
1232
+
1233
+ {!recovered ? (
1234
+ <>
1235
+ {/* Option 1: Paste QR data */}
1236
+ <div>
1237
+ <label className="block text-sm font-medium mb-2">
1238
+ QR Code Data (JSON)
1239
+ </label>
1240
+ <textarea
1241
+ value={scannedData}
1242
+ onChange={(e) => setScannedData(e.target.value)}
1243
+ className="w-full px-3 py-2 border rounded-lg font-mono text-xs"
1244
+ rows={6}
1245
+ placeholder='{"version":1,"type":"encrypted",...}'
1246
+ />
1247
+ </div>
1248
+
1249
+ {/* Option 2: Upload QR image */}
1250
+ <div>
1251
+ <label className="block text-sm font-medium mb-2">
1252
+ Or Upload QR Image
1253
+ </label>
1254
+ <input
1255
+ type="file"
1256
+ accept="image/*"
1257
+ onChange={async (e) => {
1258
+ const file = e.target.files?.[0];
1259
+ if (file) {
1260
+ // Use jsQR or similar library to decode image
1261
+ // const data = await decodeQRFromImage(file);
1262
+ // setScannedData(data);
1263
+ }
1264
+ }}
1265
+ className="w-full"
1266
+ />
1267
+ </div>
1268
+
1269
+ {/* Password */}
1270
+ <div>
1271
+ <label className="block text-sm font-medium mb-2">
1272
+ Password
1273
+ </label>
1274
+ <input
1275
+ type="password"
1276
+ value={password}
1277
+ onChange={(e) => setPassword(e.target.value)}
1278
+ className="w-full px-3 py-2 border rounded-lg"
1279
+ placeholder="Enter backup password"
1280
+ />
1281
+ </div>
1282
+
1283
+ {error && (
1284
+ <div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700">
1285
+ {error}
1286
+ </div>
1287
+ )}
1288
+
1289
+ <button
1290
+ onClick={handleRecover}
1291
+ disabled={!scannedData || !password}
1292
+ className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
1293
+ >
1294
+ Recover Wallet
1295
+ </button>
1296
+ </>
1297
+ ) : (
1298
+ <div className="p-6 bg-green-50 border border-green-200 rounded-lg">
1299
+ <h3 className="text-xl font-bold text-green-800 mb-4">
1300
+ ✅ Wallet Recovered Successfully!
1301
+ </h3>
1302
+
1303
+ <div className="space-y-2">
1304
+ <p className="text-sm font-medium text-gray-700">
1305
+ Recovered Address:
1306
+ </p>
1307
+ <p className="font-mono text-sm break-all p-3 bg-white rounded border">
1308
+ {recovered.address}
1309
+ </p>
1310
+ </div>
1311
+
1312
+ <div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded">
1313
+ <p className="text-sm text-yellow-800">
1314
+ ⚠️ Please verify this address matches your expected wallet address.
1315
+ </p>
1316
+ </div>
1317
+ </div>
1318
+ )}
1319
+ </div>
1320
+ );
1321
+ }
1322
+ ```
1323
+
1324
+ ---
1325
+
1326
+ ### Comparison: w3pk vs qrcode.react
1327
+
1328
+ | Feature | w3pk built-in | qrcode.react | Hybrid |
1329
+ |---------|--------------|--------------|--------|
1330
+ | **Setup** | Simple | Need extra package | Medium |
1331
+ | **Encryption** | ✅ Built-in | ❌ Manual | ✅ Built-in |
1332
+ | **Format** | PNG data URL | SVG/Canvas | Your choice |
1333
+ | **Customization** | Limited | Full | Full |
1334
+ | **Error Correction** | Always H | Must specify | Must match |
1335
+ | **SSR Compatible** | ⚠️ Client only | ⚠️ Client only | ⚠️ Client only |
1336
+ | **File Size** | Larger (base64) | Smaller (SVG) | Smaller |
1337
+ | **React Integration** | Medium | Native | Native |
1338
+
1339
+ ---
1340
+
1341
+ ### Recommendation for React/Next.js Developers
1342
+
1343
+ **If you already have `qrcode.react` installed:** Use **Option 3 (Hybrid)**
1344
+ - Use w3pk for encryption and data generation
1345
+ - Use `qrcode.react` for rendering the QR code
1346
+ - Best of both worlds
1347
+
1348
+ **If you're starting fresh:** Use **Option 1 (w3pk built-in)**
1349
+ - Fewer dependencies
1350
+ - Simpler integration
1351
+ - Consistent with w3pk's design
1352
+
1353
+ **Code example:**
1354
+ ```tsx
1355
+ // ✅ Hybrid approach (recommended if using qrcode.react)
1356
+ const backup = await sdk.createQRBackup(password);
1357
+
1358
+ <QRCodeSVG
1359
+ value={backup.rawData} // Use rawData, not qrCodeDataURL
1360
+ size={512}
1361
+ level="H" // Must match w3pk's error correction
1362
+ />
1363
+
1364
+ // ✅ Built-in approach (simplest)
1365
+ const backup = await sdk.createQRBackup(password);
1366
+
1367
+ <img src={backup.qrCodeDataURL} alt="Backup QR" />
1368
+ ```
1369
+
1370
+ ---
1371
+
1372
+ ## For End Users
1373
+
1374
+ ### Creating a QR Backup
1375
+
1376
+ **Step-by-step guide:**
1377
+
1378
+ 1. **Open Backup Settings**
1379
+ - Navigate to wallet settings
1380
+ - Click "Backup Wallet"
1381
+ - Select "QR Code Backup"
1382
+
1383
+ 2. **Choose Encryption**
1384
+ - **Recommended:** Enable password protection
1385
+ - Enter a strong password (12+ characters)
1386
+ - Confirm password
1387
+ - Write down password separately
1388
+
1389
+ 3. **Generate QR Code**
1390
+ - System generates encrypted QR code
1391
+ - QR code appears on screen
1392
+ - Instructions are displayed
1393
+
1394
+ 4. **Test Scannability**
1395
+ - Use your phone camera to scan QR code
1396
+ - Verify it can be read
1397
+ - Don't enter password yet (just testing)
1398
+
1399
+ 5. **Print or Save**
1400
+ - **Option A:** Print on quality paper
1401
+ - **Option B:** Download as PNG image
1402
+ - Create multiple copies
1403
+
1404
+ 6. **Store Securely**
1405
+ - Place in safe, safety deposit box, or with trusted person
1406
+ - Store password separately (not with QR code!)
1407
+ - Keep multiple copies in different locations
1408
+
1409
+ ### Recovering from QR Backup
1410
+
1411
+ **Step-by-step recovery:**
1412
+
1413
+ 1. **Locate Your QR Backup**
1414
+ - Retrieve printed QR code or image file
1415
+ - Ensure QR code is clear and undamaged
1416
+
1417
+ 2. **Scan QR Code**
1418
+ - Open w3pk recovery page
1419
+ - Click "Restore from QR"
1420
+ - Use phone/computer camera to scan
1421
+ - Or upload image file
1422
+
1423
+ 3. **Enter Password**
1424
+ - Enter the password used when creating backup
1425
+ - Click "Decrypt and Restore"
1426
+
1427
+ 4. **Verify Address**
1428
+ - System displays recovered wallet address
1429
+ - **Important:** Verify this matches your expected address
1430
+ - If incorrect, try different password
1431
+
1432
+ 5. **Complete Recovery**
1433
+ - System creates new passkey for this device
1434
+ - Authenticate with biometric/PIN
1435
+ - Wallet is now accessible
1436
+
1437
+ 6. **Test Recovered Wallet**
1438
+ - Check balance
1439
+ - Verify transaction history
1440
+ - Test signing a transaction (small amount first)
1441
+
1442
+ ### Storage Recommendations
1443
+
1444
+ **Best storage options ranked:**
1445
+
1446
+ 1. **🏆 Bank Safe Deposit Box** (Most secure)
1447
+ - Physical security
1448
+ - Fire/flood protection
1449
+ - Access during bank hours
1450
+ - Small annual fee
1451
+
1452
+ 2. **🏠 Home Safe** (Convenient)
1453
+ - Immediate access
1454
+ - Fire-resistant safe recommended
1455
+ - Keep at home but secured
1456
+ - Moderate cost
1457
+
1458
+ 3. **👨‍👩‍👧‍👦 Trusted Family Member** (Redundancy)
1459
+ - Give sealed envelope to family
1460
+ - Geographic distribution
1461
+ - Verbal instructions
1462
+ - Free
1463
+
1464
+ 4. **💼 Multiple Locations** (Maximum protection)
1465
+ - Combine 2-3 methods above
1466
+ - Different geographic areas
1467
+ - Survives regional disasters
1468
+ - Recommended for high-value wallets
1469
+
1470
+ **Storage checklist:**
1471
+
1472
+ - [ ] QR code printed on quality paper
1473
+ - [ ] Placed in protective envelope/sleeve
1474
+ - [ ] Password written down separately
1475
+ - [ ] Multiple copies created (3+)
1476
+ - [ ] Stored in 2+ different locations
1477
+ - [ ] Family members informed (optional)
1478
+ - [ ] Periodic verification (yearly)
1479
+
1480
+ ---
1481
+
1482
+ ## Technical Specifications
1483
+
1484
+ ### QR Code Format
1485
+
1486
+ ```typescript
1487
+ // Version 1 (current)
1488
+ interface QRBackupData {
1489
+ version: 1; // Format version
1490
+ type: 'encrypted' | 'plain'; // Encryption status
1491
+ data: string; // Encrypted mnemonic (base64) or plain text
1492
+ salt?: string; // PBKDF2 salt (base64, if encrypted)
1493
+ iv?: string; // AES-GCM IV (base64, if encrypted)
1494
+ iterations?: number; // PBKDF2 iterations (if encrypted)
1495
+ checksum: string; // Address checksum (hex)
1496
+ }
1497
+ ```
1498
+
1499
+ ### Encryption Specifications
1500
+
1501
+ | Parameter | Value | Standard |
1502
+ |-----------|-------|----------|
1503
+ | **Key Derivation** | PBKDF2-SHA256 | OWASP 2025 |
1504
+ | **Iterations** | 310,000 | OWASP 2025 minimum |
1505
+ | **Salt** | 32 bytes random | NIST SP 800-132 |
1506
+ | **Encryption** | AES-256-GCM | FIPS 197 |
1507
+ | **Key Size** | 256 bits | NIST recommended |
1508
+ | **IV** | 12 bytes random | NIST SP 800-38D |
1509
+ | **Authentication** | GCM mode built-in | NIST SP 800-38D |
1510
+
1511
+ ### QR Code Specifications
1512
+
1513
+ | Parameter | Value | Reason |
1514
+ |-----------|-------|--------|
1515
+ | **Error Correction** | Level H (30%) | Maximum damage tolerance |
1516
+ | **Size** | 512×512 pixels | Optimal for scanning |
1517
+ | **Margin** | 2 modules | Compact while scannable |
1518
+ | **Format** | PNG | Lossless, widely supported |
1519
+ | **Encoding** | Base64 data URL | Embeddable in HTML/apps |
1520
+ | **Version** | Auto-detected | Based on data size |
1521
+
1522
+ ### Data Size Limits
1523
+
1524
+ | QR Version | Max Capacity (Level H) | w3pk Usage |
1525
+ |------------|----------------------|------------|
1526
+ | Version 10 | ~468 bytes | Encrypted mnemonic fits |
1527
+ | Version 20 | ~1,248 bytes | Encrypted + metadata fits |
1528
+ | Version 40 | ~2,953 bytes | Maximum supported |
1529
+
1530
+ **w3pk typical sizes:**
1531
+ - Plain mnemonic: ~80-100 characters
1532
+ - Encrypted mnemonic: ~200-300 characters (base64)
1533
+ - Full QR data (JSON): ~400-500 characters
1534
+ - Fits comfortably in Version 10-15 QR codes
1535
+
1536
+ ### Browser Compatibility
1537
+
1538
+ | Feature | Chrome | Firefox | Safari | Edge |
1539
+ |---------|--------|---------|--------|------|
1540
+ | QR generation | ✅ | ✅ | ✅ | ✅ |
1541
+ | Canvas fallback | ✅ | ✅ | ✅ | ✅ |
1542
+ | Crypto API | ✅ | ✅ | ✅ | ✅ |
1543
+ | Data URL download | ✅ | ✅ | ✅ | ✅ |
1544
+
1545
+ **Minimum versions:**
1546
+ - Chrome 90+
1547
+ - Firefox 88+
1548
+ - Safari 14+
1549
+ - Edge 90+
1550
+
1551
+ ---
1552
+
1553
+ ## Troubleshooting
1554
+
1555
+ ### QR Code Generation Issues
1556
+
1557
+ #### Problem: "qrcode library not available"
1558
+
1559
+ **Cause:** Optional dependency `qrcode` not installed.
1560
+
1561
+ **Solution:**
1562
+ ```bash
1563
+ npm install qrcode
1564
+ ```
1565
+
1566
+ **Alternative:** Use encrypted ZIP backup instead:
1567
+ ```typescript
1568
+ const backup = await sdk.createZipBackup('password');
1569
+ ```
1570
+
1571
+ ---
1572
+
1573
+ #### Problem: QR code too small/large
1574
+
1575
+ **Cause:** Default size doesn't fit use case.
1576
+
1577
+ **Solution:** Adjust width in options:
1578
+ ```typescript
1579
+ // Larger QR (768×768)
1580
+ const backup = await sdk.createQRBackup('password', {
1581
+ errorCorrection: 'H',
1582
+ width: 768
1583
+ });
1584
+
1585
+ // Smaller QR (384×384)
1586
+ const backup = await sdk.createQRBackup('password', {
1587
+ errorCorrection: 'H',
1588
+ width: 384
1589
+ });
1590
+ ```
1591
+
1592
+ ---
1593
+
1594
+ #### Problem: QR code won't scan
1595
+
1596
+ **Causes & Solutions:**
1597
+
1598
+ 1. **Poor print quality**
1599
+ - Solution: Print at higher resolution (300+ DPI)
1600
+ - Use laser printer, not inkjet
1601
+
1602
+ 2. **QR code damaged**
1603
+ - Solution: Level H supports up to 30% damage
1604
+ - Try different copy or reprint
1605
+
1606
+ 3. **Lighting issues**
1607
+ - Solution: Scan in bright, even lighting
1608
+ - Avoid glare/shadows
1609
+
1610
+ 4. **Camera focus**
1611
+ - Solution: Hold phone steady, allow autofocus
1612
+ - Clean camera lens
1613
+
1614
+ 5. **Scanning app incompatible**
1615
+ - Solution: Use native phone camera app
1616
+ - iOS: Camera app has built-in QR scanner
1617
+ - Android: Google Lens or Camera app
1618
+
1619
+ ---
1620
+
1621
+ ### Recovery Issues
1622
+
1623
+ #### Problem: "Address checksum mismatch"
1624
+
1625
+ **Causes:**
1626
+ 1. Wrong password entered
1627
+ 2. QR code corrupted/damaged
1628
+ 3. QR code from different wallet
1629
+
1630
+ **Solutions:**
1631
+ 1. Double-check password (case-sensitive!)
1632
+ 2. Try different copy of QR code
1633
+ 3. Verify QR code matches your wallet address
1634
+
1635
+ ---
1636
+
1637
+ #### Problem: "Unsupported QR backup version"
1638
+
1639
+ **Cause:** QR code from newer/older w3pk version.
1640
+
1641
+ **Solution:**
1642
+ ```bash
1643
+ # Update w3pk to latest version
1644
+ npm update w3pk
1645
+ ```
1646
+
1647
+ Or restore using version that created the backup.
1648
+
1649
+ ---
1650
+
1651
+ #### Problem: Can't scan QR code
1652
+
1653
+ **Solutions:**
1654
+
1655
+ 1. **Clean the QR code**
1656
+ - Remove dust, smudges
1657
+ - Ensure paper is flat
1658
+
1659
+ 2. **Adjust camera distance**
1660
+ - Too close: Move back 6-12 inches
1661
+ - Too far: Move closer
1662
+
1663
+ 3. **Improve lighting**
1664
+ - Use bright, indirect light
1665
+ - Avoid glare/shadows
1666
+
1667
+ 4. **Use QR scanner app**
1668
+ - Download dedicated QR scanner
1669
+ - More forgiving than camera app
1670
+
1671
+ 5. **Manual entry (last resort)**
1672
+ - Copy JSON data manually
1673
+ - Paste into recovery form
1674
+
1675
+ ---
1676
+
1677
+ #### Problem: Lost password
1678
+
1679
+ **Solutions:**
1680
+
1681
+ Unfortunately, **password cannot be recovered**. However:
1682
+
1683
+ 1. **Try other recovery methods:**
1684
+ - Passkey sync (if enabled)
1685
+ - Social recovery (if configured)
1686
+ - Plain mnemonic backup (if created)
1687
+
1688
+ 2. **Password hints:**
1689
+ - Check password manager
1690
+ - Review common passwords you use
1691
+ - Ask family members if shared
1692
+
1693
+ 3. **Prevention:**
1694
+ - Store password separately from QR
1695
+ - Use password manager
1696
+ - Set up multiple backup methods
1697
+
1698
+ ---
1699
+
1700
+ ## FAQ
1701
+
1702
+ ### General Questions
1703
+
1704
+ **Q: Is QR backup secure?**
1705
+
1706
+ A: Yes, when password-protected:
1707
+ - Military-grade AES-256-GCM encryption
1708
+ - 310,000 PBKDF2 iterations (brute-force resistant)
1709
+ - Address checksum prevents wrong password
1710
+ - Even with physical QR, password required
1711
+
1712
+ **Q: What if someone finds my QR code?**
1713
+
1714
+ A: If encrypted, they need your password to decrypt. Without password, QR code is useless. This is why strong passwords are critical.
1715
+
1716
+ **Q: Can I store QR in the cloud?**
1717
+
1718
+ A: Yes, if encrypted:
1719
+ - ✅ Google Drive, Dropbox (encrypted QR)
1720
+ - ✅ Password in separate location
1721
+ - ❌ Never store password with QR
1722
+
1723
+ **Q: How many copies should I make?**
1724
+
1725
+ A: Recommended: **3-5 copies** in different locations:
1726
+ - 1 at home (safe)
1727
+ - 1 at bank (safety deposit box)
1728
+ - 1 with family (sealed envelope)
1729
+ - 1-2 backups (various locations)
1730
+
1731
+ **Q: What if QR gets damaged?**
1732
+
1733
+ A: Level H error correction tolerates 30% damage:
1734
+ - Folding/creasing: Usually OK
1735
+ - Water damage: Often OK if dried
1736
+ - Partial tearing: Up to 30% can be missing
1737
+ - Complete destruction: Need another copy
1738
+
1739
+ **Q: Does QR backup expire?**
1740
+
1741
+ A: No! Your mnemonic never changes, so QR backup is valid forever (assuming paper doesn't deteriorate).
1742
+
1743
+ ---
1744
+
1745
+ ### Technical Questions
1746
+
1747
+ **Q: What error correction level should I use?**
1748
+
1749
+ A: **Always use Level H** (30% damage tolerance). This is w3pk's default and recommended for all backups.
1750
+
1751
+ **Q: Can I customize QR appearance?**
1752
+
1753
+ A: Yes, but with caution:
1754
+ ```typescript
1755
+ import QRCode from 'qrcode';
1756
+
1757
+ // Custom colors (ensure good contrast!)
1758
+ QRCode.toDataURL(data, {
1759
+ errorCorrectionLevel: 'H',
1760
+ color: {
1761
+ dark: '#000080', // Navy blue
1762
+ light: '#FFFFFF' // White
1763
+ }
1764
+ });
1765
+ ```
1766
+
1767
+ **Important:** Maintain high contrast for scannability!
1768
+
1769
+ **Q: What's the maximum data size?**
1770
+
1771
+ A: QR Version 40 with Level H supports ~2,953 bytes. w3pk backups typically use ~400-500 bytes, well within limits.
1772
+
1773
+ **Q: Can I backup multiple wallets in one QR?**
1774
+
1775
+ A: Not recommended. Create separate QR codes for each wallet. This:
1776
+ - Limits damage if one is compromised
1777
+ - Easier to manage individually
1778
+ - Smaller QR codes (easier to scan)
1779
+
1780
+ **Q: Does w3pk support plain (unencrypted) QR codes?**
1781
+
1782
+ A: Yes, but **strongly discouraged**:
1783
+ ```typescript
1784
+ // Unencrypted QR (DANGEROUS!)
1785
+ const backup = await sdk.createQRBackup(undefined);
1786
+ ```
1787
+
1788
+ Anyone with this QR can steal your wallet. Only use for testing.
1789
+
1790
+ ---
1791
+
1792
+ ### Recovery Questions
1793
+
1794
+ **Q: Can I recover on a different device?**
1795
+
1796
+ A: Yes! QR backups are cross-device and cross-platform:
1797
+ - Scan on any device with camera
1798
+ - Works on iOS, Android, Windows, Mac, Linux
1799
+ - Compatible with any BIP39 wallet
1800
+
1801
+ **Q: Do I need w3pk to recover?**
1802
+
1803
+ A: No! Your mnemonic is BIP39-compatible:
1804
+ 1. Decrypt QR code (with w3pk or manually)
1805
+ 2. Extract mnemonic
1806
+ 3. Import into **any** BIP39 wallet (MetaMask, Ledger, etc.)
1807
+
1808
+ **Q: How long does recovery take?**
1809
+
1810
+ A: Typically **2-5 minutes**:
1811
+ 1. Scan QR code (30 seconds)
1812
+ 2. Enter password (30 seconds)
1813
+ 3. Verify address (1 minute)
1814
+ 4. Create new passkey (1 minute)
1815
+ 5. Test wallet (2 minutes)
1816
+
1817
+ **Q: Can I test recovery without losing my current wallet?**
1818
+
1819
+ A: Yes! Use a different browser/device:
1820
+ 1. Create test wallet
1821
+ 2. Generate QR backup
1822
+ 3. Open incognito/private window
1823
+ 4. Restore from QR
1824
+ 5. Verify it works
1825
+ 6. Delete test wallet
1826
+
1827
+ Never lose access to your main wallet during testing.
1828
+
1829
+ ---
1830
+
1831
+ ### Security Questions
1832
+
1833
+ **Q: What if my password is compromised?**
1834
+
1835
+ A: If someone has both your QR code AND password:
1836
+ 1. **Immediately move funds** to a new wallet
1837
+ 2. Create new backup with different password
1838
+ 3. Destroy old QR codes
1839
+ 4. Review how compromise occurred
1840
+
1841
+ **Q: Should I share my QR with family?**
1842
+
1843
+ A: Depends:
1844
+ - ✅ Encrypted QR: Safe to share (keep password separate)
1845
+ - ❌ Plain QR: Never share (instant wallet theft)
1846
+ - ✅ Sealed envelope: Good middle ground
1847
+
1848
+ **Q: What happens if w3pk shuts down?**
1849
+
1850
+ A: You're still safe!
1851
+ - QR backups are standard BIP39 format
1852
+ - Import into any wallet (MetaMask, Trust Wallet, etc.)
1853
+ - No vendor lock-in
1854
+
1855
+ **Q: Can quantum computers break QR encryption?**
1856
+
1857
+ A: Current quantum computers: No. Future: Possibly.
1858
+
1859
+ **Mitigation:**
1860
+ - AES-256 has strong quantum resistance
1861
+ - Re-encrypt with post-quantum algorithms when available
1862
+ - Most vulnerable part is PBKDF2 (use very strong password)
1863
+
1864
+ ---
1865
+
1866
+ ## Additional Resources
1867
+
1868
+ ### Documentation
1869
+ - [Recovery System Overview](RECOVERY.md)
1870
+ - [Security Architecture](SECURITY.md)
1871
+ - [API Documentation](../README.md)
1872
+
1873
+ ### External References
1874
+ - [QR Code Specification (ISO/IEC 18004)](https://www.iso.org/standard/62021.html)
1875
+ - [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
1876
+ - [NIST Cryptographic Standards](https://csrc.nist.gov/publications)
1877
+ - [BIP39 Mnemonic Specification](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)
1878
+
1879
+ ### Community
1880
+ - [GitHub Issues](https://github.com/w3hc/w3pk/issues)
1881
+ - [Security Reporting](../SECURITY.md#reporting-security-issues)
1882
+
1883
+ ---
1884
+
1885
+ **Last Updated:** 2025-10-30
1886
+ **w3pk Version:** 0.7.1+
1887
+ **Document Version:** 1.0