ufsecp 3.22.0 → 3.63.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/README.md CHANGED
@@ -12,6 +12,20 @@ High-performance Node.js native addon for secp256k1 elliptic curve cryptography,
12
12
  - **Addresses** -- P2PKH, P2WPKH, P2TR
13
13
  - **WIF** -- encode/decode
14
14
  - **Hashing** -- SHA-256 (hardware-accelerated), HASH160, tagged hash
15
+ - **Key tweaking** -- negate, add, multiply
16
+ - **Ethereum** -- Keccak-256, EIP-55 addresses, EIP-155 sign, ecrecover
17
+ - **BIP-39** -- mnemonic generation, validation, seed derivation
18
+ - **Multi-coin wallet** -- 7-coin address dispatch (BTC/LTC/DOGE/DASH/ETH/BCH/TRX)
19
+ - **Batch verification** -- ECDSA + Schnorr batch verify with invalid identification
20
+ - **MuSig2** -- BIP-327 multi-signatures (key agg, nonce gen, partial sign, aggregate)
21
+ - **FROST** -- threshold signatures (keygen, sign, aggregate, verify)
22
+ - **Adaptor signatures** -- Schnorr + ECDSA adaptor pre-sign, adapt, extract
23
+ - **Pedersen commitments** -- commit, verify, sum balance, switch commitments
24
+ - **ZK proofs** -- knowledge proof, DLEQ proof, Bulletproof range proof
25
+ - **Multi-scalar multiplication** -- Shamir's trick, MSM
26
+ - **Pubkey arithmetic** -- add, negate, combine N keys
27
+ - **SHA-512** -- full SHA-512 hash
28
+ - **Message signing** -- BIP-137 Bitcoin message sign/verify
15
29
 
16
30
  ## Install
17
31
 
@@ -110,9 +124,43 @@ const { outputKeyX, parity } = secp.taprootOutputKey(xOnlyPub);
110
124
  const tweakedPriv = secp.taprootTweakPrivkey(privkey);
111
125
  ```
112
126
 
127
+ ## Security Notes
128
+
129
+ - Secret-bearing operations in the stable `ufsecp_*` C ABI route through the
130
+ native constant-time CPU paths. Public verification and serialization routes
131
+ stay on the native fast paths.
132
+ - JavaScript cannot guarantee full secret erasure. V8/Node may keep Buffer
133
+ copies alive longer than expected, and helper methods may create additional
134
+ copies in JS memory.
135
+ - Treat caller-owned `Buffer` and `Uint8Array` objects as sensitive material:
136
+ minimize copies, overwrite them after use, and avoid long-lived references.
137
+ - If you use the lower-level context-based wrapper in `lib/ufsecp.js`, destroy
138
+ each context explicitly and do not share one context across worker threads
139
+ without external synchronization.
140
+ - To minimize human error, the Node bindings now support `autoZeroInputs: true`
141
+ and `SensitiveBytes.take(...)`. In secure mode, secret-bearing input buffers
142
+ are wiped in a `finally` block after the native call returns, even on error.
143
+ - This is still best-effort hygiene, not a formal guarantee against all JS/V8
144
+ copies. It reduces operational mistakes, but it cannot erase unknown aliases
145
+ or already-copied data elsewhere in the process.
146
+
147
+ ### Secure Usage
148
+
149
+ ```js
150
+ const { Secp256k1, SensitiveBytes } = require('ultrafast-secp256k1');
151
+ const crypto = require('crypto');
152
+
153
+ const secp = new Secp256k1({ autoZeroInputs: true });
154
+ const privkey = SensitiveBytes.take(crypto.randomBytes(32));
155
+ const msgHash = crypto.randomBytes(32);
156
+
157
+ const sig = secp.ecdsaSign(msgHash, privkey);
158
+ // privkey is wiped automatically in the finally path
159
+ ```
160
+
113
161
  ## Architecture Note
114
162
 
115
- Built on hand-optimized C/C++ with platform-specific acceleration (AVX2, SHA-NI, BMI2 on x86; NEON on ARM). The C ABI layer uses the **fast** (variable-time) implementation for maximum throughput. A constant-time (CT) layer with identical mathematical operations is available via the C++ headers for applications requiring timing-attack resistance.
163
+ Built on hand-optimized C/C++ with platform-specific acceleration (AVX2, SHA-NI, BMI2 on x86; NEON on ARM). The native library contains both fast and constant-time (CT) paths. In the stable `ufsecp_*` C ABI, secret-bearing CPU operations are routed through CT internals, while public verification/serialization operations stay on the optimized fast paths. This improves native-side timing safety, but it does not eliminate caller-side secret-copy risks in JavaScript memory.
116
164
 
117
165
  | Operation | x86-64 | ARM64 | RISC-V |
118
166
  |-----------|--------|-------|--------|
@@ -136,6 +184,12 @@ cmake -S . -B build -DSECP256K1_GLV_WINDOW_WIDTH=6
136
184
 
137
185
  See [docs/PERFORMANCE_GUIDE.md](../../docs/PERFORMANCE_GUIDE.md) for detailed benchmarks and per-platform tuning advice.
138
186
 
187
+ ## Smoke Validation
188
+
189
+ ```bash
190
+ bash libs/UltrafastSecp256k1/scripts/validate_bindings.sh
191
+ ```
192
+
139
193
  ## License
140
194
 
141
195
  MIT
package/lib/ufsecp.js CHANGED
@@ -4,6 +4,14 @@
4
4
  * High-performance secp256k1 elliptic curve cryptography with dual-layer
5
5
  * constant-time architecture. Context-based API.
6
6
  *
7
+ * Security notes:
8
+ * - Each context is single-thread; create one per worker/thread or
9
+ * synchronize externally.
10
+ * - Call destroy() when finished. A FinalizationRegistry fallback exists,
11
+ * but explicit destroy remains the safe lifecycle contract.
12
+ * - Native code erases its transient secret buffers, but JavaScript callers
13
+ * must still wipe their own Buffer/Uint8Array copies.
14
+ *
7
15
  * Usage:
8
16
  * const { Ufsecp } = require('ufsecp');
9
17
  * const ctx = new Ufsecp();
@@ -24,6 +32,16 @@ const uint8Ptr = ref.refType(ref.types.uint8);
24
32
  const int32Ptr = ref.refType(ref.types.int32);
25
33
  const sizeTPtr = ref.refType(ref.types.size_t);
26
34
 
35
+ const CTX_FINALIZER = typeof FinalizationRegistry === 'function'
36
+ ? new FinalizationRegistry(({ lib, ctx }) => {
37
+ try {
38
+ lib.ufsecp_ctx_destroy(ctx);
39
+ } catch {
40
+ // Best-effort finalizer only.
41
+ }
42
+ })
43
+ : null;
44
+
27
45
  // ── Error codes ────────────────────────────────────────────────────────
28
46
 
29
47
  const UFSECP_OK = 0;
@@ -151,24 +169,115 @@ const LIB_SPEC = {
151
169
  const NET_MAINNET = 0;
152
170
  const NET_TESTNET = 1;
153
171
 
172
+ function bestEffortZero(buf) {
173
+ if (!Buffer.isBuffer(buf) && !(buf instanceof Uint8Array)) {
174
+ throw new TypeError('buf must be a Buffer or Uint8Array');
175
+ }
176
+ buf.fill(0);
177
+ return buf;
178
+ }
179
+
180
+ class SensitiveBytes {
181
+ constructor(buf, { copy = false } = {}) {
182
+ if (!Buffer.isBuffer(buf) && !(buf instanceof Uint8Array)) {
183
+ throw new TypeError('buf must be a Buffer or Uint8Array');
184
+ }
185
+ this._buf = copy
186
+ ? Buffer.from(buf)
187
+ : Buffer.isBuffer(buf)
188
+ ? buf
189
+ : Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
190
+ this._destroyed = false;
191
+ }
192
+
193
+ static take(buf) {
194
+ return new SensitiveBytes(buf, { copy: false });
195
+ }
196
+
197
+ static copy(buf) {
198
+ return new SensitiveBytes(buf, { copy: true });
199
+ }
200
+
201
+ get bytes() {
202
+ if (this._destroyed) {
203
+ throw new Error('SensitiveBytes already destroyed');
204
+ }
205
+ return this._buf;
206
+ }
207
+
208
+ destroy() {
209
+ if (!this._destroyed) {
210
+ bestEffortZero(this._buf);
211
+ this._destroyed = true;
212
+ }
213
+ }
214
+ }
215
+
216
+ if (typeof Symbol.dispose === 'symbol') {
217
+ SensitiveBytes.prototype[Symbol.dispose] = SensitiveBytes.prototype.destroy;
218
+ }
219
+
220
+ function _normalizeSecretInput(value) {
221
+ return value instanceof SensitiveBytes ? value.bytes : value;
222
+ }
223
+
224
+ function _wipeSecretInput(value) {
225
+ if (value instanceof SensitiveBytes) {
226
+ value.destroy();
227
+ return;
228
+ }
229
+ bestEffortZero(value);
230
+ }
231
+
232
+ function _ffiBytes(value) {
233
+ return value.length === 0 ? Buffer.alloc(1) : value;
234
+ }
235
+
236
+ function _parseConstructorArgs(libPathOrOptions, maybeOptions) {
237
+ if (typeof libPathOrOptions === 'string' || libPathOrOptions == null) {
238
+ return {
239
+ libPath: libPathOrOptions || undefined,
240
+ autoZeroInputs: Boolean(maybeOptions && maybeOptions.autoZeroInputs),
241
+ };
242
+ }
243
+ if (typeof libPathOrOptions === 'object') {
244
+ return {
245
+ libPath: libPathOrOptions.libPath,
246
+ autoZeroInputs: Boolean(libPathOrOptions.autoZeroInputs),
247
+ };
248
+ }
249
+ throw new TypeError('constructor expects a library path string or options object');
250
+ }
251
+
154
252
  // ── Ufsecp class ───────────────────────────────────────────────────────
155
253
 
156
254
  class Ufsecp {
157
255
  /**
158
- * @param {string} [libPath] Path to the ufsecp shared library.
256
+ * @param {string|object} [libPathOrOptions] Path to the ufsecp shared library,
257
+ * or options: { libPath?: string, autoZeroInputs?: boolean }.
258
+ * @param {object} [maybeOptions] Optional options when the first arg is a path.
159
259
  */
160
- constructor(libPath) {
161
- this._lib = ffi.Library(libPath || findLibrary(), LIB_SPEC);
260
+ constructor(libPathOrOptions, maybeOptions) {
261
+ const opts = _parseConstructorArgs(libPathOrOptions, maybeOptions);
262
+ this._lib = ffi.Library(opts.libPath || findLibrary(), LIB_SPEC);
162
263
  const pp = ref.alloc(voidPtr);
163
264
  const rc = this._lib.ufsecp_ctx_create(pp);
164
265
  if (rc !== UFSECP_OK) throw new UfsecpError('ctx_create', rc);
165
266
  this._ctx = pp.deref();
166
267
  this._destroyed = false;
268
+ this._autoZeroInputs = opts.autoZeroInputs;
269
+ this._finalizerToken = {};
270
+ if (CTX_FINALIZER) {
271
+ CTX_FINALIZER.register(this, { lib: this._lib, ctx: this._ctx }, this._finalizerToken);
272
+ }
167
273
  }
168
274
 
169
275
  /** Explicitly destroy the context. Safe to call multiple times. */
170
276
  destroy() {
171
277
  if (!this._destroyed && this._ctx) {
278
+ if (CTX_FINALIZER) {
279
+ CTX_FINALIZER.unregister(this._finalizerToken);
280
+ }
172
281
  this._lib.ufsecp_ctx_destroy(this._ctx);
173
282
  this._ctx = null;
174
283
  this._destroyed = true;
@@ -181,24 +290,30 @@ class Ufsecp {
181
290
  abiVersion() { return this._lib.ufsecp_abi_version(); }
182
291
  versionString() { return this._lib.ufsecp_version_string(); }
183
292
  lastError() { this._alive(); return this._lib.ufsecp_last_error(this._ctx); }
184
- lastErrorMsg() { this._alive(); return this._lib.ufsecp_last_error_msg(this._ctx); }
293
+ lastErrorMsg() { this._alive(); return String(this._lib.ufsecp_last_error_msg(this._ctx)); }
185
294
 
186
295
  // ── Key operations ────────────────────────────────────────────────────
187
296
 
188
297
  /** Compressed public key (33 bytes). */
189
298
  pubkeyCreate(privkey) {
190
- _chk(privkey, 32, 'privkey'); this._alive();
191
- const out = Buffer.alloc(33);
192
- this._throw(this._lib.ufsecp_pubkey_create(this._ctx, privkey, out), 'pubkey_create');
193
- return out;
299
+ const raw = _normalizeSecretInput(privkey);
300
+ _chk(raw, 32, 'privkey'); this._alive();
301
+ return this._withSecretCleanup([privkey], () => {
302
+ const out = Buffer.alloc(33);
303
+ this._throw(this._lib.ufsecp_pubkey_create(this._ctx, raw, out), 'pubkey_create');
304
+ return out;
305
+ });
194
306
  }
195
307
 
196
308
  /** Uncompressed public key (65 bytes). */
197
309
  pubkeyCreateUncompressed(privkey) {
198
- _chk(privkey, 32, 'privkey'); this._alive();
199
- const out = Buffer.alloc(65);
200
- this._throw(this._lib.ufsecp_pubkey_create_uncompressed(this._ctx, privkey, out), 'pubkey_create_uncompressed');
201
- return out;
310
+ const raw = _normalizeSecretInput(privkey);
311
+ _chk(raw, 32, 'privkey'); this._alive();
312
+ return this._withSecretCleanup([privkey], () => {
313
+ const out = Buffer.alloc(65);
314
+ this._throw(this._lib.ufsecp_pubkey_create_uncompressed(this._ctx, raw, out), 'pubkey_create_uncompressed');
315
+ return out;
316
+ });
202
317
  }
203
318
 
204
319
  /** Parse compressed/uncompressed → compressed 33 bytes. */
@@ -211,42 +326,63 @@ class Ufsecp {
211
326
 
212
327
  /** X-only (32 bytes, BIP-340) from private key. */
213
328
  pubkeyXonly(privkey) {
214
- _chk(privkey, 32, 'privkey'); this._alive();
215
- const out = Buffer.alloc(32);
216
- this._throw(this._lib.ufsecp_pubkey_xonly(this._ctx, privkey, out), 'pubkey_xonly');
217
- return out;
329
+ const raw = _normalizeSecretInput(privkey);
330
+ _chk(raw, 32, 'privkey'); this._alive();
331
+ return this._withSecretCleanup([privkey], () => {
332
+ const out = Buffer.alloc(32);
333
+ this._throw(this._lib.ufsecp_pubkey_xonly(this._ctx, raw, out), 'pubkey_xonly');
334
+ return out;
335
+ });
218
336
  }
219
337
 
220
- seckeyVerify(privkey) { _chk(privkey, 32, 'privkey'); this._alive(); return this._lib.ufsecp_seckey_verify(this._ctx, privkey) === UFSECP_OK; }
338
+ seckeyVerify(privkey) {
339
+ const raw = _normalizeSecretInput(privkey);
340
+ _chk(raw, 32, 'privkey'); this._alive();
341
+ return this._withSecretCleanup([privkey], () => this._lib.ufsecp_seckey_verify(this._ctx, raw) === UFSECP_OK);
342
+ }
221
343
 
222
344
  seckeyNegate(privkey) {
223
- _chk(privkey, 32, 'privkey'); this._alive();
224
- const buf = Buffer.from(privkey);
225
- this._throw(this._lib.ufsecp_seckey_negate(this._ctx, buf), 'seckey_negate');
226
- return buf;
345
+ const raw = _normalizeSecretInput(privkey);
346
+ _chk(raw, 32, 'privkey'); this._alive();
347
+ return this._withSecretCleanup([privkey], () => {
348
+ const buf = Buffer.from(raw);
349
+ this._throw(this._lib.ufsecp_seckey_negate(this._ctx, buf), 'seckey_negate');
350
+ return buf;
351
+ });
227
352
  }
228
353
 
229
354
  seckeyTweakAdd(privkey, tweak) {
230
- _chk(privkey, 32, 'privkey'); _chk(tweak, 32, 'tweak'); this._alive();
231
- const buf = Buffer.from(privkey);
232
- this._throw(this._lib.ufsecp_seckey_tweak_add(this._ctx, buf, tweak), 'seckey_tweak_add');
233
- return buf;
355
+ const rawPrivkey = _normalizeSecretInput(privkey);
356
+ const rawTweak = _normalizeSecretInput(tweak);
357
+ _chk(rawPrivkey, 32, 'privkey'); _chk(rawTweak, 32, 'tweak'); this._alive();
358
+ return this._withSecretCleanup([privkey, tweak], () => {
359
+ const buf = Buffer.from(rawPrivkey);
360
+ this._throw(this._lib.ufsecp_seckey_tweak_add(this._ctx, buf, rawTweak), 'seckey_tweak_add');
361
+ return buf;
362
+ });
234
363
  }
235
364
 
236
365
  seckeyTweakMul(privkey, tweak) {
237
- _chk(privkey, 32, 'privkey'); _chk(tweak, 32, 'tweak'); this._alive();
238
- const buf = Buffer.from(privkey);
239
- this._throw(this._lib.ufsecp_seckey_tweak_mul(this._ctx, buf, tweak), 'seckey_tweak_mul');
240
- return buf;
366
+ const rawPrivkey = _normalizeSecretInput(privkey);
367
+ const rawTweak = _normalizeSecretInput(tweak);
368
+ _chk(rawPrivkey, 32, 'privkey'); _chk(rawTweak, 32, 'tweak'); this._alive();
369
+ return this._withSecretCleanup([privkey, tweak], () => {
370
+ const buf = Buffer.from(rawPrivkey);
371
+ this._throw(this._lib.ufsecp_seckey_tweak_mul(this._ctx, buf, rawTweak), 'seckey_tweak_mul');
372
+ return buf;
373
+ });
241
374
  }
242
375
 
243
376
  // ── ECDSA ─────────────────────────────────────────────────────────────
244
377
 
245
378
  ecdsaSign(msgHash, privkey) {
246
- _chk(msgHash, 32, 'msgHash'); _chk(privkey, 32, 'privkey'); this._alive();
247
- const sig = Buffer.alloc(64);
248
- this._throw(this._lib.ufsecp_ecdsa_sign(this._ctx, msgHash, privkey, sig), 'ecdsa_sign');
249
- return sig;
379
+ const rawPrivkey = _normalizeSecretInput(privkey);
380
+ _chk(msgHash, 32, 'msgHash'); _chk(rawPrivkey, 32, 'privkey'); this._alive();
381
+ return this._withSecretCleanup([privkey], () => {
382
+ const sig = Buffer.alloc(64);
383
+ this._throw(this._lib.ufsecp_ecdsa_sign(this._ctx, msgHash, rawPrivkey, sig), 'ecdsa_sign');
384
+ return sig;
385
+ });
250
386
  }
251
387
 
252
388
  ecdsaVerify(msgHash, sig, pubkey) {
@@ -272,11 +408,14 @@ class Ufsecp {
272
408
  // ── Recovery ──────────────────────────────────────────────────────────
273
409
 
274
410
  ecdsaSignRecoverable(msgHash, privkey) {
275
- _chk(msgHash, 32, 'msgHash'); _chk(privkey, 32, 'privkey'); this._alive();
276
- const sig = Buffer.alloc(64);
277
- const recid = ref.alloc('int');
278
- this._throw(this._lib.ufsecp_ecdsa_sign_recoverable(this._ctx, msgHash, privkey, sig, recid), 'ecdsa_sign_recoverable');
279
- return { signature: sig, recoveryId: recid.deref() };
411
+ const rawPrivkey = _normalizeSecretInput(privkey);
412
+ _chk(msgHash, 32, 'msgHash'); _chk(rawPrivkey, 32, 'privkey'); this._alive();
413
+ return this._withSecretCleanup([privkey], () => {
414
+ const sig = Buffer.alloc(64);
415
+ const recid = ref.alloc('int');
416
+ this._throw(this._lib.ufsecp_ecdsa_sign_recoverable(this._ctx, msgHash, rawPrivkey, sig, recid), 'ecdsa_sign_recoverable');
417
+ return { signature: sig, recoveryId: recid.deref() };
418
+ });
280
419
  }
281
420
 
282
421
  ecdsaRecover(msgHash, sig, recid) {
@@ -289,10 +428,14 @@ class Ufsecp {
289
428
  // ── Schnorr ───────────────────────────────────────────────────────────
290
429
 
291
430
  schnorrSign(msg, privkey, auxRand) {
292
- _chk(msg, 32, 'msg'); _chk(privkey, 32, 'privkey'); _chk(auxRand, 32, 'auxRand'); this._alive();
293
- const sig = Buffer.alloc(64);
294
- this._throw(this._lib.ufsecp_schnorr_sign(this._ctx, msg, privkey, auxRand, sig), 'schnorr_sign');
295
- return sig;
431
+ const rawPrivkey = _normalizeSecretInput(privkey);
432
+ const rawAuxRand = _normalizeSecretInput(auxRand);
433
+ _chk(msg, 32, 'msg'); _chk(rawPrivkey, 32, 'privkey'); _chk(rawAuxRand, 32, 'auxRand'); this._alive();
434
+ return this._withSecretCleanup([privkey, auxRand], () => {
435
+ const sig = Buffer.alloc(64);
436
+ this._throw(this._lib.ufsecp_schnorr_sign(this._ctx, msg, rawPrivkey, rawAuxRand, sig), 'schnorr_sign');
437
+ return sig;
438
+ });
296
439
  }
297
440
 
298
441
  schnorrVerify(msg, sig, pubkeyX) {
@@ -303,43 +446,52 @@ class Ufsecp {
303
446
  // ── ECDH ──────────────────────────────────────────────────────────────
304
447
 
305
448
  ecdh(privkey, pubkey) {
306
- _chk(privkey, 32, 'privkey'); _chk(pubkey, 33, 'pubkey'); this._alive();
307
- const out = Buffer.alloc(32);
308
- this._throw(this._lib.ufsecp_ecdh(this._ctx, privkey, pubkey, out), 'ecdh');
309
- return out;
449
+ const rawPrivkey = _normalizeSecretInput(privkey);
450
+ _chk(rawPrivkey, 32, 'privkey'); _chk(pubkey, 33, 'pubkey'); this._alive();
451
+ return this._withSecretCleanup([privkey], () => {
452
+ const out = Buffer.alloc(32);
453
+ this._throw(this._lib.ufsecp_ecdh(this._ctx, rawPrivkey, pubkey, out), 'ecdh');
454
+ return out;
455
+ });
310
456
  }
311
457
 
312
458
  ecdhXonly(privkey, pubkey) {
313
- _chk(privkey, 32, 'privkey'); _chk(pubkey, 33, 'pubkey'); this._alive();
314
- const out = Buffer.alloc(32);
315
- this._throw(this._lib.ufsecp_ecdh_xonly(this._ctx, privkey, pubkey, out), 'ecdh_xonly');
316
- return out;
459
+ const rawPrivkey = _normalizeSecretInput(privkey);
460
+ _chk(rawPrivkey, 32, 'privkey'); _chk(pubkey, 33, 'pubkey'); this._alive();
461
+ return this._withSecretCleanup([privkey], () => {
462
+ const out = Buffer.alloc(32);
463
+ this._throw(this._lib.ufsecp_ecdh_xonly(this._ctx, rawPrivkey, pubkey, out), 'ecdh_xonly');
464
+ return out;
465
+ });
317
466
  }
318
467
 
319
468
  ecdhRaw(privkey, pubkey) {
320
- _chk(privkey, 32, 'privkey'); _chk(pubkey, 33, 'pubkey'); this._alive();
321
- const out = Buffer.alloc(32);
322
- this._throw(this._lib.ufsecp_ecdh_raw(this._ctx, privkey, pubkey, out), 'ecdh_raw');
323
- return out;
469
+ const rawPrivkey = _normalizeSecretInput(privkey);
470
+ _chk(rawPrivkey, 32, 'privkey'); _chk(pubkey, 33, 'pubkey'); this._alive();
471
+ return this._withSecretCleanup([privkey], () => {
472
+ const out = Buffer.alloc(32);
473
+ this._throw(this._lib.ufsecp_ecdh_raw(this._ctx, rawPrivkey, pubkey, out), 'ecdh_raw');
474
+ return out;
475
+ });
324
476
  }
325
477
 
326
478
  // ── Hashing ───────────────────────────────────────────────────────────
327
479
 
328
480
  sha256(data) {
329
481
  const out = Buffer.alloc(32);
330
- this._throw(this._lib.ufsecp_sha256(data, data.length, out), 'sha256');
482
+ this._throw(this._lib.ufsecp_sha256(_ffiBytes(data), data.length, out), 'sha256');
331
483
  return out;
332
484
  }
333
485
 
334
486
  hash160(data) {
335
487
  const out = Buffer.alloc(20);
336
- this._throw(this._lib.ufsecp_hash160(data, data.length, out), 'hash160');
488
+ this._throw(this._lib.ufsecp_hash160(_ffiBytes(data), data.length, out), 'hash160');
337
489
  return out;
338
490
  }
339
491
 
340
492
  taggedHash(tag, data) {
341
493
  const out = Buffer.alloc(32);
342
- this._throw(this._lib.ufsecp_tagged_hash(tag, data, data.length, out), 'tagged_hash');
494
+ this._throw(this._lib.ufsecp_tagged_hash(tag, _ffiBytes(data), data.length, out), 'tagged_hash');
343
495
  return out;
344
496
  }
345
497
 
@@ -360,11 +512,14 @@ class Ufsecp {
360
512
  // ── WIF ───────────────────────────────────────────────────────────────
361
513
 
362
514
  wifEncode(privkey, compressed = true, network = NET_MAINNET) {
363
- _chk(privkey, 32, 'privkey'); this._alive();
364
- const buf = Buffer.alloc(128);
365
- const len = ref.alloc('size_t', 128);
366
- this._throw(this._lib.ufsecp_wif_encode(this._ctx, privkey, compressed ? 1 : 0, network, buf, len), 'wif_encode');
367
- return buf.toString('utf8', 0, len.deref());
515
+ const rawPrivkey = _normalizeSecretInput(privkey);
516
+ _chk(rawPrivkey, 32, 'privkey'); this._alive();
517
+ return this._withSecretCleanup([privkey], () => {
518
+ const buf = Buffer.alloc(128);
519
+ const len = ref.alloc('size_t', 128);
520
+ this._throw(this._lib.ufsecp_wif_encode(this._ctx, rawPrivkey, compressed ? 1 : 0, network, buf, len), 'wif_encode');
521
+ return buf.toString('utf8', 0, len.deref());
522
+ });
368
523
  }
369
524
 
370
525
  wifDecode(wif) {
@@ -379,11 +534,14 @@ class Ufsecp {
379
534
  // ── BIP-32 ────────────────────────────────────────────────────────────
380
535
 
381
536
  bip32Master(seed) {
537
+ const raw = _normalizeSecretInput(seed);
382
538
  this._alive();
383
- if (seed.length < 16 || seed.length > 64) throw new RangeError('Seed must be 16-64 bytes');
384
- const key = Buffer.alloc(82);
385
- this._throw(this._lib.ufsecp_bip32_master(this._ctx, seed, seed.length, key), 'bip32_master');
386
- return key;
539
+ if (raw.length < 16 || raw.length > 64) throw new RangeError('Seed must be 16-64 bytes');
540
+ return this._withSecretCleanup([seed], () => {
541
+ const key = Buffer.alloc(82);
542
+ this._throw(this._lib.ufsecp_bip32_master(this._ctx, raw, raw.length, key), 'bip32_master');
543
+ return key;
544
+ });
387
545
  }
388
546
 
389
547
  bip32Derive(parent, index) {
@@ -425,10 +583,13 @@ class Ufsecp {
425
583
  }
426
584
 
427
585
  taprootTweakSeckey(privkey, merkleRoot = null) {
428
- _chk(privkey, 32, 'privkey'); this._alive();
429
- const out = Buffer.alloc(32);
430
- this._throw(this._lib.ufsecp_taproot_tweak_seckey(this._ctx, privkey, merkleRoot, out), 'taproot_tweak_seckey');
431
- return out;
586
+ const rawPrivkey = _normalizeSecretInput(privkey);
587
+ _chk(rawPrivkey, 32, 'privkey'); this._alive();
588
+ return this._withSecretCleanup([privkey], () => {
589
+ const out = Buffer.alloc(32);
590
+ this._throw(this._lib.ufsecp_taproot_tweak_seckey(this._ctx, rawPrivkey, merkleRoot, out), 'taproot_tweak_seckey');
591
+ return out;
592
+ });
432
593
  }
433
594
 
434
595
  taprootVerify(outputKeyX, parity, internalKeyX, merkleRoot = null) {
@@ -447,6 +608,18 @@ class Ufsecp {
447
608
  if (rc !== UFSECP_OK) throw new UfsecpError(op, rc);
448
609
  }
449
610
 
611
+ _withSecretCleanup(values, fn) {
612
+ try {
613
+ return fn();
614
+ } finally {
615
+ for (const value of values) {
616
+ if (value instanceof SensitiveBytes || (this._autoZeroInputs && value && (Buffer.isBuffer(value) || value instanceof Uint8Array))) {
617
+ _wipeSecretInput(value);
618
+ }
619
+ }
620
+ }
621
+ }
622
+
450
623
  _getAddr(fnName, key, network) {
451
624
  this._alive();
452
625
  const buf = Buffer.alloc(128);
@@ -456,13 +629,17 @@ class Ufsecp {
456
629
  }
457
630
  }
458
631
 
632
+ if (typeof Symbol.dispose === 'symbol') {
633
+ Ufsecp.prototype[Symbol.dispose] = Ufsecp.prototype.destroy;
634
+ }
635
+
459
636
  function _chk(buf, expected, name) {
460
637
  if (!Buffer.isBuffer(buf) && !(buf instanceof Uint8Array)) {
461
- throw new TypeError(`${name} must be a Buffer`);
638
+ throw new TypeError(`${name} must be a Buffer or Uint8Array`);
462
639
  }
463
640
  if (buf.length !== expected) {
464
641
  throw new RangeError(`${name} must be ${expected} bytes, got ${buf.length}`);
465
642
  }
466
643
  }
467
644
 
468
- module.exports = { Ufsecp, UfsecpError, NET_MAINNET, NET_TESTNET };
645
+ module.exports = { Ufsecp, UfsecpError, NET_MAINNET, NET_TESTNET, SensitiveBytes, bestEffortZero };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ufsecp",
3
- "version": "3.22.0",
3
+ "version": "3.63.0",
4
4
  "description": "Node.js bindings for UltrafastSecp256k1 — high-performance secp256k1 ECC (ufsecp C ABI v1)",
5
5
  "main": "lib/ufsecp.js",
6
6
  "files": ["lib/ufsecp.js", "prebuilds/", "README.md"],
Binary file
Binary file
Binary file
Binary file