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/API_REFERENCE.md
CHANGED
|
@@ -20,6 +20,7 @@ Complete reference for all methods, types, and utilities in the w3pk SDK.
|
|
|
20
20
|
- [Standalone Utilities](#standalone-utilities)
|
|
21
21
|
- [Validation Utilities](#validation-utilities)
|
|
22
22
|
- [Build Verification Utilities](#build-verification-utilities)
|
|
23
|
+
- [Post-Quantum Cryptography](#post-quantum-cryptography)
|
|
23
24
|
- [Wallet Generation Utilities](#wallet-generation-utilities)
|
|
24
25
|
- [Security Inspection](#security-inspection)
|
|
25
26
|
- [Browser Inspection](#browser-inspection)
|
|
@@ -1613,6 +1614,8 @@ const w3pk = createWeb3Passkey({
|
|
|
1613
1614
|
|
|
1614
1615
|
All stealth address methods are accessed via `w3pk.stealth.*`
|
|
1615
1616
|
|
|
1617
|
+
> **🔒 Security Note:** The ERC-5564 implementation properly reduces all scalars modulo the secp256k1 curve order to prevent unspendable funds. The same reduced scalar is used consistently in both public-key operations (`s_h × G`) and private-key operations (`spendingKey + s_h`) to ensure the derived stealth address always matches the derived private key.
|
|
1618
|
+
|
|
1616
1619
|
### `stealth.generateStealthAddress(options?: { requireAuth?: boolean }): Promise<StealthAddressResult>`
|
|
1617
1620
|
|
|
1618
1621
|
Generate a fresh ERC-5564 compliant stealth address for a recipient.
|
|
@@ -2308,11 +2311,23 @@ const hybridBackup = await w3pk.createBackupFile('hybrid', 'MySecurePassword123!
|
|
|
2308
2311
|
|
|
2309
2312
|
---
|
|
2310
2313
|
|
|
2311
|
-
|
|
2314
|
+
## Social Recovery API
|
|
2315
|
+
|
|
2316
|
+
Social recovery is handled by the `SocialRecoveryManager` class, which splits password-encrypted backup files into guardian shares using Shamir Secret Sharing.
|
|
2317
|
+
|
|
2318
|
+
### Import
|
|
2319
|
+
|
|
2320
|
+
```typescript
|
|
2321
|
+
import { SocialRecoveryManager } from 'w3pk';
|
|
2322
|
+
```
|
|
2312
2323
|
|
|
2313
|
-
|
|
2324
|
+
### `SocialRecoveryManager.setupSocialRecovery(backupFileJson: string, ethereumAddress: string, guardians: Guardian[], threshold: number): Promise<Guardian[]>`
|
|
2325
|
+
|
|
2326
|
+
Set up social recovery by splitting an encrypted backup file among guardians.
|
|
2314
2327
|
|
|
2315
2328
|
**Parameters:**
|
|
2329
|
+
- `backupFileJson: string` - Password-encrypted backup file JSON
|
|
2330
|
+
- `ethereumAddress: string` - Ethereum address for verification
|
|
2316
2331
|
- `guardians: Guardian[]`
|
|
2317
2332
|
```typescript
|
|
2318
2333
|
interface Guardian {
|
|
@@ -2323,15 +2338,27 @@ Set up social recovery with M-of-N guardian shares using Shamir Secret Sharing.
|
|
|
2323
2338
|
```
|
|
2324
2339
|
- `threshold: number` - Number of guardians required to recover (M in M-of-N)
|
|
2325
2340
|
|
|
2326
|
-
**Returns:** Array of guardians with encrypted
|
|
2341
|
+
**Returns:** Array of guardians with encrypted share fragments
|
|
2327
2342
|
|
|
2328
|
-
**Security:**
|
|
2343
|
+
**Security:**
|
|
2344
|
+
- Guardians receive fragments of an already-encrypted backup file
|
|
2345
|
+
- Password is never shared with guardians
|
|
2346
|
+
- Requires both threshold shares + password to recover
|
|
2329
2347
|
|
|
2330
2348
|
**Example:**
|
|
2331
2349
|
|
|
2332
2350
|
```typescript
|
|
2333
|
-
|
|
2334
|
-
|
|
2351
|
+
import { SocialRecoveryManager } from 'w3pk';
|
|
2352
|
+
|
|
2353
|
+
// Step 1: Create password-encrypted backup file
|
|
2354
|
+
const { blob } = await w3pk.createBackupFile('password', 'MySecurePassword123!');
|
|
2355
|
+
const backupFileJson = await blob.text();
|
|
2356
|
+
|
|
2357
|
+
// Step 2: Setup social recovery with 3-of-5 threshold
|
|
2358
|
+
const socialRecovery = new SocialRecoveryManager();
|
|
2359
|
+
const guardians = await socialRecovery.setupSocialRecovery(
|
|
2360
|
+
backupFileJson,
|
|
2361
|
+
w3pk.user.ethereumAddress,
|
|
2335
2362
|
[
|
|
2336
2363
|
{ name: 'Alice', email: 'alice@example.com' },
|
|
2337
2364
|
{ name: 'Bob', phone: '+1234567890' },
|
|
@@ -2340,19 +2367,19 @@ const guardians = await w3pk.setupSocialRecovery(
|
|
|
2340
2367
|
{ name: 'Eve', email: 'eve@example.com' }
|
|
2341
2368
|
],
|
|
2342
2369
|
3 // Need 3 guardians to recover
|
|
2343
|
-
)
|
|
2370
|
+
);
|
|
2344
2371
|
|
|
2345
|
-
console.log('Social recovery configured with', guardians.length, 'guardians')
|
|
2372
|
+
console.log('Social recovery configured with', guardians.length, 'guardians');
|
|
2346
2373
|
```
|
|
2347
2374
|
|
|
2348
2375
|
---
|
|
2349
2376
|
|
|
2350
|
-
### `generateGuardianInvite(
|
|
2377
|
+
### `SocialRecoveryManager.generateGuardianInvite(guardian: Guardian): Promise<GuardianInvite>`
|
|
2351
2378
|
|
|
2352
|
-
Generate invitation for a guardian with their
|
|
2379
|
+
Generate invitation for a guardian with their encrypted share fragment.
|
|
2353
2380
|
|
|
2354
2381
|
**Parameters:**
|
|
2355
|
-
- `
|
|
2382
|
+
- `guardian: Guardian` - Guardian object with share
|
|
2356
2383
|
|
|
2357
2384
|
**Returns:**
|
|
2358
2385
|
|
|
@@ -2360,57 +2387,89 @@ Generate invitation for a guardian with their recovery share.
|
|
|
2360
2387
|
interface GuardianInvite {
|
|
2361
2388
|
guardianId: string;
|
|
2362
2389
|
qrCode: string; // Data URL for QR code
|
|
2363
|
-
shareCode: string; //
|
|
2390
|
+
shareCode: string; // JSON string for manual entry
|
|
2364
2391
|
explainer: string; // Educational text for guardian
|
|
2365
|
-
link?: string; // Optional deep link
|
|
2366
2392
|
}
|
|
2367
2393
|
```
|
|
2368
2394
|
|
|
2369
2395
|
**Example:**
|
|
2370
2396
|
|
|
2371
2397
|
```typescript
|
|
2372
|
-
const
|
|
2398
|
+
const socialRecovery = new SocialRecoveryManager();
|
|
2399
|
+
|
|
2400
|
+
for (const guardian of guardians) {
|
|
2401
|
+
const invite = await socialRecovery.generateGuardianInvite(guardian);
|
|
2402
|
+
|
|
2403
|
+
// Show QR code to guardian
|
|
2404
|
+
console.log('QR code:', invite.qrCode);
|
|
2405
|
+
console.log('Share code:', invite.shareCode);
|
|
2406
|
+
console.log('Instructions:', invite.explainer);
|
|
2373
2407
|
|
|
2374
|
-
//
|
|
2375
|
-
|
|
2376
|
-
console.log('Or share this code:', invite.shareCode)
|
|
2377
|
-
console.log('Instructions:', invite.explainer)
|
|
2408
|
+
// Send via email, messaging app, etc.
|
|
2409
|
+
}
|
|
2378
2410
|
```
|
|
2379
2411
|
|
|
2380
2412
|
---
|
|
2381
2413
|
|
|
2382
|
-
### `recoverFromGuardians(shares: string[]): Promise<
|
|
2414
|
+
### `SocialRecoveryManager.recoverFromGuardians(shares: string[]): Promise<{ backupFileJson: string; ethereumAddress: string }>`
|
|
2383
2415
|
|
|
2384
|
-
|
|
2416
|
+
Reconstruct encrypted backup file from guardian shares.
|
|
2385
2417
|
|
|
2386
2418
|
**Parameters:**
|
|
2387
|
-
- `shares: string[]` - Array of share
|
|
2419
|
+
- `shares: string[]` - Array of share codes from guardians (JSON strings)
|
|
2388
2420
|
|
|
2389
2421
|
**Returns:**
|
|
2390
2422
|
|
|
2391
2423
|
```typescript
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
ethereumAddress: string;
|
|
2424
|
+
{
|
|
2425
|
+
backupFileJson: string; // Reconstructed encrypted backup file
|
|
2426
|
+
ethereumAddress: string; // Ethereum address from backup
|
|
2395
2427
|
}
|
|
2396
2428
|
```
|
|
2397
2429
|
|
|
2398
2430
|
**Example:**
|
|
2399
2431
|
|
|
2400
2432
|
```typescript
|
|
2401
|
-
|
|
2433
|
+
import { SocialRecoveryManager } from 'w3pk';
|
|
2434
|
+
|
|
2435
|
+
const socialRecovery = new SocialRecoveryManager();
|
|
2436
|
+
|
|
2437
|
+
// Step 1: Collect shares from 3+ guardians
|
|
2402
2438
|
const shares = [
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
]
|
|
2439
|
+
aliceShareCode, // JSON string from Alice
|
|
2440
|
+
bobShareCode, // JSON string from Bob
|
|
2441
|
+
charlieShareCode // JSON string from Charlie
|
|
2442
|
+
];
|
|
2407
2443
|
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
console.log('
|
|
2444
|
+
// Step 2: Reconstruct encrypted backup file
|
|
2445
|
+
const { backupFileJson, ethereumAddress } = await socialRecovery.recoverFromGuardians(shares);
|
|
2446
|
+
console.log('Backup file reconstructed for:', ethereumAddress);
|
|
2411
2447
|
|
|
2412
|
-
//
|
|
2413
|
-
|
|
2448
|
+
// Step 3: Decrypt with password and register
|
|
2449
|
+
const password = 'MySecurePassword123!'; // Password set during setup
|
|
2450
|
+
await w3pk.registerWithBackupFile(backupFileJson, password, 'username');
|
|
2451
|
+
console.log('Wallet recovered and registered!');
|
|
2452
|
+
```
|
|
2453
|
+
|
|
2454
|
+
---
|
|
2455
|
+
|
|
2456
|
+
### `SocialRecoveryManager.getSocialRecoveryConfig(): SocialRecoveryConfig | null`
|
|
2457
|
+
|
|
2458
|
+
Get current social recovery configuration from localStorage.
|
|
2459
|
+
|
|
2460
|
+
**Returns:** Social recovery configuration or null if not set up
|
|
2461
|
+
|
|
2462
|
+
**Example:**
|
|
2463
|
+
|
|
2464
|
+
```typescript
|
|
2465
|
+
const socialRecovery = new SocialRecoveryManager();
|
|
2466
|
+
const config = socialRecovery.getSocialRecoveryConfig();
|
|
2467
|
+
|
|
2468
|
+
if (config) {
|
|
2469
|
+
console.log('Threshold:', config.threshold);
|
|
2470
|
+
console.log('Total guardians:', config.totalGuardians);
|
|
2471
|
+
console.log('Guardians:', config.guardians);
|
|
2472
|
+
}
|
|
2414
2473
|
```
|
|
2415
2474
|
|
|
2416
2475
|
---
|
|
@@ -3151,7 +3210,89 @@ console.log(encoded) // => SGVsbG8gV29ybGQ=
|
|
|
3151
3210
|
### Cryptographic Utilities
|
|
3152
3211
|
|
|
3153
3212
|
```typescript
|
|
3154
|
-
import {
|
|
3213
|
+
import {
|
|
3214
|
+
extractRS,
|
|
3215
|
+
deriveEncryptionKeyFromWebAuthn,
|
|
3216
|
+
generateSalt
|
|
3217
|
+
} from 'w3pk'
|
|
3218
|
+
```
|
|
3219
|
+
|
|
3220
|
+
#### `deriveEncryptionKeyFromWebAuthn(prfOutput: ArrayBuffer, salt: Uint8Array): Promise<CryptoKey>`
|
|
3221
|
+
|
|
3222
|
+
**🔐 SECURE - WebAuthn PRF-based encryption**
|
|
3223
|
+
|
|
3224
|
+
Derives an AES-256-GCM encryption key from WebAuthn PRF (Pseudo-Random Function) extension output.
|
|
3225
|
+
|
|
3226
|
+
**Security:**
|
|
3227
|
+
- Uses authenticator-held secrets via PRF extension (never exposed)
|
|
3228
|
+
- Implements random unique salts (no precomputation attacks)
|
|
3229
|
+
- PBKDF2 with 210,000 iterations (OWASP 2023 recommendation)
|
|
3230
|
+
- Prevents offline decryption attacks
|
|
3231
|
+
|
|
3232
|
+
**Parameters:**
|
|
3233
|
+
- `prfOutput: ArrayBuffer` - 32-byte secret from WebAuthn PRF extension
|
|
3234
|
+
- `salt: Uint8Array` - 32-byte random salt (generate with `generateSalt()`)
|
|
3235
|
+
|
|
3236
|
+
**Returns:** `Promise<CryptoKey>` - AES-256-GCM key for encryption/decryption
|
|
3237
|
+
|
|
3238
|
+
**Example:**
|
|
3239
|
+
```typescript
|
|
3240
|
+
// 1. Enable PRF during WebAuthn registration
|
|
3241
|
+
const credential = await navigator.credentials.create({
|
|
3242
|
+
publicKey: {
|
|
3243
|
+
challenge: new Uint8Array(32),
|
|
3244
|
+
rp: { name: "Example" },
|
|
3245
|
+
user: { id: userId, name: "user", displayName: "User" },
|
|
3246
|
+
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
|
|
3247
|
+
extensions: {
|
|
3248
|
+
prf: {} // Enable PRF extension
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
|
|
3253
|
+
// 2. Get PRF output during authentication
|
|
3254
|
+
const assertion = await navigator.credentials.get({
|
|
3255
|
+
publicKey: {
|
|
3256
|
+
challenge: new Uint8Array(32),
|
|
3257
|
+
extensions: {
|
|
3258
|
+
prf: {
|
|
3259
|
+
eval: {
|
|
3260
|
+
first: new Uint8Array(32) // Salt for PRF
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
});
|
|
3266
|
+
|
|
3267
|
+
const prfOutput = assertion.getClientExtensionResults().prf.results.first;
|
|
3268
|
+
|
|
3269
|
+
// 3. Generate and store salt
|
|
3270
|
+
const salt = generateSalt(); // Store this with your ciphertext
|
|
3271
|
+
|
|
3272
|
+
// 4. Derive encryption key
|
|
3273
|
+
const encryptionKey = await deriveEncryptionKeyFromWebAuthn(
|
|
3274
|
+
prfOutput,
|
|
3275
|
+
salt
|
|
3276
|
+
);
|
|
3277
|
+
|
|
3278
|
+
// 5. Encrypt sensitive data
|
|
3279
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
3280
|
+
{ name: "AES-GCM", iv: crypto.getRandomValues(new Uint8Array(12)) },
|
|
3281
|
+
encryptionKey,
|
|
3282
|
+
new TextEncoder().encode("secret data")
|
|
3283
|
+
);
|
|
3284
|
+
```
|
|
3285
|
+
|
|
3286
|
+
#### `generateSalt(): Uint8Array`
|
|
3287
|
+
|
|
3288
|
+
Generates a cryptographically secure random 32-byte salt for encryption.
|
|
3289
|
+
|
|
3290
|
+
**Returns:** `Uint8Array` - 32 random bytes
|
|
3291
|
+
|
|
3292
|
+
**Example:**
|
|
3293
|
+
```typescript
|
|
3294
|
+
const salt = generateSalt();
|
|
3295
|
+
// Store salt alongside ciphertext for later decryption
|
|
3155
3296
|
```
|
|
3156
3297
|
|
|
3157
3298
|
#### `extractRS(derSignature: Uint8Array): { r: string; s: string }`
|
|
@@ -3209,6 +3350,189 @@ This ensures compatibility with Ethereum's signature malleability protection.
|
|
|
3209
3350
|
|
|
3210
3351
|
---
|
|
3211
3352
|
|
|
3353
|
+
### Post-Quantum Cryptography
|
|
3354
|
+
|
|
3355
|
+
w3pk provides ML-KEM-1024 encryption both as instance methods (recommended) and standalone functions.
|
|
3356
|
+
|
|
3357
|
+
#### Instance Methods (Recommended)
|
|
3358
|
+
|
|
3359
|
+
Use these methods with your w3pk wallet instance - no private key exposure:
|
|
3360
|
+
|
|
3361
|
+
##### `w3pk.deriveMLKemPublicKey(options?): Promise<string>`
|
|
3362
|
+
|
|
3363
|
+
Derive your ML-KEM-1024 public key for sharing with others.
|
|
3364
|
+
|
|
3365
|
+
**Parameters:**
|
|
3366
|
+
```typescript
|
|
3367
|
+
{
|
|
3368
|
+
context?: string; // Domain separation (default: 'mlkem-v1')
|
|
3369
|
+
mode?: SecurityMode; // 'STANDARD', 'STRICT', or 'YOLO' (default: 'STANDARD')
|
|
3370
|
+
tag?: string; // Address derivation tag (default: 'MAIN')
|
|
3371
|
+
origin?: string; // Origin (default: current origin)
|
|
3372
|
+
requireAuth?: boolean; // Force re-auth (default: false, always true in STRICT)
|
|
3373
|
+
}
|
|
3374
|
+
```
|
|
3375
|
+
|
|
3376
|
+
**Returns:** Base64-encoded public key (1568 bytes)
|
|
3377
|
+
|
|
3378
|
+
**Example:**
|
|
3379
|
+
```typescript
|
|
3380
|
+
const w3pk = createWeb3Passkey();
|
|
3381
|
+
await w3pk.login();
|
|
3382
|
+
|
|
3383
|
+
const myPubKey = await w3pk.deriveMLKemPublicKey();
|
|
3384
|
+
// Share myPubKey with others
|
|
3385
|
+
```
|
|
3386
|
+
|
|
3387
|
+
**Note:** PRIMARY mode not supported (uses P-256 WebAuthn keys).
|
|
3388
|
+
|
|
3389
|
+
---
|
|
3390
|
+
|
|
3391
|
+
##### `w3pk.mlkemEncrypt(plaintext: string, recipientPublicKeys: Array<string | Uint8Array>, options?): Promise<EncryptedPayload>`
|
|
3392
|
+
|
|
3393
|
+
Encrypt data for yourself and additional recipients.
|
|
3394
|
+
|
|
3395
|
+
**Parameters:**
|
|
3396
|
+
```typescript
|
|
3397
|
+
{
|
|
3398
|
+
context?: string; // Domain separation (default: 'mlkem-v1')
|
|
3399
|
+
mode?: SecurityMode; // Security mode (default: 'STANDARD')
|
|
3400
|
+
tag?: string; // Tag (default: 'MAIN')
|
|
3401
|
+
origin?: string; // Origin (default: current)
|
|
3402
|
+
requireAuth?: boolean; // Force re-auth (default: false)
|
|
3403
|
+
}
|
|
3404
|
+
```
|
|
3405
|
+
|
|
3406
|
+
**Example:**
|
|
3407
|
+
```typescript
|
|
3408
|
+
const serverPubKey = await fetch('/api/mlkem-key').then(r => r.text());
|
|
3409
|
+
|
|
3410
|
+
const encrypted = await w3pk.mlkemEncrypt(
|
|
3411
|
+
'my secret data',
|
|
3412
|
+
[serverPubKey] // You + server can both decrypt
|
|
3413
|
+
);
|
|
3414
|
+
```
|
|
3415
|
+
|
|
3416
|
+
---
|
|
3417
|
+
|
|
3418
|
+
##### `w3pk.mlkemDecrypt(payload: EncryptedPayload, options?): Promise<string>`
|
|
3419
|
+
|
|
3420
|
+
Decrypt data encrypted for your wallet.
|
|
3421
|
+
|
|
3422
|
+
**Parameters:** Same options as `mlkemEncrypt`
|
|
3423
|
+
|
|
3424
|
+
**Example:**
|
|
3425
|
+
```typescript
|
|
3426
|
+
const plaintext = await w3pk.mlkemDecrypt(encrypted);
|
|
3427
|
+
```
|
|
3428
|
+
|
|
3429
|
+
---
|
|
3430
|
+
|
|
3431
|
+
#### Standalone Functions
|
|
3432
|
+
|
|
3433
|
+
Low-level functions for direct key management:
|
|
3434
|
+
|
|
3435
|
+
```typescript
|
|
3436
|
+
import { mlkemEncrypt, mlkemDecrypt, deriveMLKemKeypair, type EncryptedPayload } from 'w3pk'
|
|
3437
|
+
```
|
|
3438
|
+
|
|
3439
|
+
##### `deriveMLKemKeypair(privateKey: string | Uint8Array, context?: string): Promise<MLKemKeypair>`
|
|
3440
|
+
|
|
3441
|
+
Derive deterministic ML-KEM keypair from any private key using HKDF-SHA256.
|
|
3442
|
+
|
|
3443
|
+
**Parameters:**
|
|
3444
|
+
- `privateKey` - Ethereum private key (hex with optional `0x` prefix or Uint8Array)
|
|
3445
|
+
- `context` - Context string for domain separation (default: `'mlkem-v1'`)
|
|
3446
|
+
|
|
3447
|
+
**Returns:**
|
|
3448
|
+
```typescript
|
|
3449
|
+
{
|
|
3450
|
+
publicKey: Uint8Array; // 1568 bytes
|
|
3451
|
+
privateKey: Uint8Array; // 3168 bytes
|
|
3452
|
+
}
|
|
3453
|
+
```
|
|
3454
|
+
|
|
3455
|
+
**Example:**
|
|
3456
|
+
```typescript
|
|
3457
|
+
const keypair = await deriveMLKemKeypair('0x1234...', 'my-app');
|
|
3458
|
+
```
|
|
3459
|
+
|
|
3460
|
+
---
|
|
3461
|
+
|
|
3462
|
+
##### `mlkemEncrypt(plaintext: string, publicKeys: (string | Uint8Array) | Array<string | Uint8Array>): Promise<EncryptedPayload>`
|
|
3463
|
+
|
|
3464
|
+
Encrypt data using ML-KEM-1024 (post-quantum KEM) + AES-256-GCM for one or more recipients.
|
|
3465
|
+
|
|
3466
|
+
**Parameters:**
|
|
3467
|
+
- `plaintext: string` - The data to encrypt
|
|
3468
|
+
- `publicKeys: (string | Uint8Array) | Array<string | Uint8Array>` - Single public key or array of ML-KEM-1024 public keys (1568 bytes each)
|
|
3469
|
+
|
|
3470
|
+
**Returns:**
|
|
3471
|
+
```typescript
|
|
3472
|
+
{
|
|
3473
|
+
recipients: Array<{
|
|
3474
|
+
publicKey: string; // Base64 recipient public key (1568 bytes)
|
|
3475
|
+
ciphertext: string; // Base64 ML-KEM ciphertext for this recipient (1600 bytes)
|
|
3476
|
+
}>;
|
|
3477
|
+
encryptedData: string; // Base64 AES-encrypted data (shared across all recipients)
|
|
3478
|
+
iv: string; // Base64 initialization vector (12 bytes)
|
|
3479
|
+
authTag: string; // Base64 authentication tag (16 bytes)
|
|
3480
|
+
}
|
|
3481
|
+
```
|
|
3482
|
+
|
|
3483
|
+
**Example:**
|
|
3484
|
+
|
|
3485
|
+
```typescript
|
|
3486
|
+
import { mlkemEncrypt } from 'w3pk'
|
|
3487
|
+
|
|
3488
|
+
// Single recipient
|
|
3489
|
+
const encrypted = await mlkemEncrypt('my secret data', publicKey1)
|
|
3490
|
+
|
|
3491
|
+
// Multiple recipients
|
|
3492
|
+
const encrypted = await mlkemEncrypt('my secret data', [publicKey1, publicKey2, publicKey3])
|
|
3493
|
+
|
|
3494
|
+
console.log('Recipients:', encrypted.recipients.length)
|
|
3495
|
+
```
|
|
3496
|
+
|
|
3497
|
+
**Security:**
|
|
3498
|
+
- ✅ **Quantum-resistant** - ML-KEM-1024 (NIST FIPS 203)
|
|
3499
|
+
- ✅ **Multi-recipient support** - Encrypt once for multiple recipients
|
|
3500
|
+
- ✅ **Key zeroization** - All secrets securely wiped from memory
|
|
3501
|
+
- ✅ **Authenticated encryption** - AES-256-GCM with 128-bit auth tag
|
|
3502
|
+
|
|
3503
|
+
---
|
|
3504
|
+
|
|
3505
|
+
#### `mlkemDecrypt(payload: EncryptedPayload, privateKey: string | Uint8Array, publicKey?: string | Uint8Array): Promise<string>`
|
|
3506
|
+
|
|
3507
|
+
Decrypt data encrypted with `mlkemEncrypt()`.
|
|
3508
|
+
|
|
3509
|
+
**Parameters:**
|
|
3510
|
+
- `payload: EncryptedPayload` - The encrypted payload from `mlkemEncrypt()`
|
|
3511
|
+
- `privateKey: string | Uint8Array` - ML-KEM-1024 private key (3168 bytes)
|
|
3512
|
+
- `publicKey?: string | Uint8Array` - (Optional) Your public key for faster recipient lookup (1568 bytes)
|
|
3513
|
+
|
|
3514
|
+
**Returns:** `string` - Decrypted plaintext
|
|
3515
|
+
|
|
3516
|
+
**Example:**
|
|
3517
|
+
|
|
3518
|
+
```typescript
|
|
3519
|
+
import { mlkemDecrypt } from 'w3pk'
|
|
3520
|
+
|
|
3521
|
+
// Auto-detect which recipient you are
|
|
3522
|
+
const plaintext = await mlkemDecrypt(encrypted, privateKey)
|
|
3523
|
+
|
|
3524
|
+
// Or specify your public key for faster lookup
|
|
3525
|
+
const plaintext = await mlkemDecrypt(encrypted, privateKey, myPublicKey)
|
|
3526
|
+
```
|
|
3527
|
+
|
|
3528
|
+
**Throws:** Error if decryption fails (invalid key, no matching recipient, corrupted data, or tampered auth tag)
|
|
3529
|
+
|
|
3530
|
+
**Related:**
|
|
3531
|
+
- See [POST_QUANTUM.md](../docs/POST_QUANTUM.md) for full quantum readiness roadmap
|
|
3532
|
+
- See [NIST FIPS 203](https://csrc.nist.gov/pubs/fips/203/final) for ML-KEM specification
|
|
3533
|
+
|
|
3534
|
+
---
|
|
3535
|
+
|
|
3212
3536
|
### Wallet Generation Utilities
|
|
3213
3537
|
|
|
3214
3538
|
```typescript
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -148,33 +148,42 @@ m/44'/60'/0'/0/2 ← Third address
|
|
|
148
148
|
|
|
149
149
|
**Purpose**: Link WebAuthn authentication to wallet encryption
|
|
150
150
|
|
|
151
|
-
**Implementation**: [src/wallet/crypto.ts
|
|
151
|
+
**Implementation**: [src/wallet/crypto.ts](src/wallet/crypto.ts)
|
|
152
|
+
|
|
153
|
+
✅ **SECURITY**: Uses PRF-based encryption with authenticator-held secrets and random salts
|
|
152
154
|
|
|
153
|
-
**
|
|
155
|
+
**SECURE Implementation (PRF-based)**:
|
|
154
156
|
```typescript
|
|
155
157
|
function deriveEncryptionKeyFromWebAuthn(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
prfOutput: ArrayBuffer, // From WebAuthn PRF extension
|
|
159
|
+
salt: Uint8Array // Random 32-byte salt
|
|
158
160
|
): CryptoKey {
|
|
159
|
-
// Step 1:
|
|
160
|
-
|
|
161
|
+
// Step 1: Validate inputs
|
|
162
|
+
assert(prfOutput.byteLength === 32);
|
|
163
|
+
assert(salt.byteLength === 32);
|
|
161
164
|
|
|
162
|
-
// Step 2:
|
|
163
|
-
const
|
|
165
|
+
// Step 2: Import PRF output (authenticator-held secret)
|
|
166
|
+
const keyMaterial = importKey(prfOutput);
|
|
164
167
|
|
|
165
|
-
// Step 3: PBKDF2 key derivation
|
|
166
|
-
const encryptionKey = PBKDF2(
|
|
168
|
+
// Step 3: PBKDF2 key derivation with random salt
|
|
169
|
+
const encryptionKey = PBKDF2(keyMaterial, salt, 210000);
|
|
167
170
|
|
|
168
171
|
// Result: AES-256-GCM key (NEVER STORED)
|
|
169
172
|
return encryptionKey;
|
|
170
173
|
}
|
|
171
174
|
```
|
|
172
175
|
|
|
173
|
-
**Properties**:
|
|
174
|
-
- **
|
|
175
|
-
- **
|
|
176
|
-
- **
|
|
177
|
-
- **
|
|
176
|
+
**Security Properties**:
|
|
177
|
+
- **Authenticator Secret**: Uses PRF output from hardware (never exposed to application)
|
|
178
|
+
- **Random Salts**: Each encryption uses unique 32-byte salt (prevents precomputation)
|
|
179
|
+
- **PBKDF2**: 210,000 iterations (OWASP 2023 recommendation)
|
|
180
|
+
- **AES-256-GCM**: Strong authenticated encryption
|
|
181
|
+
- **Authentication-gated**: Requires biometric/PIN to trigger PRF
|
|
182
|
+
|
|
183
|
+
**Implementation Note**:
|
|
184
|
+
The SDK uses `deriveEncryptionKeyAuto()` as a helper that:
|
|
185
|
+
1. Uses PRF-based encryption when available (secure)
|
|
186
|
+
2. Falls back to v2 implementation for existing wallets without PRF support
|
|
178
187
|
|
|
179
188
|
---
|
|
180
189
|
|
|
@@ -217,11 +226,12 @@ STEP 2: Create WebAuthn Credential (INDEPENDENT)
|
|
|
217
226
|
|
|
218
227
|
STEP 3: Derive Encryption Key
|
|
219
228
|
┌──────────────────────────────────────┐
|
|
220
|
-
│
|
|
229
|
+
│ deriveEncryptionKeyFromWebAuthn() │
|
|
221
230
|
│ │
|
|
222
|
-
│
|
|
231
|
+
│ Input: PRF output (32 bytes secret) │
|
|
232
|
+
│ + random salt (32 bytes) │
|
|
223
233
|
│ ↓ │
|
|
224
|
-
│
|
|
234
|
+
│ Import PRF as key material │
|
|
225
235
|
│ ↓ │
|
|
226
236
|
│ PBKDF2 (210,000 iterations) │
|
|
227
237
|
│ ↓ │
|
|
@@ -486,43 +496,29 @@ const signedTx = await wallet.signTransaction(tx);
|
|
|
486
496
|
**Implementation**: [src/wallet/crypto.ts:21-66](src/wallet/crypto.ts#L21-L66)
|
|
487
497
|
|
|
488
498
|
```javascript
|
|
499
|
+
// SECURE: PRF-based key derivation
|
|
489
500
|
async function deriveEncryptionKeyFromWebAuthn(
|
|
490
|
-
|
|
491
|
-
|
|
501
|
+
prfOutput: ArrayBuffer, // From WebAuthn PRF extension
|
|
502
|
+
salt: Uint8Array // Random 32-byte salt
|
|
492
503
|
): Promise<CryptoKey> {
|
|
493
|
-
// 1.
|
|
494
|
-
const keyMaterial =
|
|
495
|
-
|
|
496
|
-
// 2. SHA-256 hash
|
|
497
|
-
const keyMaterialHash = await crypto.subtle.digest(
|
|
498
|
-
"SHA-256",
|
|
499
|
-
new TextEncoder().encode(keyMaterial)
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
// 3. Import as PBKDF2 base key
|
|
503
|
-
const importedKey = await crypto.subtle.importKey(
|
|
504
|
+
// 1. Import PRF output as key material (authenticator secret)
|
|
505
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
504
506
|
"raw",
|
|
505
|
-
|
|
507
|
+
prfOutput, // SECRET from authenticator
|
|
506
508
|
{ name: "PBKDF2" },
|
|
507
509
|
false,
|
|
508
510
|
["deriveKey"]
|
|
509
511
|
);
|
|
510
512
|
|
|
511
|
-
//
|
|
512
|
-
const salt = await crypto.subtle.digest(
|
|
513
|
-
"SHA-256",
|
|
514
|
-
new TextEncoder().encode("w3pk-salt-v4")
|
|
515
|
-
);
|
|
516
|
-
|
|
517
|
-
// 5. Derive AES-256-GCM key
|
|
513
|
+
// 2. Derive AES-256-GCM key with PBKDF2
|
|
518
514
|
return crypto.subtle.deriveKey(
|
|
519
515
|
{
|
|
520
516
|
name: "PBKDF2",
|
|
521
|
-
salt:
|
|
517
|
+
salt: salt, // Random 32-byte salt (stored with ciphertext)
|
|
522
518
|
iterations: 210000, // OWASP 2023 recommendation
|
|
523
519
|
hash: "SHA-256",
|
|
524
520
|
},
|
|
525
|
-
|
|
521
|
+
keyMaterial, // PRF output from authenticator
|
|
526
522
|
{ name: "AES-GCM", length: 256 },
|
|
527
523
|
false,
|
|
528
524
|
["encrypt", "decrypt"]
|
|
@@ -530,11 +526,12 @@ async function deriveEncryptionKeyFromWebAuthn(
|
|
|
530
526
|
}
|
|
531
527
|
```
|
|
532
528
|
|
|
533
|
-
**Properties**:
|
|
534
|
-
- **
|
|
535
|
-
- **
|
|
529
|
+
**Security Properties**:
|
|
530
|
+
- **Authenticator-Bound**: PRF output is hardware-held secret (never exposed)
|
|
531
|
+
- **Unique Salts**: Each encryption uses random salt (no precomputation)
|
|
532
|
+
- **Strong KDF**: 210,000 PBKDF2 iterations make brute-force impractical (OWASP 2023)
|
|
536
533
|
- **Stateless**: No need to store the encryption key
|
|
537
|
-
- **Fast
|
|
534
|
+
- **Fast**: ~100-200ms on modern devices
|
|
538
535
|
|
|
539
536
|
---
|
|
540
537
|
|