hd-wallet-ui 2.0.2 → 2.0.4
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/package.json +2 -2
- package/src/app.js +67 -54
- package/src/wallet-storage.js +56 -92
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hd-wallet-ui",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "HD Wallet modal UI — login, keys, identity, trust map, and security bond. Attach to any button in your app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/app.js",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"buffer": "^6.0.3",
|
|
41
41
|
"flatbuffers": "^25.9.23",
|
|
42
42
|
"flatc-wasm": "^26.1.15",
|
|
43
|
-
"hd-wallet-wasm": "^2.0.
|
|
43
|
+
"hd-wallet-wasm": "^2.0.4",
|
|
44
44
|
"qrcode": "^1.5.3",
|
|
45
45
|
"spacedatastandards.org": "^23.3.3-0.3.4",
|
|
46
46
|
"vcard-cryptoperson": "^1.1.11"
|
package/src/app.js
CHANGED
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
// =============================================================================
|
|
11
11
|
|
|
12
12
|
import initHDWallet, { Curve, getSigningKey, getEncryptionKey, buildSigningPath, buildEncryptionPath, WellKnownCoinType } from 'hd-wallet-wasm';
|
|
13
|
-
import { x25519, ed25519 } from '@noble/curves/ed25519';
|
|
14
|
-
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
15
|
-
import { p256 } from '@noble/curves/p256';
|
|
16
|
-
import { sha256 as sha256Noble } from '@noble/hashes/sha256';
|
|
17
13
|
import { keccak_256 } from '@noble/hashes/sha3';
|
|
18
14
|
import QRCode from 'qrcode';
|
|
19
15
|
import { Buffer } from 'buffer';
|
|
@@ -172,41 +168,55 @@ function bytesToBase64(bytes) {
|
|
|
172
168
|
}
|
|
173
169
|
|
|
174
170
|
// =============================================================================
|
|
175
|
-
//
|
|
171
|
+
// WASM-backed cryptographic helpers
|
|
176
172
|
// =============================================================================
|
|
177
173
|
|
|
174
|
+
function hdWallet() {
|
|
175
|
+
if (!state.hdWalletModule) throw new Error('HD wallet WASM module not initialized');
|
|
176
|
+
return state.hdWalletModule;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function asUint8Array(value) {
|
|
180
|
+
if (value instanceof Uint8Array) return value;
|
|
181
|
+
if (ArrayBuffer.isView(value)) {
|
|
182
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
183
|
+
}
|
|
184
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
185
|
+
throw new Error('Expected byte array');
|
|
186
|
+
}
|
|
187
|
+
|
|
178
188
|
async function sha256(data) {
|
|
179
|
-
|
|
180
|
-
return new Uint8Array(hash);
|
|
189
|
+
return hdWallet().utils.sha256(asUint8Array(data));
|
|
181
190
|
}
|
|
182
191
|
|
|
183
192
|
async function hkdf(ikm, salt, info, length) {
|
|
184
|
-
|
|
185
|
-
const derived = await crypto.subtle.deriveBits(
|
|
186
|
-
{ name: 'HKDF', hash: 'SHA-256', salt, info },
|
|
187
|
-
key,
|
|
188
|
-
length * 8
|
|
189
|
-
);
|
|
190
|
-
return new Uint8Array(derived);
|
|
193
|
+
return hdWallet().utils.hkdf(asUint8Array(ikm), asUint8Array(salt), asUint8Array(info), length);
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
async function aesGcmEncryptJson(keyBytes, obj, aadStr) {
|
|
194
197
|
if (!(keyBytes instanceof Uint8Array)) throw new Error('Invalid AES key');
|
|
195
|
-
const iv =
|
|
196
|
-
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
|
|
197
|
-
const alg = { name: 'AES-GCM', iv };
|
|
198
|
-
if (aadStr) alg.additionalData = new TextEncoder().encode(aadStr);
|
|
198
|
+
const iv = hdWallet().utils.getRandomBytes(12);
|
|
199
199
|
const plaintext = new TextEncoder().encode(JSON.stringify(obj));
|
|
200
|
-
const
|
|
201
|
-
|
|
200
|
+
const aad = aadStr ? new TextEncoder().encode(aadStr) : new Uint8Array(0);
|
|
201
|
+
const { ciphertext, tag } = hdWallet().utils.aesGcm.encrypt(keyBytes, plaintext, iv, aad);
|
|
202
|
+
const sealed = new Uint8Array(ciphertext.length + tag.length);
|
|
203
|
+
sealed.set(ciphertext, 0);
|
|
204
|
+
sealed.set(tag, ciphertext.length);
|
|
205
|
+
return { iv, ciphertext: sealed };
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
async function aesGcmDecryptJson(keyBytes, iv, ciphertextBytes, aadStr) {
|
|
205
209
|
if (!(keyBytes instanceof Uint8Array)) throw new Error('Invalid AES key');
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const plaintext =
|
|
210
|
+
const sealed = asUint8Array(ciphertextBytes);
|
|
211
|
+
if (sealed.length <= 16) throw new Error('Ciphertext is too short');
|
|
212
|
+
const aad = aadStr ? new TextEncoder().encode(aadStr) : new Uint8Array(0);
|
|
213
|
+
const plaintext = hdWallet().utils.aesGcm.decrypt(
|
|
214
|
+
keyBytes,
|
|
215
|
+
sealed.subarray(0, sealed.length - 16),
|
|
216
|
+
sealed.subarray(sealed.length - 16),
|
|
217
|
+
asUint8Array(iv),
|
|
218
|
+
aad
|
|
219
|
+
);
|
|
210
220
|
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
211
221
|
}
|
|
212
222
|
|
|
@@ -215,35 +225,39 @@ async function aesGcmDecryptJson(keyBytes, iv, ciphertextBytes, aadStr) {
|
|
|
215
225
|
// =============================================================================
|
|
216
226
|
|
|
217
227
|
function generateKeyPair(curveType) {
|
|
228
|
+
const w = hdWallet();
|
|
218
229
|
if (curveType === Curve.SECP256K1) {
|
|
219
|
-
|
|
220
|
-
const publicKey = secp256k1.getPublicKey(privateKey, true);
|
|
221
|
-
return { privateKey, publicKey };
|
|
230
|
+
return generateEcKeyPair(w, Curve.SECP256K1, 32);
|
|
222
231
|
}
|
|
223
232
|
if (curveType === Curve.X25519) {
|
|
224
|
-
const privateKey =
|
|
225
|
-
const publicKey = x25519.
|
|
233
|
+
const privateKey = w.utils.getRandomBytes(32);
|
|
234
|
+
const publicKey = w.curves.x25519.publicKey(privateKey);
|
|
226
235
|
return { privateKey, publicKey };
|
|
227
236
|
}
|
|
228
237
|
throw new Error(`Unsupported curve type: ${curveType}`);
|
|
229
238
|
}
|
|
230
239
|
|
|
240
|
+
function generateEcKeyPair(w, curveType, privateKeyLength) {
|
|
241
|
+
let lastError = null;
|
|
242
|
+
for (let i = 0; i < 16; i += 1) {
|
|
243
|
+
const privateKey = w.utils.getRandomBytes(privateKeyLength);
|
|
244
|
+
try {
|
|
245
|
+
const publicKey = w.curves.publicKeyFromPrivate(privateKey, curveType);
|
|
246
|
+
return { privateKey, publicKey };
|
|
247
|
+
} catch (err) {
|
|
248
|
+
lastError = err;
|
|
249
|
+
privateKey.fill(0);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
throw lastError || new Error('Failed to generate curve keypair');
|
|
253
|
+
}
|
|
254
|
+
|
|
231
255
|
async function p256GenerateKeyPairAsync() {
|
|
232
|
-
|
|
233
|
-
{ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']
|
|
234
|
-
);
|
|
235
|
-
const rawPublic = await crypto.subtle.exportKey('raw', keyPair.publicKey);
|
|
236
|
-
const pkcs8Private = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
|
|
237
|
-
return { publicKey: new Uint8Array(rawPublic), privateKey: new Uint8Array(pkcs8Private) };
|
|
256
|
+
return generateEcKeyPair(hdWallet(), Curve.P256, 32);
|
|
238
257
|
}
|
|
239
258
|
|
|
240
259
|
async function p384GenerateKeyPairAsync() {
|
|
241
|
-
|
|
242
|
-
{ name: 'ECDSA', namedCurve: 'P-384' }, true, ['sign', 'verify']
|
|
243
|
-
);
|
|
244
|
-
const rawPublic = await crypto.subtle.exportKey('raw', keyPair.publicKey);
|
|
245
|
-
const pkcs8Private = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
|
|
246
|
-
return { publicKey: new Uint8Array(rawPublic), privateKey: new Uint8Array(pkcs8Private) };
|
|
260
|
+
return generateEcKeyPair(hdWallet(), Curve.P384, 48);
|
|
247
261
|
}
|
|
248
262
|
|
|
249
263
|
// =============================================================================
|
|
@@ -437,7 +451,7 @@ function deriveKeysFromHDRoot(hdRoot) {
|
|
|
437
451
|
|
|
438
452
|
// SOL signing key m/44'/501'/0'/0/0 — ed25519
|
|
439
453
|
const solSigning = getSigningKey(hdRoot, 501, 0, 0);
|
|
440
|
-
const ed25519PubKey = ed25519.
|
|
454
|
+
const ed25519PubKey = hdWallet().curves.ed25519.publicKeyFromSeed(solSigning.privateKey);
|
|
441
455
|
|
|
442
456
|
return {
|
|
443
457
|
secp256k1: { privateKey: btcSigning.privateKey, publicKey: btcSigning.publicKey },
|
|
@@ -470,7 +484,7 @@ function deriveAllAddressesFromHD() {
|
|
|
470
484
|
const solPath = buildSigningPath(501, 0, 0);
|
|
471
485
|
const solDerived = state.hdRoot.derivePath(solPath);
|
|
472
486
|
const solPrivKey = solDerived.privateKey();
|
|
473
|
-
solAddress = generateSolAddress(ed25519.
|
|
487
|
+
solAddress = generateSolAddress(hdWallet().curves.ed25519.publicKeyFromSeed(solPrivKey));
|
|
474
488
|
} catch (e) {
|
|
475
489
|
console.error('Failed to derive SOL address:', e);
|
|
476
490
|
}
|
|
@@ -549,7 +563,7 @@ function deriveAddressForPath(coinType, account, index) {
|
|
|
549
563
|
if (coinType === 501) {
|
|
550
564
|
// Solana: ed25519
|
|
551
565
|
const privKey = derived.privateKey();
|
|
552
|
-
const pubKey = ed25519.
|
|
566
|
+
const pubKey = hdWallet().curves.ed25519.publicKeyFromSeed(privKey);
|
|
553
567
|
return { address: generateSolAddress(pubKey), publicKey: pubKey, path };
|
|
554
568
|
}
|
|
555
569
|
|
|
@@ -2182,7 +2196,7 @@ function deriveKeyFromPath(path) {
|
|
|
2182
2196
|
|
|
2183
2197
|
function deriveX25519FromSeed(seed) {
|
|
2184
2198
|
const privateKey = new Uint8Array(seed);
|
|
2185
|
-
const publicKey = x25519.
|
|
2199
|
+
const publicKey = hdWallet().curves.x25519.publicKey(privateKey);
|
|
2186
2200
|
return {
|
|
2187
2201
|
privateKey,
|
|
2188
2202
|
publicKey: new Uint8Array(publicKey),
|
|
@@ -2191,7 +2205,7 @@ function deriveX25519FromSeed(seed) {
|
|
|
2191
2205
|
|
|
2192
2206
|
function deriveSecp256k1FromSeed(seed) {
|
|
2193
2207
|
const privateKey = new Uint8Array(seed);
|
|
2194
|
-
const publicKey =
|
|
2208
|
+
const publicKey = hdWallet().curves.publicKeyFromPrivate(privateKey, Curve.SECP256K1);
|
|
2195
2209
|
return {
|
|
2196
2210
|
privateKey,
|
|
2197
2211
|
publicKey: new Uint8Array(publicKey),
|
|
@@ -2200,7 +2214,7 @@ function deriveSecp256k1FromSeed(seed) {
|
|
|
2200
2214
|
|
|
2201
2215
|
function deriveP256FromSeed(seed) {
|
|
2202
2216
|
const privateKey = new Uint8Array(seed);
|
|
2203
|
-
const publicKey =
|
|
2217
|
+
const publicKey = hdWallet().curves.publicKeyFromPrivate(privateKey, Curve.P256);
|
|
2204
2218
|
return {
|
|
2205
2219
|
privateKey,
|
|
2206
2220
|
publicKey: new Uint8Array(publicKey),
|
|
@@ -2493,7 +2507,7 @@ function login(keys) {
|
|
|
2493
2507
|
try {
|
|
2494
2508
|
const sdnSigning = getSigningKey(state.hdRoot, 0, 0, 0);
|
|
2495
2509
|
const sdnPrivKey = sdnSigning.privateKey;
|
|
2496
|
-
const sdnPubKey = ed25519.
|
|
2510
|
+
const sdnPubKey = hdWallet().curves.ed25519.publicKeyFromSeed(sdnPrivKey);
|
|
2497
2511
|
// Don't keep derived private key bytes around longer than needed.
|
|
2498
2512
|
if (sdnPrivKey instanceof Uint8Array) sdnPrivKey.fill(0);
|
|
2499
2513
|
const xpub = state.hdRoot.toXpub();
|
|
@@ -2507,7 +2521,7 @@ function login(keys) {
|
|
|
2507
2521
|
: message;
|
|
2508
2522
|
const signing = getSigningKey(state.hdRoot, 0, 0, 0);
|
|
2509
2523
|
try {
|
|
2510
|
-
return ed25519.sign(msgBytes, signing.privateKey);
|
|
2524
|
+
return hdWallet().curves.ed25519.sign(msgBytes, signing.privateKey);
|
|
2511
2525
|
} finally {
|
|
2512
2526
|
if (signing?.privateKey instanceof Uint8Array) signing.privateKey.fill(0);
|
|
2513
2527
|
}
|
|
@@ -3400,7 +3414,7 @@ function signVCard(vcardText) {
|
|
|
3400
3414
|
|
|
3401
3415
|
const body = getSignableBody(vcardText);
|
|
3402
3416
|
const messageBytes = new TextEncoder().encode(body);
|
|
3403
|
-
const signature = ed25519.sign(messageBytes, state.wallet.ed25519.privateKey);
|
|
3417
|
+
const signature = hdWallet().curves.ed25519.sign(messageBytes, state.wallet.ed25519.privateKey);
|
|
3404
3418
|
const sigB64 = toBase64(signature);
|
|
3405
3419
|
|
|
3406
3420
|
// Encode signature + derivation path (coinType=501, account=0, index=0)
|
|
@@ -3491,7 +3505,7 @@ function verifyVCardSignature(vcardText) {
|
|
|
3491
3505
|
const pubKeyBytes = Uint8Array.from(atob(ed25519PubB64), c => c.charCodeAt(0));
|
|
3492
3506
|
|
|
3493
3507
|
try {
|
|
3494
|
-
const valid = ed25519.verify(
|
|
3508
|
+
const valid = hdWallet().curves.ed25519.verify(messageBytes, sigBytes, pubKeyBytes);
|
|
3495
3509
|
return { verified: valid, path, publicKey: ed25519PubB64, error: valid ? null : 'Signature invalid' };
|
|
3496
3510
|
} catch (e) {
|
|
3497
3511
|
return { verified: false, path, publicKey: ed25519PubB64, error: e.message };
|
|
@@ -5942,13 +5956,12 @@ export async function init(rootElement, options = {}) {
|
|
|
5942
5956
|
|
|
5943
5957
|
// Auto-login with saved PKI keys if no stored wallet
|
|
5944
5958
|
if (hasSavedKeys && !hasStoredWallet) {
|
|
5945
|
-
const tempEd25519Seed =
|
|
5946
|
-
crypto.getRandomValues(tempEd25519Seed);
|
|
5959
|
+
const tempEd25519Seed = hdWallet().utils.getRandomBytes(32);
|
|
5947
5960
|
const tempKeys = {
|
|
5948
5961
|
x25519: generateKeyPair(Curve.X25519),
|
|
5949
5962
|
ed25519: {
|
|
5950
5963
|
privateKey: tempEd25519Seed,
|
|
5951
|
-
publicKey: ed25519.
|
|
5964
|
+
publicKey: hdWallet().curves.ed25519.publicKeyFromSeed(tempEd25519Seed),
|
|
5952
5965
|
},
|
|
5953
5966
|
secp256k1: generateKeyPair(Curve.SECP256K1),
|
|
5954
5967
|
p256: await p256GenerateKeyPairAsync(),
|
package/src/wallet-storage.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* @module wallet-storage
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import initHDWallet from 'hd-wallet-wasm';
|
|
16
|
+
|
|
15
17
|
// =============================================================================
|
|
16
18
|
// Storage Keys
|
|
17
19
|
// =============================================================================
|
|
@@ -31,6 +33,23 @@ const AES_GCM_IV_LENGTH = 12;
|
|
|
31
33
|
// (This is still not a substitute for rate-limiting when an attacker can query online.)
|
|
32
34
|
const PIN_PBKDF2_ITERATIONS = 600000;
|
|
33
35
|
const LEGACY_PIN_PBKDF2_ITERATIONS = 100000;
|
|
36
|
+
const AES_GCM_TAG_LENGTH = 16;
|
|
37
|
+
|
|
38
|
+
let walletPromise = null;
|
|
39
|
+
|
|
40
|
+
function getWallet() {
|
|
41
|
+
if (!walletPromise) walletPromise = initHDWallet();
|
|
42
|
+
return walletPromise;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function asUint8Array(value) {
|
|
46
|
+
if (value instanceof Uint8Array) return value;
|
|
47
|
+
if (ArrayBuffer.isView(value)) {
|
|
48
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
49
|
+
}
|
|
50
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
51
|
+
throw new Error('Expected byte array');
|
|
52
|
+
}
|
|
34
53
|
|
|
35
54
|
// =============================================================================
|
|
36
55
|
// Storage Method Enum
|
|
@@ -73,10 +92,9 @@ function base64ToUint8Array(base64) {
|
|
|
73
92
|
/**
|
|
74
93
|
* Generate cryptographically secure random bytes
|
|
75
94
|
*/
|
|
76
|
-
function generateRandomBytes(length) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
return bytes;
|
|
95
|
+
async function generateRandomBytes(length) {
|
|
96
|
+
const wallet = await getWallet();
|
|
97
|
+
return wallet.utils.getRandomBytes(length);
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
// =============================================================================
|
|
@@ -95,29 +113,9 @@ function generateRandomBytes(length) {
|
|
|
95
113
|
*/
|
|
96
114
|
async function hkdfDerive(inputKeyMaterial, salt, info, length) {
|
|
97
115
|
const encoder = new TextEncoder();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
'raw',
|
|
102
|
-
inputKeyMaterial,
|
|
103
|
-
'HKDF',
|
|
104
|
-
false,
|
|
105
|
-
['deriveBits']
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
// Derive bits using HKDF
|
|
109
|
-
const derivedBits = await crypto.subtle.deriveBits(
|
|
110
|
-
{
|
|
111
|
-
name: 'HKDF',
|
|
112
|
-
hash: 'SHA-256',
|
|
113
|
-
salt: salt,
|
|
114
|
-
info: encoder.encode(info)
|
|
115
|
-
},
|
|
116
|
-
keyMaterial,
|
|
117
|
-
length * 8
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
return new Uint8Array(derivedBits);
|
|
116
|
+
const wallet = await getWallet();
|
|
117
|
+
const infoBytes = typeof info === 'string' ? encoder.encode(info) : asUint8Array(info);
|
|
118
|
+
return wallet.utils.hkdf(asUint8Array(inputKeyMaterial), asUint8Array(salt), infoBytes, length);
|
|
121
119
|
}
|
|
122
120
|
|
|
123
121
|
/**
|
|
@@ -174,31 +172,11 @@ async function deriveKeyFromPIN(pin, storedSalt, iterations = PIN_PBKDF2_ITERATI
|
|
|
174
172
|
const pinBytes = encoder.encode(pin);
|
|
175
173
|
|
|
176
174
|
// Use stored salt or generate new one
|
|
177
|
-
const salt = storedSalt || generateRandomBytes(16);
|
|
178
|
-
|
|
179
|
-
// Import PIN as key material
|
|
180
|
-
const keyMaterial = await crypto.subtle.importKey(
|
|
181
|
-
'raw',
|
|
182
|
-
pinBytes,
|
|
183
|
-
'PBKDF2',
|
|
184
|
-
false,
|
|
185
|
-
['deriveBits']
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// Use PBKDF2 with high iteration count for PIN (since PINs have low entropy)
|
|
189
|
-
const derivedBits = await crypto.subtle.deriveBits(
|
|
190
|
-
{
|
|
191
|
-
name: 'PBKDF2',
|
|
192
|
-
hash: 'SHA-256',
|
|
193
|
-
salt: salt,
|
|
194
|
-
iterations
|
|
195
|
-
},
|
|
196
|
-
keyMaterial,
|
|
197
|
-
256
|
|
198
|
-
);
|
|
175
|
+
const salt = storedSalt || await generateRandomBytes(16);
|
|
176
|
+
const wallet = await getWallet();
|
|
199
177
|
|
|
200
178
|
return {
|
|
201
|
-
keyMaterial:
|
|
179
|
+
keyMaterial: wallet.utils.pbkdf2(pinBytes, salt, iterations, 32),
|
|
202
180
|
salt
|
|
203
181
|
};
|
|
204
182
|
}
|
|
@@ -236,7 +214,7 @@ export async function isPRFLikelySupported() {
|
|
|
236
214
|
/**
|
|
237
215
|
* Generate WebAuthn challenge
|
|
238
216
|
*/
|
|
239
|
-
function generateChallenge() {
|
|
217
|
+
async function generateChallenge() {
|
|
240
218
|
return generateRandomBytes(32);
|
|
241
219
|
}
|
|
242
220
|
|
|
@@ -274,8 +252,8 @@ export async function registerPasskey(options = {}) {
|
|
|
274
252
|
userDisplayName = 'Wallet User'
|
|
275
253
|
} = options;
|
|
276
254
|
|
|
277
|
-
const challenge = generateChallenge();
|
|
278
|
-
const userId = generateRandomBytes(16);
|
|
255
|
+
const challenge = await generateChallenge();
|
|
256
|
+
const userId = await generateRandomBytes(16);
|
|
279
257
|
const prfInputs = createPRFInputs();
|
|
280
258
|
|
|
281
259
|
const publicKeyCredentialCreationOptions = {
|
|
@@ -327,9 +305,7 @@ export async function registerPasskey(options = {}) {
|
|
|
327
305
|
// PRF not available — derive key material from credential ID + a fixed salt.
|
|
328
306
|
const rawId = new Uint8Array(credential.rawId);
|
|
329
307
|
const salt = new TextEncoder().encode('wallet-storage-credid-fallback-v1');
|
|
330
|
-
|
|
331
|
-
const bits = await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info: new Uint8Array(0) }, base, 256);
|
|
332
|
-
keyMaterial = new Uint8Array(bits);
|
|
308
|
+
keyMaterial = await hkdfDerive(rawId, salt, new Uint8Array(0), 32);
|
|
333
309
|
}
|
|
334
310
|
|
|
335
311
|
return {
|
|
@@ -350,7 +326,7 @@ export async function authenticatePasskey(credentialId) {
|
|
|
350
326
|
throw new Error('Passkeys are not supported on this device');
|
|
351
327
|
}
|
|
352
328
|
|
|
353
|
-
const challenge = generateChallenge();
|
|
329
|
+
const challenge = await generateChallenge();
|
|
354
330
|
const prfInputs = createPRFInputs();
|
|
355
331
|
const credentialIdBytes = base64ToUint8Array(credentialId);
|
|
356
332
|
|
|
@@ -390,9 +366,7 @@ export async function authenticatePasskey(credentialId) {
|
|
|
390
366
|
// PRF not available — derive key material from credential ID + a fixed salt.
|
|
391
367
|
const rawId = new Uint8Array(assertion.rawId);
|
|
392
368
|
const salt = new TextEncoder().encode('wallet-storage-credid-fallback-v1');
|
|
393
|
-
|
|
394
|
-
const bits = await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info: new Uint8Array(0) }, base, 256);
|
|
395
|
-
keyMaterial = new Uint8Array(bits);
|
|
369
|
+
keyMaterial = await hkdfDerive(rawId, salt, new Uint8Array(0), 32);
|
|
396
370
|
}
|
|
397
371
|
|
|
398
372
|
return { keyMaterial, hasPRF };
|
|
@@ -408,47 +382,37 @@ export async function authenticatePasskey(credentialId) {
|
|
|
408
382
|
async function encryptData(data, encryptionKey, aad) {
|
|
409
383
|
const encoder = new TextEncoder();
|
|
410
384
|
const plaintext = encoder.encode(JSON.stringify(data));
|
|
411
|
-
const iv = generateRandomBytes(AES_GCM_IV_LENGTH);
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
['encrypt']
|
|
385
|
+
const iv = await generateRandomBytes(AES_GCM_IV_LENGTH);
|
|
386
|
+
const wallet = await getWallet();
|
|
387
|
+
const { ciphertext, tag } = wallet.utils.aesGcm.encrypt(
|
|
388
|
+
asUint8Array(encryptionKey),
|
|
389
|
+
plaintext,
|
|
390
|
+
iv,
|
|
391
|
+
aad ? asUint8Array(aad) : new Uint8Array(0)
|
|
419
392
|
);
|
|
393
|
+
const sealed = new Uint8Array(ciphertext.length + tag.length);
|
|
394
|
+
sealed.set(ciphertext, 0);
|
|
395
|
+
sealed.set(tag, ciphertext.length);
|
|
420
396
|
|
|
421
|
-
|
|
422
|
-
if (aad) alg.additionalData = aad;
|
|
423
|
-
|
|
424
|
-
const ciphertext = await crypto.subtle.encrypt(
|
|
425
|
-
alg,
|
|
426
|
-
cryptoKey,
|
|
427
|
-
plaintext
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
return { iv, ciphertext: new Uint8Array(ciphertext) };
|
|
397
|
+
return { iv, ciphertext: sealed };
|
|
431
398
|
}
|
|
432
399
|
|
|
433
400
|
/**
|
|
434
401
|
* Decrypt data using AES-256-GCM
|
|
435
402
|
*/
|
|
436
403
|
async function decryptData(ciphertext, encryptionKey, iv, aad) {
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
false,
|
|
442
|
-
['decrypt']
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
const alg = { name: 'AES-GCM', iv };
|
|
446
|
-
if (aad) alg.additionalData = aad;
|
|
404
|
+
const sealed = asUint8Array(ciphertext);
|
|
405
|
+
if (sealed.length <= AES_GCM_TAG_LENGTH) {
|
|
406
|
+
throw new Error('Ciphertext is too short');
|
|
407
|
+
}
|
|
447
408
|
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
409
|
+
const wallet = await getWallet();
|
|
410
|
+
const plaintext = wallet.utils.aesGcm.decrypt(
|
|
411
|
+
asUint8Array(encryptionKey),
|
|
412
|
+
sealed.subarray(0, sealed.length - AES_GCM_TAG_LENGTH),
|
|
413
|
+
sealed.subarray(sealed.length - AES_GCM_TAG_LENGTH),
|
|
414
|
+
asUint8Array(iv),
|
|
415
|
+
aad ? asUint8Array(aad) : new Uint8Array(0)
|
|
452
416
|
);
|
|
453
417
|
|
|
454
418
|
const decoder = new TextDecoder();
|