w3pk 0.10.1 → 0.10.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.
@@ -57,6 +57,8 @@ interface PersistentSessionConfig {
57
57
  }
58
58
  ```
59
59
 
60
+ **PRF requirement:** persistent sessions are encrypted under a key derived from the WebAuthn PRF extension, which the authenticator only releases during a user-verified assertion. On authenticators without PRF support, no persistent session is stored — the device falls back to in-memory sessions only (there is no weaker at-rest fallback). The session is re-keyed at every real (prompted) login; the configured `duration` is therefore also the renewal interval. With `requireReauth: true` the decryption key is never written to disk; with `requireReauth: false` it is stored as a non-extractable `CryptoKey` to allow silent restore. See [SECURITY.md](./SECURITY.md#at-rest-encryption-what-it-protects-and-what-it-doesnt) for the full model.
61
+
60
62
  **Example:**
61
63
 
62
64
  ```typescript
@@ -2847,13 +2849,12 @@ Enable persistent sessions to maintain user login across page refreshes.
2847
2849
  4. `requireReauth: true` prompts for biometric on refresh (more secure)
2848
2850
  5. `requireReauth: false` silently restores session (more convenient)
2849
2851
 
2850
- **Security:**
2851
- - Sessions only persist for STANDARD and YOLO modes
2852
- - STRICT mode sessions are NEVER persisted
2853
- - Encrypted at rest with WebAuthn-derived key
2854
- - Requires valid WebAuthn credential to decrypt
2855
- - Time-limited expiration
2856
- - Origin-isolated via IndexedDB
2852
+ **Security (read the trade-off):**
2853
+ - Sessions only persist for STANDARD and YOLO modes; STRICT mode is NEVER persisted
2854
+ - Time-limited expiration; origin-isolated via IndexedDB
2855
+ - ⚠️ The "WebAuthn-derived key" is derived from **public** credential metadata stored in the same browser profile, **not** from an authenticator-held secret. A persistent session is therefore **decryptable by anyone who can read this origin's storage** (malicious extension, XSS exfil, disk image), and it keeps the seed in that state for the whole duration it's enabled. This is a UX-for-security trade-off, not hardware-backed protection.
2856
+ - Enabling "Remember Me" does not add a new trust dependency (you already trust your origin's code), it **widens the window** in which a compromise of your origin can reach the seed. Keep `duration` short for higher-value wallets; don't enable on shared/untrusted devices.
2857
+ - Full threat model: **[SECURITY.md → At-Rest Encryption](./SECURITY.md#at-rest-encryption-what-it-protects-and-what-it-doesnt)**.
2857
2858
 
2858
2859
  **Example:**
2859
2860
 
@@ -3210,89 +3211,7 @@ console.log(encoded) // => SGVsbG8gV29ybGQ=
3210
3211
  ### Cryptographic Utilities
3211
3212
 
3212
3213
  ```typescript
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
3214
+ import { extractRS } from 'w3pk'
3296
3215
  ```
3297
3216
 
3298
3217
  #### `extractRS(derSignature: Uint8Array): { r: string; s: string }`
@@ -148,42 +148,33 @@ 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](src/wallet/crypto.ts)
152
-
153
- ✅ **SECURITY**: Uses PRF-based encryption with authenticator-held secrets and random salts
151
+ **Implementation**: [src/wallet/crypto.ts:21-66](src/wallet/crypto.ts#L21-L66)
154
152
 
155
- **SECURE Implementation (PRF-based)**:
153
+ **Process**:
156
154
  ```typescript
157
155
  function deriveEncryptionKeyFromWebAuthn(
158
- prfOutput: ArrayBuffer, // From WebAuthn PRF extension
159
- salt: Uint8Array // Random 32-byte salt
156
+ credentialId: string,
157
+ publicKey: string
160
158
  ): CryptoKey {
161
- // Step 1: Validate inputs
162
- assert(prfOutput.byteLength === 32);
163
- assert(salt.byteLength === 32);
159
+ // Step 1: Combine inputs
160
+ const keyMaterial = `w3pk-v4:${credentialId}:${publicKey}`;
164
161
 
165
- // Step 2: Import PRF output (authenticator-held secret)
166
- const keyMaterial = importKey(prfOutput);
162
+ // Step 2: Hash with SHA-256
163
+ const hash = SHA256(keyMaterial);
167
164
 
168
- // Step 3: PBKDF2 key derivation with random salt
169
- const encryptionKey = PBKDF2(keyMaterial, salt, 210000);
165
+ // Step 3: PBKDF2 key derivation (210,000 iterations)
166
+ const encryptionKey = PBKDF2(hash, salt, 210000);
170
167
 
171
168
  // Result: AES-256-GCM key (NEVER STORED)
172
169
  return encryptionKey;
173
170
  }
174
171
  ```
175
172
 
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
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
187
178
 
188
179
  ---
189
180
 
@@ -226,12 +217,11 @@ STEP 2: Create WebAuthn Credential (INDEPENDENT)
226
217
 
227
218
  STEP 3: Derive Encryption Key
228
219
  ┌──────────────────────────────────────┐
229
- deriveEncryptionKeyFromWebAuthn()
220
+ deriveEncryptionKeyFromWebAuthn()
230
221
  │ │
231
- Input: PRF output (32 bytes secret)
232
- │ + random salt (32 bytes) │
222
+ Input: credentialId + publicKey
233
223
  │ ↓ │
234
- Import PRF as key material
224
+ SHA-256 hash
235
225
  │ ↓ │
236
226
  │ PBKDF2 (210,000 iterations) │
237
227
  │ ↓ │
@@ -496,29 +486,43 @@ const signedTx = await wallet.signTransaction(tx);
496
486
  **Implementation**: [src/wallet/crypto.ts:21-66](src/wallet/crypto.ts#L21-L66)
497
487
 
498
488
  ```javascript
499
- // SECURE: PRF-based key derivation
500
489
  async function deriveEncryptionKeyFromWebAuthn(
501
- prfOutput: ArrayBuffer, // From WebAuthn PRF extension
502
- salt: Uint8Array // Random 32-byte salt
490
+ credentialId: string,
491
+ publicKey: string
503
492
  ): Promise<CryptoKey> {
504
- // 1. Import PRF output as key material (authenticator secret)
505
- const keyMaterial = await crypto.subtle.importKey(
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(
506
504
  "raw",
507
- prfOutput, // SECRET from authenticator
505
+ keyMaterialHash,
508
506
  { name: "PBKDF2" },
509
507
  false,
510
508
  ["deriveKey"]
511
509
  );
512
510
 
513
- // 2. Derive AES-256-GCM key with PBKDF2
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
514
518
  return crypto.subtle.deriveKey(
515
519
  {
516
520
  name: "PBKDF2",
517
- salt: salt, // Random 32-byte salt (stored with ciphertext)
521
+ salt: new Uint8Array(salt),
518
522
  iterations: 210000, // OWASP 2023 recommendation
519
523
  hash: "SHA-256",
520
524
  },
521
- keyMaterial, // PRF output from authenticator
525
+ importedKey,
522
526
  { name: "AES-GCM", length: 256 },
523
527
  false,
524
528
  ["encrypt", "decrypt"]
@@ -526,12 +530,11 @@ async function deriveEncryptionKeyFromWebAuthn(
526
530
  }
527
531
  ```
528
532
 
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)
533
+ **Properties**:
534
+ - **Deterministic**: Same inputs always produce same output
535
+ - **Secure**: 210,000 iterations make brute-force impractical
533
536
  - **Stateless**: No need to store the encryption key
534
- - **Fast**: ~100-200ms on modern devices
537
+ - **Fast enough**: ~100-200ms on modern devices
535
538
 
536
539
  ---
537
540
 
@@ -1412,13 +1412,10 @@ const strictWallet = await w3pk.deriveWallet('STRICT')
1412
1412
  ```
1413
1413
 
1414
1414
  **Persistent Session Security:**
1415
- - STANDARD mode: Persistent sessions allowed
1416
- - YOLO mode: Persistent sessions ✅ allowed
1417
- - STRICT mode: Persistent sessions NEVER allowed
1418
- - Sessions encrypted with WebAuthn-derived keys
1419
- - Requires valid credential to decrypt
1420
- - Time-limited expiration
1421
- - Origin-isolated via IndexedDB
1415
+ - STANDARD / YOLO: persistent sessions allowed. STRICT: ❌ NEVER allowed.
1416
+ - Time-limited expiration; origin-isolated via IndexedDB.
1417
+ - ⚠️ The at-rest key is derived from **public** credential metadata in the same browser profile, not an authenticator secret — a persistent session is **decryptable by anyone who can read this origin's storage**, for the whole duration it's enabled. "Remember Me" is a UX-for-security trade-off that widens the exposure window; it does not add a new trust dependency beyond the origin-code integrity you already rely on. Keep the duration short for higher-value wallets and don't enable on shared devices.
1418
+ - Decide this deliberately against your threat model: **[SECURITY.md → At-Rest Encryption](./SECURITY.md#at-rest-encryption-what-it-protects-and-what-it-doesnt)**.
1422
1419
 
1423
1420
  ### Build Verification
1424
1421