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.
- package/README.md +76 -19
- package/dist/index.d.mts +650 -5
- package/dist/index.d.ts +650 -5
- package/dist/index.js +776 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +776 -5
- package/dist/index.mjs.map +1 -1
- package/docs/BUNDLE_SIZES.md +7 -4
- package/docs/MIGRATION.md +15 -15
- package/docs/QR_CODE.md +1887 -0
- package/docs/RECOVERY.md +992 -0
- package/docs/SECURITY.md +631 -0
- package/docs/ZK_INTEGRATION_GUIDE.md +6 -4
- package/docs/index.html +4 -3
- package/package.json +9 -2
package/docs/QR_CODE.md
ADDED
|
@@ -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
|