w3pk 0.9.2 → 0.10.0
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 +21 -5
- package/dist/index.d.mts +300 -1
- package/dist/index.d.ts +300 -1
- package/dist/index.js +111 -108
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +111 -108
- package/dist/index.mjs.map +1 -1
- package/docs/API_REFERENCE.md +359 -35
- package/docs/ARCHITECTURE.md +42 -45
- package/docs/POST_QUANTUM.md +288 -3
- package/docs/RECOVERY.md +138 -99
- package/docs/SECURITY.md +239 -20
- package/package.json +13 -10
package/docs/POST_QUANTUM.md
CHANGED
|
@@ -1290,14 +1290,299 @@ const delegation = await w3pk.requestExternalWalletDelegation({
|
|
|
1290
1290
|
|
|
1291
1291
|
---
|
|
1292
1292
|
|
|
1293
|
+
## ML-KEM Encryption
|
|
1294
|
+
|
|
1295
|
+
w3pk supports ML-KEM-1024 encryption for quantum-resistant data protection with multi-recipient support and deterministic key derivation from private keys.
|
|
1296
|
+
|
|
1297
|
+
### Deterministic Key Derivation
|
|
1298
|
+
|
|
1299
|
+
Derive ML-KEM keypairs deterministically from any private key material (Ethereum keys, random seeds, etc.):
|
|
1300
|
+
|
|
1301
|
+
```typescript
|
|
1302
|
+
import { deriveMLKemKeypair, mlkemEncryptWithKey, mlkemDecryptWithKey } from 'w3pk';
|
|
1303
|
+
|
|
1304
|
+
// Derive ML-KEM keypair from Ethereum private key
|
|
1305
|
+
const ethPrivateKey = '0x1234...'; // Your wallet private key
|
|
1306
|
+
const keypair = await deriveMLKemKeypair(ethPrivateKey, 'my-app');
|
|
1307
|
+
// Returns: { publicKey: Uint8Array(1568), privateKey: Uint8Array(3168) }
|
|
1308
|
+
|
|
1309
|
+
// Share your public key with others
|
|
1310
|
+
const publicKeyBase64 = Buffer.from(keypair.publicKey).toString('base64');
|
|
1311
|
+
|
|
1312
|
+
// Later: Recreate keypair from same private key (deterministic!)
|
|
1313
|
+
const sameKeypair = await deriveMLKemKeypair(ethPrivateKey, 'my-app');
|
|
1314
|
+
// sameKeypair.publicKey === keypair.publicKey ✅
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
### Encrypt/Decrypt with w3pk Instance (Recommended)
|
|
1318
|
+
|
|
1319
|
+
The easiest way to use ML-KEM encryption - no private key exposure:
|
|
1320
|
+
|
|
1321
|
+
```typescript
|
|
1322
|
+
import { createWeb3Passkey } from 'w3pk';
|
|
1323
|
+
|
|
1324
|
+
const w3pk = createWeb3Passkey();
|
|
1325
|
+
await w3pk.login();
|
|
1326
|
+
|
|
1327
|
+
// 1. Share your public key with others
|
|
1328
|
+
const myPublicKey = await w3pk.deriveMLKemPublicKey();
|
|
1329
|
+
// Send myPublicKey to the server or publish it
|
|
1330
|
+
|
|
1331
|
+
// 2. Get server's public key
|
|
1332
|
+
const serverPubKey = await fetch('/api/mlkem-public-key').then(r => r.text());
|
|
1333
|
+
|
|
1334
|
+
// 3. Encrypt for yourself + server
|
|
1335
|
+
const encrypted = await w3pk.mlkemEncrypt(
|
|
1336
|
+
'my secret data',
|
|
1337
|
+
[serverPubKey] // Server can decrypt
|
|
1338
|
+
);
|
|
1339
|
+
// You can also decrypt because you're auto-added as recipient
|
|
1340
|
+
|
|
1341
|
+
// 4. Later: Decrypt locally (server never sees plaintext!)
|
|
1342
|
+
const plaintext = await w3pk.mlkemDecrypt(encrypted);
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
**Security:** Your Ethereum private key never leaves the w3pk instance, matching the security model of `signMessage()` and `sendTransaction()`.
|
|
1346
|
+
|
|
1347
|
+
**Supported Modes:**
|
|
1348
|
+
- ✅ **STANDARD** (default) - Private key derived internally, not exposed to app
|
|
1349
|
+
- ✅ **STRICT** - Same as STANDARD but always requires WebAuthn re-authentication
|
|
1350
|
+
- ✅ **YOLO** - Private key available to app
|
|
1351
|
+
- ❌ **PRIMARY** - Not supported (uses P-256 WebAuthn keys, not Ethereum keys)
|
|
1352
|
+
|
|
1353
|
+
### Encrypt/Decrypt with Key Derivation (Low-level API)
|
|
1354
|
+
|
|
1355
|
+
For advanced use cases where you manage private keys directly:
|
|
1356
|
+
|
|
1357
|
+
```typescript
|
|
1358
|
+
// Client encrypts for themselves + server
|
|
1359
|
+
const serverKeypair = await deriveMLKemKeypair(serverEthKey, 'server');
|
|
1360
|
+
const encrypted = await mlkemEncryptWithKey(
|
|
1361
|
+
'my secret data',
|
|
1362
|
+
myEthPrivateKey, // Sender's private key (auto-added as recipient)
|
|
1363
|
+
[serverKeypair.publicKey], // Additional recipients
|
|
1364
|
+
'client' // Context for sender's key derivation
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
// Later: Client decrypts locally (no server needed!)
|
|
1368
|
+
const plaintext = await mlkemDecryptWithKey(
|
|
1369
|
+
encrypted,
|
|
1370
|
+
myEthPrivateKey, // Same private key used for encryption
|
|
1371
|
+
'client' // Same context
|
|
1372
|
+
);
|
|
1373
|
+
|
|
1374
|
+
// Or: Server decrypts for operations
|
|
1375
|
+
const plaintext = await mlkemDecryptWithKey(
|
|
1376
|
+
encrypted,
|
|
1377
|
+
serverEthKey,
|
|
1378
|
+
'server'
|
|
1379
|
+
);
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
### Direct Encryption (Without Key Derivation)
|
|
1383
|
+
|
|
1384
|
+
For maximum flexibility, use raw public keys directly:
|
|
1385
|
+
|
|
1386
|
+
```typescript
|
|
1387
|
+
import { mlkemEncrypt, mlkemDecrypt } from 'w3pk';
|
|
1388
|
+
|
|
1389
|
+
// Single recipient
|
|
1390
|
+
const publicKey = 'base64_encoded_public_key...'; // 1568 bytes
|
|
1391
|
+
const encrypted = await mlkemEncrypt('my secret data', publicKey);
|
|
1392
|
+
|
|
1393
|
+
// Multiple recipients (encrypt once, multiple people can decrypt)
|
|
1394
|
+
const encrypted = await mlkemEncrypt('my secret data', [
|
|
1395
|
+
publicKey1,
|
|
1396
|
+
publicKey2,
|
|
1397
|
+
publicKey3
|
|
1398
|
+
]);
|
|
1399
|
+
// Returns: { recipients: [...], encryptedData, iv, authTag }
|
|
1400
|
+
|
|
1401
|
+
// Decrypt with your private key (auto-detects which recipient you are)
|
|
1402
|
+
const privateKey = 'base64_encoded_private_key...'; // 3168 bytes
|
|
1403
|
+
const plaintext = await mlkemDecrypt(encrypted, privateKey);
|
|
1404
|
+
// Returns: 'my secret data'
|
|
1405
|
+
|
|
1406
|
+
// Or specify your public key for faster lookup
|
|
1407
|
+
const plaintext = await mlkemDecrypt(encrypted, privateKey, publicKey);
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
### Security Properties
|
|
1411
|
+
|
|
1412
|
+
- ✅ **ML-KEM-1024** (NIST FIPS 203) - Post-quantum secure
|
|
1413
|
+
- ✅ **AES-256-GCM** - 128-bit quantum security (sufficient)
|
|
1414
|
+
- ✅ **Key zeroization** - Shared secrets securely wiped from memory
|
|
1415
|
+
- ✅ **Hybrid encryption** - KEM + symmetric cipher for efficiency
|
|
1416
|
+
- ✅ **Cross-platform** - Works in browser and Node.js
|
|
1417
|
+
|
|
1418
|
+
### API Reference
|
|
1419
|
+
|
|
1420
|
+
#### Instance Methods (Recommended)
|
|
1421
|
+
|
|
1422
|
+
These methods work with your w3pk wallet instance - no private key exposure:
|
|
1423
|
+
|
|
1424
|
+
#### `w3pk.deriveMLKemPublicKey(options?): Promise<string>`
|
|
1425
|
+
|
|
1426
|
+
Derives your ML-KEM-1024 public key that can be shared for encryption.
|
|
1427
|
+
|
|
1428
|
+
**Parameters:**
|
|
1429
|
+
- `options.context` - Context string for domain separation (default: `'mlkem-v1'`)
|
|
1430
|
+
- `options.mode` - Security mode: `'STANDARD'`, `'STRICT'`, or `'YOLO'` (default: `'STANDARD'`)
|
|
1431
|
+
- `options.tag` - Tag for address derivation (default: `'MAIN'`)
|
|
1432
|
+
- `options.origin` - Origin for derivation (default: current origin)
|
|
1433
|
+
- `options.requireAuth` - Force WebAuthn re-authentication (default: false, always true in STRICT mode)
|
|
1434
|
+
|
|
1435
|
+
**Note:** PRIMARY mode is not supported as it uses P-256 WebAuthn keys instead of Ethereum keys.
|
|
1436
|
+
|
|
1437
|
+
**Returns:** Base64-encoded ML-KEM public key (1568 bytes)
|
|
1438
|
+
|
|
1439
|
+
**Example:**
|
|
1440
|
+
```typescript
|
|
1441
|
+
const w3pk = createWeb3Passkey();
|
|
1442
|
+
await w3pk.login();
|
|
1443
|
+
const pubKey = await w3pk.deriveMLKemPublicKey();
|
|
1444
|
+
```
|
|
1445
|
+
|
|
1446
|
+
#### `w3pk.mlkemEncrypt(plaintext: string, recipientPublicKeys: Array<string | Uint8Array>, options?): Promise<EncryptedPayload>`
|
|
1447
|
+
|
|
1448
|
+
Encrypts data for yourself and additional recipients.
|
|
1449
|
+
|
|
1450
|
+
**Parameters:**
|
|
1451
|
+
- `plaintext` - The data to encrypt
|
|
1452
|
+
- `recipientPublicKeys` - Array of recipient ML-KEM public keys (base64 or Uint8Array)
|
|
1453
|
+
- `options.context` - Context string (default: `'mlkem-v1'`)
|
|
1454
|
+
- `options.mode` - Security mode: `'STANDARD'`, `'STRICT'`, or `'YOLO'` (default: `'STANDARD'`)
|
|
1455
|
+
- `options.tag` - Tag for address derivation (default: `'MAIN'`)
|
|
1456
|
+
- `options.origin` - Origin (default: current origin)
|
|
1457
|
+
- `options.requireAuth` - Force re-authentication (default: false, always true in STRICT mode)
|
|
1458
|
+
|
|
1459
|
+
**Returns:** Encrypted payload
|
|
1460
|
+
|
|
1461
|
+
**Example:**
|
|
1462
|
+
```typescript
|
|
1463
|
+
const encrypted = await w3pk.mlkemEncrypt('secret', [serverPubKey]);
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
#### `w3pk.mlkemDecrypt(payload: EncryptedPayload, options?): Promise<string>`
|
|
1467
|
+
|
|
1468
|
+
Decrypts data encrypted for your wallet.
|
|
1469
|
+
|
|
1470
|
+
**Parameters:**
|
|
1471
|
+
- `payload` - The encrypted payload
|
|
1472
|
+
- `options.context` - Context string (default: `'mlkem-v1'`, must match encryption)
|
|
1473
|
+
- `options.mode` - Security mode: `'STANDARD'`, `'STRICT'`, or `'YOLO'` (default: `'STANDARD'`)
|
|
1474
|
+
- `options.tag` - Tag (default: `'MAIN'`)
|
|
1475
|
+
- `options.origin` - Origin (default: current origin)
|
|
1476
|
+
- `options.requireAuth` - Force re-authentication (default: false, always true in STRICT mode)
|
|
1477
|
+
|
|
1478
|
+
**Returns:** Decrypted plaintext
|
|
1479
|
+
|
|
1480
|
+
**Example:**
|
|
1481
|
+
```typescript
|
|
1482
|
+
const plaintext = await w3pk.mlkemDecrypt(encrypted);
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
---
|
|
1486
|
+
|
|
1487
|
+
#### Low-Level Functions
|
|
1488
|
+
|
|
1489
|
+
These functions require managing private keys directly:
|
|
1490
|
+
|
|
1491
|
+
#### `deriveMLKemKeypair(privateKey: string | Uint8Array, context?: string): Promise<MLKemKeypair>`
|
|
1492
|
+
|
|
1493
|
+
Derives a deterministic ML-KEM-1024 keypair from any private key material using HKDF-SHA256.
|
|
1494
|
+
|
|
1495
|
+
**Parameters:**
|
|
1496
|
+
- `privateKey` - Private key material (hex string with optional `0x` prefix, or Uint8Array)
|
|
1497
|
+
- `context` - Optional context string for domain separation (default: `'mlkem-v1'`)
|
|
1498
|
+
|
|
1499
|
+
**Returns:** `MLKemKeypair` object containing:
|
|
1500
|
+
- `publicKey` - ML-KEM-1024 public key (Uint8Array, 1568 bytes)
|
|
1501
|
+
- `privateKey` - ML-KEM-1024 private key (Uint8Array, 3168 bytes)
|
|
1502
|
+
|
|
1503
|
+
**Security:** Uses HKDF with salt `"mlkem-keypair-v1"` and the provided context to derive a 64-byte seed, then generates a deterministic ML-KEM keypair. Same input always produces same output. All sensitive material is zeroized after use.
|
|
1504
|
+
|
|
1505
|
+
#### `mlkemEncryptWithKey(plaintext: string, senderPrivateKey: string | Uint8Array, recipientPublicKeys: Array<string | Uint8Array>, senderContext?: string): Promise<EncryptedPayload>`
|
|
1506
|
+
|
|
1507
|
+
Convenience function that derives the sender's ML-KEM keypair, then encrypts for sender + recipients.
|
|
1508
|
+
|
|
1509
|
+
**Parameters:**
|
|
1510
|
+
- `plaintext` - The data to encrypt
|
|
1511
|
+
- `senderPrivateKey` - Sender's private key (hex string or Uint8Array)
|
|
1512
|
+
- `recipientPublicKeys` - Array of recipient ML-KEM public keys
|
|
1513
|
+
- `senderContext` - Optional context for sender's key derivation (default: `'mlkem-v1'`)
|
|
1514
|
+
|
|
1515
|
+
**Returns:** `EncryptedPayload` (sender's public key is automatically included as first recipient)
|
|
1516
|
+
|
|
1517
|
+
**Use case:** Encrypt data so both you and a server can decrypt independently.
|
|
1518
|
+
|
|
1519
|
+
#### `mlkemDecryptWithKey(payload: EncryptedPayload, privateKey: string | Uint8Array, context?: string): Promise<string>`
|
|
1520
|
+
|
|
1521
|
+
Convenience function that derives an ML-KEM keypair, then decrypts the payload.
|
|
1522
|
+
|
|
1523
|
+
**Parameters:**
|
|
1524
|
+
- `payload` - The encrypted payload
|
|
1525
|
+
- `privateKey` - Private key material (hex string or Uint8Array)
|
|
1526
|
+
- `context` - Optional context for key derivation (default: `'mlkem-v1'`)
|
|
1527
|
+
|
|
1528
|
+
**Returns:** Decrypted plaintext string
|
|
1529
|
+
|
|
1530
|
+
**Use case:** Decrypt data using your Ethereum private key without managing separate ML-KEM keys.
|
|
1531
|
+
|
|
1532
|
+
#### `mlkemEncrypt(plaintext: string, publicKeys: (string | Uint8Array) | Array<string | Uint8Array>): Promise<EncryptedPayload>`
|
|
1533
|
+
|
|
1534
|
+
Encrypts data using ML-KEM-1024 + AES-256-GCM for one or more recipients.
|
|
1535
|
+
|
|
1536
|
+
**Parameters:**
|
|
1537
|
+
- `plaintext` - The data to encrypt
|
|
1538
|
+
- `publicKeys` - Single public key or array of ML-KEM-1024 public keys (base64 strings or Uint8Arrays, 1568 bytes each)
|
|
1539
|
+
|
|
1540
|
+
**Returns:** `EncryptedPayload` object containing:
|
|
1541
|
+
- `recipients` - Array of recipient entries, each containing:
|
|
1542
|
+
- `publicKey` - Base64 recipient public key (1568 bytes)
|
|
1543
|
+
- `ciphertext` - Base64 ML-KEM ciphertext for this recipient (1600 bytes: 1568 KEM + 32 encrypted AES key)
|
|
1544
|
+
- `encryptedData` - Base64 AES-encrypted data (shared across all recipients)
|
|
1545
|
+
- `iv` - Base64 initialization vector (12 bytes)
|
|
1546
|
+
- `authTag` - Base64 authentication tag (16 bytes)
|
|
1547
|
+
|
|
1548
|
+
**How it works:**
|
|
1549
|
+
1. Generates a random AES-256 key
|
|
1550
|
+
2. Encrypts plaintext with AES-256-GCM using that key
|
|
1551
|
+
3. For each recipient: encapsulates a shared secret with their ML-KEM public key, then XOR-encrypts the AES key with the shared secret
|
|
1552
|
+
4. Each recipient can decrypt using their private key to recover the AES key, then decrypt the data
|
|
1553
|
+
|
|
1554
|
+
#### `mlkemDecrypt(payload: EncryptedPayload, privateKey: string | Uint8Array, publicKey?: string | Uint8Array): Promise<string>`
|
|
1555
|
+
|
|
1556
|
+
Decrypts data encrypted with `mlkemEncrypt()`.
|
|
1557
|
+
|
|
1558
|
+
**Parameters:**
|
|
1559
|
+
- `payload` - The encrypted payload from `mlkemEncrypt()`
|
|
1560
|
+
- `privateKey` - ML-KEM-1024 private key (base64 string or Uint8Array, 3168 bytes)
|
|
1561
|
+
- `publicKey` - (Optional) Your ML-KEM-1024 public key for faster recipient lookup (base64 string or Uint8Array, 1568 bytes)
|
|
1562
|
+
|
|
1563
|
+
**Returns:** Decrypted plaintext string
|
|
1564
|
+
|
|
1565
|
+
**Throws:** Error if decryption fails (invalid key, no matching recipient, corrupted data, or tampered auth tag)
|
|
1566
|
+
|
|
1567
|
+
**Note:** If `publicKey` is not provided, the function will try each recipient until it finds a match (slower but convenient)
|
|
1568
|
+
|
|
1569
|
+
---
|
|
1570
|
+
|
|
1293
1571
|
## Document Maintenance
|
|
1294
1572
|
|
|
1295
|
-
**Version:** 1.
|
|
1296
|
-
**Last Updated:** 2026-
|
|
1297
|
-
**Next Review:** 2026-
|
|
1573
|
+
**Version:** 1.2
|
|
1574
|
+
**Last Updated:** 2026-03-21
|
|
1575
|
+
**Next Review:** 2026-09-21 (6 months)
|
|
1298
1576
|
**Maintained By:** Julien Béranger ([@julienbrg](https://github.com/julienbrg))
|
|
1299
1577
|
|
|
1300
1578
|
**Changelog:**
|
|
1579
|
+
- 2026-03-21: Added ML-KEM encryption utilities with deterministic key derivation
|
|
1580
|
+
- Documented `deriveMLKemKeypair()` function for HKDF-based key derivation from private keys
|
|
1581
|
+
- Documented `mlkemEncryptWithKey()` and `mlkemDecryptWithKey()` convenience functions
|
|
1582
|
+
- Documented `mlkemEncrypt()` function for direct post-quantum encryption
|
|
1583
|
+
- Documented `mlkemDecrypt()` function for decryption
|
|
1584
|
+
- Added security properties and comprehensive API reference
|
|
1585
|
+
- Linked to NIST FIPS 203 (ML-KEM) standard
|
|
1301
1586
|
- 2026-02-27: **Updated to align with [Ethereum quantum resistance roadmap](https://x.com/VitalikButerin/status/2027075026378543132)** (February 2026)
|
|
1302
1587
|
- Added Ethereum's four quantum-vulnerable components
|
|
1303
1588
|
- Integrated [EIP-8141](https://eips.ethereum.org/EIPS/eip-8141) (native AA) as long-term solution
|
package/docs/RECOVERY.md
CHANGED
|
@@ -264,15 +264,25 @@ await sdk.importFromSync(syncData);
|
|
|
264
264
|
|
|
265
265
|
### **3. Social Recovery**
|
|
266
266
|
|
|
267
|
-
**Purpose**: Distribute encrypted
|
|
267
|
+
**Purpose**: Distribute encrypted backup file fragments among trusted guardians using Shamir Secret Sharing (M-of-N)
|
|
268
268
|
|
|
269
269
|
**Setup (3-of-5 example):**
|
|
270
270
|
```typescript
|
|
271
|
+
import { SocialRecoveryManager } from 'w3pk';
|
|
272
|
+
|
|
271
273
|
const sdk = createWeb3Passkey();
|
|
272
274
|
await sdk.login();
|
|
273
275
|
|
|
274
|
-
// Create encrypted backup
|
|
275
|
-
const
|
|
276
|
+
// Step 1: Create password-encrypted backup file
|
|
277
|
+
const password = 'MySecurePassword123!'; // User must remember this
|
|
278
|
+
const { blob } = await sdk.createBackupFile('password', password);
|
|
279
|
+
const backupFileJson = await blob.text();
|
|
280
|
+
|
|
281
|
+
// Step 2: Split backup file among guardians
|
|
282
|
+
const socialRecovery = new SocialRecoveryManager();
|
|
283
|
+
const guardians = await socialRecovery.setupSocialRecovery(
|
|
284
|
+
backupFileJson, // Encrypted backup file JSON
|
|
285
|
+
sdk.user.ethereumAddress,
|
|
276
286
|
[
|
|
277
287
|
{ name: 'Alice', email: 'alice@example.com' },
|
|
278
288
|
{ name: 'Bob', email: 'bob@example.com' },
|
|
@@ -280,84 +290,61 @@ const { guardianShares } = await sdk.setupSocialRecovery(
|
|
|
280
290
|
{ name: 'David', email: 'david@example.com' },
|
|
281
291
|
{ name: 'Eve', email: 'eve@example.com' },
|
|
282
292
|
],
|
|
283
|
-
3
|
|
284
|
-
'OptionalPassword' // encrypts backup before splitting
|
|
293
|
+
3 // threshold: need 3 shares to recover
|
|
285
294
|
);
|
|
286
295
|
|
|
287
|
-
// Generate invitation for each guardian
|
|
288
|
-
for (const
|
|
289
|
-
const
|
|
290
|
-
// Send
|
|
296
|
+
// Step 3: Generate invitation for each guardian
|
|
297
|
+
for (const guardian of guardians) {
|
|
298
|
+
const invite = await socialRecovery.generateGuardianInvite(guardian);
|
|
299
|
+
// Send invite.qrCode and invite.explainer to guardian
|
|
300
|
+
// Guardian stores their share safely (password manager, etc.)
|
|
291
301
|
}
|
|
292
302
|
```
|
|
293
303
|
|
|
294
|
-
**Recover Wallet
|
|
295
|
-
|
|
296
|
-
**Option 1: Recover and register new passkey (recommended for lost devices)**
|
|
304
|
+
**Recover Wallet:**
|
|
297
305
|
```typescript
|
|
306
|
+
import { SocialRecoveryManager } from 'w3pk';
|
|
307
|
+
|
|
298
308
|
const sdk = createWeb3Passkey();
|
|
309
|
+
const socialRecovery = new SocialRecoveryManager();
|
|
299
310
|
|
|
300
311
|
// Step 1: Collect shares from 3+ guardians
|
|
301
|
-
const shares = [
|
|
312
|
+
const shares = [aliceShareCode, bobShareCode, charlieShareCode];
|
|
302
313
|
|
|
303
|
-
// Step 2:
|
|
304
|
-
const {
|
|
305
|
-
shares,
|
|
306
|
-
'OptionalPassword' // password used during setupSocialRecovery (if any)
|
|
307
|
-
);
|
|
314
|
+
// Step 2: Reconstruct encrypted backup file from shares
|
|
315
|
+
const { backupFileJson, ethereumAddress } = await socialRecovery.recoverFromGuardians(shares);
|
|
308
316
|
|
|
309
|
-
// Step 3:
|
|
310
|
-
const
|
|
311
|
-
const manager = new BackupFileManager();
|
|
312
|
-
const { backupFile } = await manager.createPasswordBackup(
|
|
313
|
-
mnemonic,
|
|
314
|
-
ethereumAddress,
|
|
315
|
-
'NewPassword123!' // Choose a new password
|
|
316
|
-
);
|
|
317
|
+
// Step 3: Decrypt backup file with password
|
|
318
|
+
const password = prompt('Enter your backup password');
|
|
317
319
|
|
|
318
|
-
// Step 4: Register new passkey
|
|
319
|
-
|
|
320
|
-
await sdk.registerWithBackupFile(backupData, 'NewPassword123!', 'username');
|
|
320
|
+
// Step 4: Register new passkey with recovered backup file
|
|
321
|
+
await sdk.registerWithBackupFile(backupFileJson, password, 'username');
|
|
321
322
|
// Now logged in with new passkey, credentials stored locally ✅
|
|
322
323
|
```
|
|
323
324
|
|
|
324
|
-
**Option 2: Recover and import to existing passkey**
|
|
325
|
-
```typescript
|
|
326
|
-
const sdk = createWeb3Passkey();
|
|
327
|
-
|
|
328
|
-
// Step 1: Login with existing WebAuthn credential
|
|
329
|
-
await sdk.login();
|
|
330
|
-
|
|
331
|
-
// Step 2: Collect shares and recover mnemonic
|
|
332
|
-
const shares = [aliceShare, bobShare, charlieShare];
|
|
333
|
-
const { mnemonic } = await sdk.recoverFromGuardians(
|
|
334
|
-
shares,
|
|
335
|
-
'OptionalPassword'
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
// Step 3: Import mnemonic to current logged-in user
|
|
339
|
-
await sdk.importMnemonic(mnemonic);
|
|
340
|
-
// Credentials stored locally under current passkey ✅
|
|
341
|
-
```
|
|
342
|
-
|
|
343
325
|
**Encryption**:
|
|
344
|
-
1.
|
|
345
|
-
2. Serialize to JSON
|
|
346
|
-
3. Split using Shamir Secret Sharing (M-of-N)
|
|
347
|
-
4. Each guardian gets one
|
|
326
|
+
1. User creates password-encrypted BackupFile (AES-256-GCM)
|
|
327
|
+
2. Serialize BackupFile to JSON
|
|
328
|
+
3. Split JSON using Shamir Secret Sharing (M-of-N threshold)
|
|
329
|
+
4. Each guardian gets one encrypted fragment
|
|
330
|
+
5. Recovery requires: threshold shares + original password
|
|
348
331
|
|
|
349
332
|
**Use Case**: Forgot password, lost all devices, ultimate safety net
|
|
350
333
|
|
|
351
|
-
**Security**: No single guardian can recover wallet alone
|
|
334
|
+
**Security**: No single guardian can recover wallet alone. Guardians hold fragments of an already-encrypted backup file.
|
|
352
335
|
|
|
353
336
|
**Pros:**
|
|
354
337
|
- ✅ No single point of failure
|
|
338
|
+
- ✅ Double encryption: backup is encrypted, then fragments are distributed
|
|
339
|
+
- ✅ Guardians never see plaintext wallet data
|
|
340
|
+
- ✅ Password protection even after collecting shares
|
|
355
341
|
- ✅ Survives your own forgetfulness
|
|
356
342
|
- ✅ Survives any 2 guardians disappearing
|
|
357
343
|
- ✅ Highest security and redundancy
|
|
358
344
|
|
|
359
345
|
**Cons:**
|
|
360
346
|
- ⚠️ Requires trusted friends/family
|
|
347
|
+
- ⚠️ Must remember password (guardians don't have it)
|
|
361
348
|
- ⚠️ Setup complexity
|
|
362
349
|
- ⚠️ Recovery takes time (24-48 hours)
|
|
363
350
|
- ⚠️ Coordination required
|
|
@@ -365,9 +352,10 @@ await sdk.importMnemonic(mnemonic);
|
|
|
365
352
|
**Recovery scenarios:**
|
|
366
353
|
| Scenario | Can Recover? | How |
|
|
367
354
|
|----------|--------------|-----|
|
|
368
|
-
|
|
|
369
|
-
| Lost backup + lost passkey | ✅ Yes | Contact 3 guardians |
|
|
355
|
+
| Lost all devices | ✅ Yes | Contact 3 guardians → combine shares → enter password |
|
|
356
|
+
| Lost backup + lost passkey | ✅ Yes | Contact 3 guardians → reconstruct backup file → enter password |
|
|
370
357
|
| 2 guardians unavailable | ✅ Yes | Still have 3 others |
|
|
358
|
+
| Forgot password + have shares | ❌ No | Backup file is encrypted with password |
|
|
371
359
|
| All guardians lost shares | ❌ No | Need another recovery layer |
|
|
372
360
|
|
|
373
361
|
---
|
|
@@ -396,45 +384,81 @@ await sdk.importMnemonic(mnemonic);
|
|
|
396
384
|
### **Social Recovery Cryptography**
|
|
397
385
|
|
|
398
386
|
```typescript
|
|
399
|
-
Shamir Secret Sharing
|
|
387
|
+
Backup File-Based Shamir Secret Sharing:
|
|
400
388
|
|
|
401
389
|
Example: 3-of-5 scheme
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
-
|
|
405
|
-
-
|
|
390
|
+
|
|
391
|
+
Step 1: Create Encrypted Backup File
|
|
392
|
+
- User creates password-encrypted backup file
|
|
393
|
+
- Backup contains: { encryptedMnemonic, ethereumAddress, ... }
|
|
394
|
+
- Encryption: PBKDF2(password) → AES-256-GCM
|
|
395
|
+
- Result: BackupFile JSON (already encrypted)
|
|
396
|
+
|
|
397
|
+
Step 2: Split Backup File
|
|
398
|
+
- Serialize BackupFile to JSON string
|
|
399
|
+
- Convert JSON to bytes
|
|
400
|
+
- Split bytes using Shamir Secret Sharing (3-of-5)
|
|
401
|
+
- Result: 5 shares of encrypted data
|
|
402
|
+
|
|
403
|
+
Step 3: Distribute Shares
|
|
404
|
+
- Each guardian gets one share (hex-encoded fragment)
|
|
405
|
+
- Shares are fragments of the encrypted backup file
|
|
406
|
+
- Guardians never see the plaintext mnemonic
|
|
407
|
+
- Guardians don't have the password
|
|
408
|
+
|
|
409
|
+
Recovery Process:
|
|
410
|
+
- Collect 3+ shares from guardians
|
|
411
|
+
- Combine shares → reconstruct BackupFile JSON
|
|
412
|
+
- Decrypt with password → extract mnemonic
|
|
413
|
+
- Register new passkey or import to existing
|
|
406
414
|
|
|
407
415
|
Mathematical basis:
|
|
408
|
-
- Polynomial interpolation over finite field
|
|
416
|
+
- Polynomial interpolation over finite field GF(256)
|
|
409
417
|
- Degree = threshold - 1 (e.g., degree-2 polynomial for 3-of-5)
|
|
410
418
|
- Points on polynomial = shares
|
|
411
|
-
-
|
|
419
|
+
- Any 3 points → reconstruct polynomial → recover secret
|
|
420
|
+
- Any 2 points → mathematically impossible to recover
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**Security Model:**
|
|
424
|
+
```
|
|
425
|
+
Double Encryption:
|
|
426
|
+
Layer 1: Backup file encrypted with password (AES-256-GCM)
|
|
427
|
+
Layer 2: Encrypted backup split into shares (Shamir)
|
|
428
|
+
|
|
429
|
+
Result:
|
|
430
|
+
- Guardians hold fragments of encrypted data
|
|
431
|
+
- Even with threshold shares, password still required
|
|
432
|
+
- No single guardian has access to wallet
|
|
433
|
+
- No collusion of guardians can bypass password
|
|
412
434
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
- Prevents share theft from coordinator
|
|
435
|
+
Attack Scenarios:
|
|
436
|
+
✅ Attacker steals 2 shares → Cannot reconstruct
|
|
437
|
+
✅ Attacker steals 3+ shares → Still needs password
|
|
438
|
+
✅ Attacker has password → Still needs threshold shares
|
|
439
|
+
❌ Attacker has password + threshold shares → Can recover
|
|
419
440
|
```
|
|
420
441
|
|
|
421
442
|
**Trust model:**
|
|
422
443
|
```
|
|
423
444
|
Coordinator (You):
|
|
445
|
+
- Must remember password (guardians don't have it)
|
|
424
446
|
- Cannot recover from less than threshold shares
|
|
425
|
-
- Can revoke/replace guardians
|
|
426
|
-
- Can
|
|
447
|
+
- Can revoke/replace guardians (requires new setup)
|
|
448
|
+
- Can verify guardian shares are distributed
|
|
427
449
|
|
|
428
450
|
Guardians:
|
|
429
|
-
-
|
|
430
|
-
- Cannot
|
|
451
|
+
- Hold encrypted fragments only
|
|
452
|
+
- Cannot decrypt without password
|
|
453
|
+
- Cannot recover alone (need threshold + password)
|
|
431
454
|
- Can verify share validity
|
|
432
|
-
- Can
|
|
455
|
+
- Can store share safely (it's already encrypted)
|
|
433
456
|
|
|
434
457
|
Attacker:
|
|
435
458
|
- Cannot recover with < threshold shares
|
|
436
|
-
- Cannot
|
|
437
|
-
- Must compromise guardians
|
|
459
|
+
- Cannot decrypt backup without password
|
|
460
|
+
- Must compromise both guardians AND password
|
|
461
|
+
- Mathematically secure (Shamir + AES-256-GCM)
|
|
438
462
|
```
|
|
439
463
|
|
|
440
464
|
---
|
|
@@ -485,24 +509,36 @@ await w3pk.importFromSync(backupData);
|
|
|
485
509
|
### Social Recovery
|
|
486
510
|
|
|
487
511
|
```typescript
|
|
488
|
-
|
|
489
|
-
const { guardianShares } = await w3pk.setupSocialRecovery(guardians, threshold, password);
|
|
512
|
+
import { SocialRecoveryManager } from 'w3pk';
|
|
490
513
|
|
|
491
|
-
//
|
|
492
|
-
const
|
|
514
|
+
// Setup - Create password-encrypted backup and split among guardians
|
|
515
|
+
const { blob } = await w3pk.createBackupFile('password', 'MyPassword123!');
|
|
516
|
+
const backupFileJson = await blob.text();
|
|
493
517
|
|
|
494
|
-
|
|
495
|
-
const
|
|
518
|
+
const socialRecovery = new SocialRecoveryManager();
|
|
519
|
+
const guardians = await socialRecovery.setupSocialRecovery(
|
|
520
|
+
backupFileJson,
|
|
521
|
+
w3pk.user.ethereumAddress,
|
|
522
|
+
[
|
|
523
|
+
{ name: 'Alice', email: 'alice@example.com' },
|
|
524
|
+
{ name: 'Bob', email: 'bob@example.com' },
|
|
525
|
+
{ name: 'Charlie', email: 'charlie@example.com' }
|
|
526
|
+
],
|
|
527
|
+
2 // threshold
|
|
528
|
+
);
|
|
496
529
|
|
|
497
|
-
//
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
530
|
+
// Generate invitations for guardians
|
|
531
|
+
for (const guardian of guardians) {
|
|
532
|
+
const invite = await socialRecovery.generateGuardianInvite(guardian);
|
|
533
|
+
// Send invite.qrCode and invite.shareCode to guardian
|
|
534
|
+
}
|
|
502
535
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
await
|
|
536
|
+
// Recover - Collect shares and reconstruct encrypted backup file
|
|
537
|
+
const shares = [aliceShareCode, bobShareCode]; // JSON strings from guardians
|
|
538
|
+
const { backupFileJson, ethereumAddress } = await socialRecovery.recoverFromGuardians(shares);
|
|
539
|
+
|
|
540
|
+
// Decrypt and register with password
|
|
541
|
+
await w3pk.registerWithBackupFile(backupFileJson, 'MyPassword123!', 'username');
|
|
506
542
|
```
|
|
507
543
|
|
|
508
544
|
---
|
|
@@ -705,17 +741,20 @@ Platform-specific:
|
|
|
705
741
|
3. Contact your guardians (need 3 of 5)
|
|
706
742
|
4. Each guardian provides their share:
|
|
707
743
|
- Scan QR code, OR
|
|
708
|
-
-
|
|
709
|
-
5. After
|
|
710
|
-
- System reconstructs backup file
|
|
711
|
-
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
-
|
|
715
|
-
-
|
|
744
|
+
- Paste share code manually (JSON format)
|
|
745
|
+
5. After collecting threshold shares (e.g., 3):
|
|
746
|
+
- System reconstructs encrypted backup file ✅
|
|
747
|
+
- Backup file is still encrypted
|
|
748
|
+
6. Enter your password:
|
|
749
|
+
- This is the password you set during social recovery setup
|
|
750
|
+
- Guardians DO NOT have this password
|
|
751
|
+
- System decrypts backup file → extracts mnemonic
|
|
752
|
+
7. Choose username and register:
|
|
753
|
+
- System creates new passkey on your device
|
|
754
|
+
- Wallet credentials stored locally ✅
|
|
716
755
|
|
|
717
756
|
Timeline: ~24-48 hours (depends on guardian availability)
|
|
718
|
-
|
|
757
|
+
Security: Requires both guardian shares AND your password
|
|
719
758
|
```
|
|
720
759
|
|
|
721
760
|
#### **Method 4: Manual Mnemonic Import**
|
|
@@ -764,7 +803,7 @@ A: You'll need to use another recovery method (passkey sync or social recovery).
|
|
|
764
803
|
A: Mathematically secure with Shamir's Secret Sharing. Any 2 guardians cannot recover (need 3 of 5).
|
|
765
804
|
|
|
766
805
|
**Q: Can guardians steal my wallet?**
|
|
767
|
-
A: No
|
|
806
|
+
A: No. Guardians hold encrypted fragments of an already-encrypted backup file. Even if they collude and combine all shares, they still need your password to decrypt the backup file. The password is never shared with guardians.
|
|
768
807
|
|
|
769
808
|
**Q: What happens if a guardian loses their share?**
|
|
770
809
|
A: No problem! You only need 3 out of 5. As long as 3 guardians have their shares, you can recover.
|