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.
@@ -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
- ### `setupSocialRecovery(guardians: Guardian[], threshold: number): Promise<Guardian[]>`
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
- Set up social recovery with M-of-N guardian shares using Shamir Secret Sharing.
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 shares
2341
+ **Returns:** Array of guardians with encrypted share fragments
2327
2342
 
2328
- **Security:** Forces fresh authentication
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
- // Setup 3-of-5 social recovery
2334
- const guardians = await w3pk.setupSocialRecovery(
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(guardianId: string): Promise<GuardianInvite>`
2377
+ ### `SocialRecoveryManager.generateGuardianInvite(guardian: Guardian): Promise<GuardianInvite>`
2351
2378
 
2352
- Generate invitation for a guardian with their recovery share.
2379
+ Generate invitation for a guardian with their encrypted share fragment.
2353
2380
 
2354
2381
  **Parameters:**
2355
- - `guardianId: string` - Guardian ID
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; // Text code for manual entry
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 invite = await w3pk.generateGuardianInvite(guardian.id)
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
- // Show QR code to guardian
2375
- console.log('Show this QR code to guardian:', invite.qrCode)
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<RecoveryResult>`
2414
+ ### `SocialRecoveryManager.recoverFromGuardians(shares: string[]): Promise<{ backupFileJson: string; ethereumAddress: string }>`
2383
2415
 
2384
- Recover wallet from guardian shares (Shamir Secret Sharing).
2416
+ Reconstruct encrypted backup file from guardian shares.
2385
2417
 
2386
2418
  **Parameters:**
2387
- - `shares: string[]` - Array of share data from guardians (JSON strings)
2419
+ - `shares: string[]` - Array of share codes from guardians (JSON strings)
2388
2420
 
2389
2421
  **Returns:**
2390
2422
 
2391
2423
  ```typescript
2392
- interface RecoveryResult {
2393
- mnemonic: string;
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
- // Collect shares from 3 guardians
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
- aliceShare, // JSON string from Alice
2404
- bobShare, // JSON string from Bob
2405
- charlieShare // JSON string from Charlie
2406
- ]
2439
+ aliceShareCode, // JSON string from Alice
2440
+ bobShareCode, // JSON string from Bob
2441
+ charlieShareCode // JSON string from Charlie
2442
+ ];
2407
2443
 
2408
- const { mnemonic, ethereumAddress } = await w3pk.recoverFromGuardians(shares)
2409
- console.log('Wallet recovered:', ethereumAddress)
2410
- console.log('Mnemonic:', mnemonic)
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
- // Now import the mnemonic
2413
- await w3pk.importMnemonic(mnemonic)
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 { extractRS } from 'w3pk'
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
@@ -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:21-66](src/wallet/crypto.ts#L21-L66)
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
- **Process**:
155
+ **SECURE Implementation (PRF-based)**:
154
156
  ```typescript
155
157
  function deriveEncryptionKeyFromWebAuthn(
156
- credentialId: string,
157
- publicKey: string
158
+ prfOutput: ArrayBuffer, // From WebAuthn PRF extension
159
+ salt: Uint8Array // Random 32-byte salt
158
160
  ): CryptoKey {
159
- // Step 1: Combine inputs
160
- const keyMaterial = `w3pk-v4:${credentialId}:${publicKey}`;
161
+ // Step 1: Validate inputs
162
+ assert(prfOutput.byteLength === 32);
163
+ assert(salt.byteLength === 32);
161
164
 
162
- // Step 2: Hash with SHA-256
163
- const hash = SHA256(keyMaterial);
165
+ // Step 2: Import PRF output (authenticator-held secret)
166
+ const keyMaterial = importKey(prfOutput);
164
167
 
165
- // Step 3: PBKDF2 key derivation (210,000 iterations)
166
- const encryptionKey = PBKDF2(hash, salt, 210000);
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
- - **Deterministic**: Same inputs Same output
175
- - **Secure**: 210,000 PBKDF2 iterations (OWASP 2023)
176
- - **Stateless**: No key storage required
177
- - **Authentication-gated**: Requires biometric/PIN to access inputs
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
- deriveEncryptionKeyFromWebAuthn()
229
+ deriveEncryptionKeyFromWebAuthn()
221
230
  │ │
222
- Input: credentialId + publicKey
231
+ Input: PRF output (32 bytes secret)
232
+ │ + random salt (32 bytes) │
223
233
  │ ↓ │
224
- SHA-256 hash
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
- credentialId: string,
491
- publicKey: string
501
+ prfOutput: ArrayBuffer, // From WebAuthn PRF extension
502
+ salt: Uint8Array // Random 32-byte salt
492
503
  ): Promise<CryptoKey> {
493
- // 1. Combine inputs into key material
494
- const keyMaterial = `w3pk-v4:${credentialId}:${publicKey}`;
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
- keyMaterialHash,
507
+ prfOutput, // SECRET from authenticator
506
508
  { name: "PBKDF2" },
507
509
  false,
508
510
  ["deriveKey"]
509
511
  );
510
512
 
511
- // 4. Generate deterministic salt
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: new Uint8Array(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
- importedKey,
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
- - **Deterministic**: Same inputs always produce same output
535
- - **Secure**: 210,000 iterations make brute-force impractical
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 enough**: ~100-200ms on modern devices
534
+ - **Fast**: ~100-200ms on modern devices
538
535
 
539
536
  ---
540
537