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/docs/SECURITY.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  This document explains the security model of w3pk and how wallet protection works.
4
4
 
5
+ ✅ **SECURITY**: Uses PRF-based encryption with authenticator-held secrets and random salts to prevent offline decryption attacks. See [PRF-based Encryption](#migration-to-prf-based-encryption) section below.
6
+
5
7
  ## Overview
6
8
 
7
9
  w3pk provides **multiple layers of security** to protect user wallets:
@@ -12,7 +14,7 @@ w3pk provides **multiple layers of security** to protect user wallets:
12
14
  4. **Mode-based access control** - STANDARD/STRICT modes are view-only, YOLO mode provides full access
13
15
  5. **Encrypted storage** - AES-256-GCM encryption at rest
14
16
  6. **Secure sessions** - In-memory and optional persistent sessions (disabled in STRICT mode)
15
- 7. **Persistent session encryption** - WebAuthn-derived key encryption for "Remember Me" functionality
17
+ 7. **PRF-based encryption** - WebAuthn PRF extension for secure key derivation (v0.9.4+)
16
18
 
17
19
  ## Enhanced Security Model (v0.8.0+)
18
20
 
@@ -1081,32 +1083,96 @@ Signature: ${authorization.r.substring(0, 10)}...
1081
1083
 
1082
1084
  This section tracks major security-related changes to w3pk's implementation.
1083
1085
 
1084
- ### v0.7.0+ (Current)
1086
+ ### v0.9.4+ - PRF-Based Encryption (Critical Security Fix)
1085
1087
 
1086
- #### Removed: `deriveEncryptionKeyFromSignature()` (Commit 182740c)
1087
- **Impact:** Security improvement
1088
+ #### BREAKING: `deriveEncryptionKeyFromWebAuthn()` signature changed
1089
+ **Impact:** Fixes critical encryption vulnerabilities (BREAKING CHANGE)
1088
1090
 
1089
1091
  **What changed:**
1090
- - Removed the `deriveEncryptionKeyFromSignature()` function
1091
- - This was a testing/legacy fallback for signature-based key derivation
1092
- - Code comment warned: "Does NOT require biometric authentication for decryption"
1092
+ - `deriveEncryptionKeyFromWebAuthn()` signature changed from `(credentialId, publicKey?)` to `(prfOutput, salt)`
1093
+ - Now uses WebAuthn PRF extension to obtain authenticator-held secrets
1094
+ - Implements random unique salts instead of hardcoded constants
1095
+ - Added `generateSalt()` helper for creating cryptographic random salts
1093
1096
 
1094
- **Why this improves security:**
1095
- - Eliminates a weaker encryption path that could have been misused
1096
- - Reduces code complexity and potential for developer errors
1097
- - Single clear encryption method (`deriveEncryptionKeyFromWebAuthn()`)
1098
- - Prevents accidental use of signature-based approach without proper session management
1097
+ **Security improvements:**
1098
+ - Key material now derived from authenticator secret (PRF output), not public credentialId/publicKey
1099
+ - Uses random 32-byte salts stored with ciphertext, not hardcoded constants
1100
+ - Eliminates offline decryption vulnerability
1101
+ - Prevents precomputation attacks
1102
+
1103
+ **Migration required:**
1104
+ ```typescript
1105
+ // ❌ OLD (v0.9.3 and earlier) - INSECURE
1106
+ const key = await deriveEncryptionKeyFromWebAuthn(credentialId, publicKey);
1107
+
1108
+ // ✅ NEW (v0.9.4+) - SECURE
1109
+ const assertion = await navigator.credentials.get({
1110
+ publicKey: {
1111
+ challenge: new Uint8Array(32),
1112
+ extensions: {
1113
+ prf: { eval: { first: new Uint8Array(32) } }
1114
+ }
1115
+ }
1116
+ });
1117
+ const prfOutput = assertion.getClientExtensionResults().prf.results.first;
1118
+ const salt = generateSalt(); // Store with ciphertext
1119
+ const key = await deriveEncryptionKeyFromWebAuthn(prfOutput, salt);
1120
+ ```
1121
+
1122
+ **Status:**
1123
+ - ✅ Secure implementation complete
1124
+ - 🔴 Breaking change - migration required
1125
+ - 📖 Migration guide below
1099
1126
 
1100
- **Migration:** No action needed. This function was never the primary method and was only used for testing.
1127
+ See: [Migration to PRF-based Encryption](#migration-to-prf-based-encryption)
1128
+
1129
+ ---
1130
+
1131
+ ### v0.9.4+ - ERC-5564 Stealth Scalar Reduction Fix (High Severity)
1132
+
1133
+ #### Fixed stealth address scalar reduction vulnerability
1134
+
1135
+ **What changed:**
1136
+ - Added `reduceScalarModN()` function to properly reduce scalars modulo secp256k1 curve order
1137
+ - Updated `generateStealthAddress()`, `checkStealthAddress()`, and `computeStealthPrivateKey()` to use reduced scalars
1138
+ - Ensures the same reduced scalar `s_h` is used consistently in both public-key and private-key operations
1139
+
1140
+ **Security improvements:**
1141
+ - ✅ Prevents intermittent failures when keccak256(sharedSecret) ≥ curve order
1142
+ - ✅ Eliminates risk of unspendable stealth funds caused by inconsistent scalar handling
1143
+ - ✅ Ensures derived stealth address always matches derived private key
1144
+ - ✅ Validates scalars are in valid range [1, n-1] and rejects zero
1145
+
1146
+ **Technical details:**
1147
+ - ERC-5564 computes `s_h = keccak256(sharedSecret)` and uses it as a scalar
1148
+ - Previously, `s_h` was used directly without reduction modulo `n` (secp256k1 curve order)
1149
+ - Public-key path: `stealthPubKey = spendingPubKey + (s_h × G)` would fail if `s_h ≥ n`
1150
+ - Private-key path: `stealthPrivKey = spendingKey + s_h` reduced the sum but not `s_h` itself
1151
+ - Inconsistent scalar values between paths could result in address/key mismatch
1152
+ - Now both paths use the same properly-reduced scalar
1153
+
1154
+ **Impact:**
1155
+ - Non-breaking change (pure bugfix)
1156
+ - All existing stealth addresses remain valid
1157
+ - Eliminates edge-case failures and unspendable funds
1158
+
1159
+ ---
1101
1160
 
1102
- #### Current Encryption Method
1103
- **Primary:** `deriveEncryptionKeyFromWebAuthn(credentialId, publicKey)`
1104
- - Deterministic key derivation from credential metadata
1105
- - PBKDF2 with 210,000 iterations (OWASP 2023)
1106
- - Fixed salt: `"w3pk-salt-v4"`
1107
- - Security relies on SDK authentication-gating
1161
+ #### No Backward Compatibility
1162
+ **Decision:** Complete removal of insecure legacy function
1108
1163
 
1109
- **Status:** Working as intended. This approach enables session management while maintaining security through WebAuthn authentication requirements.
1164
+ **What was removed:**
1165
+ - Removed `deriveEncryptionKeyFromWebAuthnLegacy()` function entirely
1166
+ - No backward compatibility layer for insecure encryption
1167
+
1168
+ **Why:**
1169
+ - Legacy function had critical security vulnerabilities
1170
+ - Offline decryption attack risk unacceptable
1171
+ - Clean break ensures no accidental use of insecure method
1172
+
1173
+ **Migration:** All call sites updated to use `deriveEncryptionKeyAuto()` which:
1174
+ - Uses PRF-based encryption when available (secure)
1175
+ - Falls back to v2 implementation internally for wallets without PRF support
1110
1176
 
1111
1177
  ### v0.7.0 - RP ID Auto-Detection (Security Hardening)
1112
1178
 
@@ -3683,6 +3749,159 @@ This democratizes security analysis and helps users make informed decisions abou
3683
3749
 
3684
3750
  ---
3685
3751
 
3752
+ ## Migration to PRF-based Encryption
3753
+
3754
+ ### Overview
3755
+
3756
+ The secure `deriveEncryptionKeyFromWebAuthn()` uses PRF-based encryption to prevent vulnerabilities:
3757
+ - **Fixed**: Now derives encryption key from authenticator-held secret (PRF output), not public credential data
3758
+ - **Fixed**: Uses random unique salts, not hardcoded constants
3759
+
3760
+ **Benefits**: Prevents offline decryption attacks by requiring authenticator interaction.
3761
+
3762
+ ### Migration Steps
3763
+
3764
+ #### 1. Enable PRF Extension in Registration
3765
+
3766
+ ```typescript
3767
+ // Update WebAuthn registration to enable PRF
3768
+ const credential = await navigator.credentials.create({
3769
+ publicKey: {
3770
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
3771
+ rp: {
3772
+ name: "Your App",
3773
+ id: window.location.hostname
3774
+ },
3775
+ user: {
3776
+ id: userId,
3777
+ name: username,
3778
+ displayName: username
3779
+ },
3780
+ pubKeyCredParams: [{ alg: -7, type: "public-key" }],
3781
+ authenticatorSelection: {
3782
+ authenticatorAttachment: "platform",
3783
+ userVerification: "required"
3784
+ },
3785
+ extensions: {
3786
+ prf: {} // ← Enable PRF extension
3787
+ }
3788
+ }
3789
+ });
3790
+
3791
+ // Check if PRF is supported
3792
+ const prfEnabled = credential.getClientExtensionResults().prf?.enabled;
3793
+ if (!prfEnabled) {
3794
+ console.warn("PRF not supported on this authenticator");
3795
+ // Fall back to legacy method or require different authenticator
3796
+ }
3797
+ ```
3798
+
3799
+ #### 2. Capture PRF Output During Authentication
3800
+
3801
+ ```typescript
3802
+ // Get PRF output during WebAuthn assertion
3803
+ const prfSalt = crypto.getRandomValues(new Uint8Array(32));
3804
+
3805
+ const assertion = await navigator.credentials.get({
3806
+ publicKey: {
3807
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
3808
+ rpId: window.location.hostname,
3809
+ extensions: {
3810
+ prf: {
3811
+ eval: {
3812
+ first: prfSalt // Input to PRF (can be constant per credential)
3813
+ }
3814
+ }
3815
+ }
3816
+ }
3817
+ });
3818
+
3819
+ // Extract PRF output
3820
+ const prfResults = assertion.getClientExtensionResults().prf;
3821
+ if (!prfResults?.results?.first) {
3822
+ throw new Error("PRF output not available");
3823
+ }
3824
+ const prfOutput = prfResults.results.first; // 32-byte secret
3825
+ ```
3826
+
3827
+ #### 3. Generate and Store Salt with Ciphertext
3828
+
3829
+ ```typescript
3830
+ import { deriveEncryptionKeyFromWebAuthn, generateSalt } from 'w3pk';
3831
+
3832
+ // Generate random salt (do this once per encryption)
3833
+ const salt = generateSalt(); // 32 random bytes
3834
+
3835
+ // Derive secure encryption key
3836
+ const encryptionKey = await deriveEncryptionKeyFromWebAuthn(
3837
+ prfOutput, // From authenticator (secret)
3838
+ salt // Random (store with ciphertext)
3839
+ );
3840
+
3841
+ // Encrypt wallet data
3842
+ const encrypted = await crypto.subtle.encrypt(
3843
+ { name: "AES-GCM", iv: crypto.getRandomValues(new Uint8Array(12)) },
3844
+ encryptionKey,
3845
+ new TextEncoder().encode(mnemonic)
3846
+ );
3847
+
3848
+ // Store both salt and ciphertext together
3849
+ await storage.save({
3850
+ encryptedMnemonic: base64(encrypted),
3851
+ salt: base64(salt), // ← Must be stored for decryption
3852
+ credentialId: credential.id
3853
+ });
3854
+ ```
3855
+
3856
+ #### 4. Decrypt Using Stored Salt
3857
+
3858
+ ```typescript
3859
+ // Load encrypted data
3860
+ const data = await storage.load();
3861
+ const salt = base64Decode(data.salt);
3862
+ const encryptedMnemonic = base64Decode(data.encryptedMnemonic);
3863
+
3864
+ // Get PRF output (same as step 2)
3865
+ const assertion = await navigator.credentials.get({
3866
+ publicKey: {
3867
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
3868
+ extensions: {
3869
+ prf: { eval: { first: prfSalt } }
3870
+ }
3871
+ }
3872
+ });
3873
+ const prfOutput = assertion.getClientExtensionResults().prf.results.first;
3874
+
3875
+ // Derive key with stored salt
3876
+ const encryptionKey = await deriveEncryptionKeyFromWebAuthn(prfOutput, salt);
3877
+
3878
+ // Decrypt
3879
+ const decrypted = await crypto.subtle.decrypt(
3880
+ { name: "AES-GCM", iv: extractIV(encryptedMnemonic) },
3881
+ encryptionKey,
3882
+ encryptedMnemonic
3883
+ );
3884
+ ```
3885
+
3886
+ ### Browser Support
3887
+
3888
+ PRF extension support (as of 2026):
3889
+ - ✅ Chrome/Edge 108+ (Windows Hello, Touch ID)
3890
+ - ✅ Safari 17+ (Touch ID, Face ID)
3891
+ - ✅ Firefox 122+ (limited)
3892
+ - ⚠️ Check with `getClientExtensionResults().prf.enabled`
3893
+
3894
+ ### Impact on Existing Users
3895
+
3896
+ ✅ **NO BREAKING CHANGE for SDK users**: The SDK handles migration automatically via `deriveEncryptionKeyAuto()`.
3897
+
3898
+ **For direct API users:**
3899
+ - **Function signature changed**: `deriveEncryptionKeyFromWebAuthn()` now requires `(prfOutput, salt)` parameters
3900
+ - **Legacy function removed**: No `deriveEncryptionKeyFromWebAuthnLegacy()` available
3901
+ - **Migration path**: Use PRF-based encryption or use the auto-fallback helper
3902
+
3903
+ ---
3904
+
3686
3905
  ## Post-Quantum Cryptography
3687
3906
 
3688
3907
  w3pk is preparing for the future quantum computing threat. While quantum computers capable of breaking current cryptography (ECDSA, secp256k1) are estimated to be **10-15 years away**, we have a comprehensive migration roadmap in place.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "w3pk",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "WebAuthn SDK for passwordless authentication, encrypted wallets, ERC-5564 stealth addresses, and zero-knowledge proofs",
5
5
  "author": "Julien Béranger",
6
6
  "license": "GPL-3.0",
@@ -84,30 +84,34 @@
84
84
  "scripts/compute-build-hash.mjs"
85
85
  ],
86
86
  "devDependencies": {
87
- "@types/node": "^24.9.0",
87
+ "@types/node": "^25.9.2",
88
88
  "@types/qrcode": "^1.5.5",
89
89
  "circomlib": "^2.0.5",
90
90
  "tsup": "^8.5.1",
91
- "tsx": "^4.21.0",
92
- "typescript": "^5.9.3"
91
+ "tsx": "^4.22.4",
92
+ "typescript": "^6.0.3"
93
93
  },
94
94
  "peerDependencies": {
95
95
  "ethers": "^6.0.0"
96
96
  },
97
97
  "optionalDependencies": {
98
- "@ipld/car": "^5.4.2",
99
- "blockstore-core": "^6.1.1",
98
+ "@ipld/car": "^5.4.6",
99
+ "blockstore-core": "^7.0.1",
100
100
  "circomlibjs": "^0.1.7",
101
- "ipfs-unixfs-importer": "^16.0.1",
101
+ "ipfs-unixfs-importer": "^17.0.1",
102
102
  "qrcode": "^1.5.4",
103
- "snarkjs": "^0.7.5"
103
+ "snarkjs": "^0.7.6"
104
+ },
105
+ "dependencies": {
106
+ "@noble/hashes": "^2.2.0",
107
+ "mlkem": "^2.7.0"
104
108
  },
105
109
  "scripts": {
106
110
  "build": "tsup",
107
111
  "build:zk": "npm run build && npm run compile:circuits",
108
112
  "compile:circuits": "node scripts/compile-circuits.js",
109
113
  "dev": "tsup --watch",
110
- "test": "tsx test/test.ts && tsx test/comprehensive.test.ts && tsx test/backup.test.ts && tsx test/social-recovery.test.ts && tsx test/recovery-education.test.ts && tsx test/zk/zk.test.ts && tsx test/nft-ownership.test.ts && tsx test/chainlist.test.ts && tsx test/eip7702.test.ts && tsx test/external-wallet.test.ts && tsx test/erc5564.test.ts && tsx test/zk/key-stretching.test.ts && tsx test/username-encoding.test.ts && tsx test/username-validation.test.ts && tsx test/origin-derivation.test.ts && tsx test/build-hash.test.ts && tsx test/credential-checking.test.ts && tsx test/webauthn-native.test.ts && tsx test/persistent-session.test.ts && tsx test/sign-message.test.ts && tsx test/siwe.test.ts && tsx test/requirereauth.test.ts && tsx test/eip7951.test.ts && tsx test/send-transaction.test.ts && tsx test/eip1193-provider.test.ts",
114
+ "test": "tsx test/test.ts && tsx test/comprehensive.test.ts && tsx test/backup.test.ts && tsx test/social-recovery.test.ts && tsx test/recovery-education.test.ts && tsx test/zk/zk.test.ts && tsx test/nft-ownership.test.ts && tsx test/chainlist.test.ts && tsx test/eip7702.test.ts && tsx test/external-wallet.test.ts && tsx test/erc5564.test.ts && tsx test/zk/key-stretching.test.ts && tsx test/username-encoding.test.ts && tsx test/username-validation.test.ts && tsx test/origin-derivation.test.ts && tsx test/build-hash.test.ts && tsx test/credential-checking.test.ts && tsx test/webauthn-native.test.ts && tsx test/persistent-session.test.ts && tsx test/sign-message.test.ts && tsx test/siwe.test.ts && tsx test/requirereauth.test.ts && tsx test/eip7951.test.ts && tsx test/send-transaction.test.ts && tsx test/eip1193-provider.test.ts && tsx test/mlkem.test.ts",
111
115
  "test:basic": "tsx test/test.ts",
112
116
  "test:comprehensive": "tsx test/comprehensive.test.ts",
113
117
  "test:backup": "tsx test/backup.test.ts",
@@ -125,7 +129,6 @@
125
129
  "test:sign-message": "tsx test/sign-message.test.ts",
126
130
  "test:siwe": "tsx test/siwe.test.ts",
127
131
  "build:hash": "node scripts/compute-build-hash.mjs",
128
- "release:notes": "node scripts/generate-release-notes.mjs",
129
132
  "html": "lsof -ti:3000 | xargs kill -9 2>/dev/null || true && tsup && npx serve . -l 3000 & sleep 5 && open http://localhost:3000/standalone/checker.html"
130
133
  }
131
134
  }