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.
- package/README.md +2 -0
- package/dist/index.d.mts +39 -12
- package/dist/index.d.ts +39 -12
- package/dist/index.js +24 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +25 -25
- package/dist/index.mjs.map +1 -1
- package/docs/API_REFERENCE.md +9 -90
- package/docs/ARCHITECTURE.md +45 -42
- package/docs/INTEGRATION_GUIDELINES.md +4 -7
- package/docs/SECURITY.md +155 -314
- package/package.json +1 -1
- package/dist/chainlist/index.d.mts +0 -96
- package/dist/chainlist/index.d.ts +0 -96
package/docs/API_REFERENCE.md
CHANGED
|
@@ -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
|
-
-
|
|
2853
|
-
-
|
|
2854
|
-
-
|
|
2855
|
-
-
|
|
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 }`
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
-
**
|
|
153
|
+
**Process**:
|
|
156
154
|
```typescript
|
|
157
155
|
function deriveEncryptionKeyFromWebAuthn(
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
credentialId: string,
|
|
157
|
+
publicKey: string
|
|
160
158
|
): CryptoKey {
|
|
161
|
-
// Step 1:
|
|
162
|
-
|
|
163
|
-
assert(salt.byteLength === 32);
|
|
159
|
+
// Step 1: Combine inputs
|
|
160
|
+
const keyMaterial = `w3pk-v4:${credentialId}:${publicKey}`;
|
|
164
161
|
|
|
165
|
-
// Step 2:
|
|
166
|
-
const
|
|
162
|
+
// Step 2: Hash with SHA-256
|
|
163
|
+
const hash = SHA256(keyMaterial);
|
|
167
164
|
|
|
168
|
-
// Step 3: PBKDF2 key derivation
|
|
169
|
-
const encryptionKey = PBKDF2(
|
|
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
|
-
**
|
|
177
|
-
- **
|
|
178
|
-
- **
|
|
179
|
-
- **
|
|
180
|
-
- **
|
|
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
|
-
│
|
|
220
|
+
│ deriveEncryptionKeyFromWebAuthn() │
|
|
230
221
|
│ │
|
|
231
|
-
│
|
|
232
|
-
│ + random salt (32 bytes) │
|
|
222
|
+
│ Input: credentialId + publicKey │
|
|
233
223
|
│ ↓ │
|
|
234
|
-
│
|
|
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
|
-
|
|
502
|
-
|
|
490
|
+
credentialId: string,
|
|
491
|
+
publicKey: string
|
|
503
492
|
): Promise<CryptoKey> {
|
|
504
|
-
// 1.
|
|
505
|
-
const keyMaterial =
|
|
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
|
-
|
|
505
|
+
keyMaterialHash,
|
|
508
506
|
{ name: "PBKDF2" },
|
|
509
507
|
false,
|
|
510
508
|
["deriveKey"]
|
|
511
509
|
);
|
|
512
510
|
|
|
513
|
-
//
|
|
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:
|
|
521
|
+
salt: new Uint8Array(salt),
|
|
518
522
|
iterations: 210000, // OWASP 2023 recommendation
|
|
519
523
|
hash: "SHA-256",
|
|
520
524
|
},
|
|
521
|
-
|
|
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
|
-
**
|
|
530
|
-
- **
|
|
531
|
-
- **
|
|
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
|
|
1416
|
-
-
|
|
1417
|
-
-
|
|
1418
|
-
-
|
|
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
|
|