ufsecp 3.21.1 → 3.50.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 +55 -1
- package/lib/ufsecp.js +249 -72
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/libufsecp.dylib +0 -0
- package/prebuilds/linux-x64/libufsecp.so +0 -0
- package/prebuilds/linux-x64/libufsecp.so.3 +0 -0
- package/prebuilds/linux-x64/libufsecp.so.3.50.0 +0 -0
- package/prebuilds/win32-x64/ufsecp.dll +0 -0
- package/prebuilds/linux-arm64/libufsecp.so +0 -0
- package/prebuilds/linux-arm64/libufsecp.so.3 +0 -0
- package/prebuilds/linux-arm64/libufsecp.so.3.21.1 +0 -0
- package/prebuilds/linux-x64/libufsecp.so.3.21.1 +0 -0
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
|
|
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} [
|
|
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(
|
|
161
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
this.
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
this.
|
|
201
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
|
|
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) {
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
this.
|
|
226
|
-
|
|
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
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
return
|
|
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
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
return
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
this.
|
|
249
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
return
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
this.
|
|
309
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
this.
|
|
316
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
this.
|
|
323
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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 (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
this.
|
|
431
|
-
|
|
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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|