Skip to content

Commit

Permalink
feat: add noble impl for fallback when BigInt is available
Browse files Browse the repository at this point in the history
  • Loading branch information
ChALkeR committed Nov 15, 2024
1 parent 358c039 commit a9356eb
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 3 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# secp256k1-node

This module provides native bindings to [bitcoin-core/secp256k1](https://github.com/bitcoin-core/secp256k1). In browser [elliptic](https://github.com/indutny/elliptic) will be used as fallback.
This module provides native bindings to [bitcoin-core/secp256k1](https://github.com/bitcoin-core/secp256k1).
In browser [noble](https://github.com/paulmillr/noble-secp256k1) or [elliptic](https://github.com/indutny/elliptic) will be used as fallback.

Works on node version 14.0.0 or greater, because use [N-API](https://nodejs.org/api/n-api.html).

Expand Down
1 change: 1 addition & 0 deletions benchmarks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const util = require('../test/util')
const implementations = {
bindings: require('../bindings'),
elliptic: require('../elliptic'),
noble: require('../noble'),
ecdsa: require('./ecdsa')
}

Expand Down
5 changes: 5 additions & 0 deletions browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if (typeof BigInt !== 'undefined') {
module.exports = require('./noble.js')
} else {
module.exports = require('./elliptic.js')
}
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
try {
module.exports = require('./bindings')
} catch (err) {
module.exports = require('./elliptic')
module.exports = require('./browser')
}
310 changes: 310 additions & 0 deletions lib/noble.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
const secp256k1 = require('@noble/secp256k1')
const { sha256 } = require('@noble/hashes/sha2')
const { hmac } = require('@noble/hashes/hmac')

if (!secp256k1.utils.hmacSha256Sync) {
secp256k1.utils.hmacSha256Sync = (key, ...msgs) => hmac(sha256, key, secp256k1.utils.concatBytes(...msgs))
}
if (!secp256k1.utils.sha256Sync) {
secp256k1.utils.sha256Sync = (...msgs) => sha256(secp256k1.utils.concatBytes(...msgs))
}

function writePublicKey(output, point) {
const buf = point.toRawBytes(output.length === 33)
if (output.length !== buf.length) return 1
output.set(buf)
return 0
}

function toBig(arr) {
// args already typechecked in ./lib/index.js
return BigInt('0x' + secp256k1.utils.bytesToHex(arr))
}

const _0n = BigInt(0)
const _1n = BigInt(1)

let elliptic // used for signing with nonce function and/or non-32 byte extra entropy data

module.exports = {
contextRandomize () {
return 0
},

privateKeyVerify (seckey) {
return secp256k1.utils.isValidPrivateKey(seckey) ? 0 : 1
},

// Validation matches ./elliptic.js
// Doesn't fail on out of bounds values, normalize them
privateKeyNegate (seckey) {
const res = secp256k1.utils.mod(secp256k1.CURVE.n - toBig(seckey), secp256k1.CURVE.n)

const buf = secp256k1.utils._bigintTo32Bytes(res)
seckey.set(buf)

return 0
},

// Validation matches ./elliptic.js
privateKeyTweakAdd (seckey, tweak) {
let res = toBig(tweak)
if (res >= secp256k1.CURVE.n) return 1

res = secp256k1.utils.mod(res + toBig(seckey), secp256k1.CURVE.n)
if (res === _0n) return 1

const buf = secp256k1.utils._bigintTo32Bytes(res)
seckey.set(buf)

return 0
},

// Validation matches ./elliptic.js
privateKeyTweakMul (seckey, tweak) {
let res = toBig(tweak)
if (res >= secp256k1.CURVE.n || res === 0n) return 1

res = secp256k1.utils.mod(res * toBig(seckey), secp256k1.CURVE.n)

const buf = secp256k1.utils._bigintTo32Bytes(res)
seckey.set(buf)

return 0
},

publicKeyVerify (pubkey) {
try {
return secp256k1.Point.fromHex(pubkey) ? 0 : 1
} catch (err) {
return 1
}
},

publicKeyCreate (output, seckey) {
try {
const publicKey = secp256k1.getPublicKey(seckey, output.length === 33)
if (output.length !== publicKey.length) return 1
output.set(publicKey)
return 0
} catch (err) {
return 1
}
},

publicKeyConvert (output, pubkey) {
try {
const publicKey = secp256k1.Point.fromHex(pubkey).toRawBytes(output.length === 33)
if (output.length !== publicKey.length) return 1
output.set(publicKey)
return 0
} catch (err) {
return 1
}
},

publicKeyNegate (output, pubkey) {
let P
try {
P = secp256k1.Point.fromHex(pubkey)
} catch (err) {
return 1
}

const point = P.negate()
return writePublicKey(output, point)
},

publicKeyCombine (output, pubkeys) {
const points = new Array(pubkeys.length)
for (let i = 0; i < pubkeys.length; ++i) {
try {
points[i] = secp256k1.Point.fromHex(pubkeys[i])
} catch (err) {
return 1
}
}

let point = points[0]
for (let i = 1; i < points.length; ++i) point = point.add(points[i])
if (point.equals(secp256k1.Point.ZERO)) return 2
return writePublicKey(output, point)
},

publicKeyTweakAdd (output, pubkey, tweak) {
let P
try {
P = secp256k1.Point.fromHex(pubkey)
} catch (err) {
return 1
}

tweak = toBig(tweak)
if (tweak >= secp256k1.CURVE.n) return 2

// returns a non-zero point or undefined
const point = secp256k1.Point.BASE.multiplyAndAddUnsafe(P, tweak, _1n) // timing-unsafe, ok here
if (!point) return 2 // returns undefined on ZERO
return writePublicKey(output, point)
},

publicKeyTweakMul (output, pubkey, tweak) {
let P
try {
P = secp256k1.Point.fromHex(pubkey)
} catch (err) {
return 1
}

tweak = toBig(tweak)
if (tweak >= secp256k1.CURVE.n || tweak === _0n) return 2

const point = P.multiply(tweak)
if (point.equals(secp256k1.Point.ZERO)) return 2
return writePublicKey(output, point)
},

signatureNormalize (sig) {
try {
const signature = secp256k1.Signature.fromCompact(sig)
if (signature.hasHighS()) {
const normal = signature.normalizeS().toCompactRawBytes()
sig.set(normal.subarray(32), 32)
}
} catch (err) {
return 1
}

return 0
},

signatureExport (obj, sig) {
let der
try {
der = secp256k1.Signature.fromCompact(sig).toDERRawBytes()
} catch (err) {
return 1
}

if (obj.output.length < der.length) return 1

obj.output.set(der)
obj.outputlen = der.length
return 0
},

signatureImport (output, sig) {
try {
buf = secp256k1.Signature.fromDER(sig).toCompactRawBytes()
} catch (err) {
return 1
}

if (output.length !== buf.length) return 1
output.set(buf)
return 0
},

ecdsaSign (obj, message, seckey, data, noncefn) {
if (noncefn || data && data.length !== 32) {
// Can we deprecate noncefn & drop it in next major? Also non-32 byte data
if (!elliptic) elliptic = require('./elliptic.js')
return elliptic.ecdsaSign(obj, message, seckey, data, noncefn)
}

let sig
try {
sig = secp256k1.signSync(message, seckey, { der: false, recovered: true, extraEntropy: data })
} catch (err) {
return 1
}

if (obj.signature.length !== sig[0].length) return 1
obj.signature.set(sig[0])
obj.recid = sig[1]
return 0
},

// Complex logic to return correct error codes
ecdsaVerify (sig, msg32, pubkey) {
if (sig.subarray(0, 32).every((x) => x === 0)) return 3
if (sig.subarray(32, 64).every((x) => x === 0)) return 3

let signature
try {
signature = secp256k1.Signature.fromCompact(sig)
} catch (err) {
return 1
}
if (signature.hasHighS()) return 3

let pub
try {
pub = secp256k1.Point.fromHex(pubkey)
} catch (err) {
return 2
}

return secp256k1.verify(sig, msg32, pub) ? 0 : 3
},

// Complex logic to return correct error codes
ecdsaRecover (output, sig, recid, msg32) {
if (sig.subarray(0, 32).every((x) => x === 0)) return 2
if (sig.subarray(32, 64).every((x) => x === 0)) return 2

let signature
try {
signature = secp256k1.Signature.fromCompact(sig)
} catch (err) {
return 1
}

let buf
try {
buf = secp256k1.recoverPublicKey(msg32, signature, recid, output.length === 33)
} catch (err) {
return 2
}

if (output.length !== buf.length) return 1
output.set(buf)
return 0
},

ecdh (output, pubkey, seckey, data, hashfn, xbuf, ybuf) {
let pub
try {
pub = secp256k1.Point.fromHex(pubkey)
} catch (err) {
return 1
}

const compressed = hashfn === undefined

let point
try {
point = secp256k1.getSharedSecret(seckey, pub, compressed)
} catch (err) {
return 2
}

if (hashfn === undefined) {
output.set(sha256(point))
} else {
if (!xbuf) xbuf = new Uint8Array(32)
xbuf.set(point.subarray(1, 33))

if (!ybuf) ybuf = new Uint8Array(32)
ybuf.set(point.subarray(33))

const hash = hashfn(xbuf, ybuf, data)
const isValid = hash instanceof Uint8Array && hash.length === output.length
if (!isValid) return 2

output.set(hash)
}

return 0
}
}
1 change: 1 addition & 0 deletions noble.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./lib')(require('./lib/noble'))
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@
],
"main": "./index.js",
"browser": {
"./index.js": "./elliptic.js"
"./index.js": "./browser.js"
},
"scripts": {
"install": "node-gyp-build || exit 0"
},
"dependencies": {
"@noble/hashes": "^1.3.3",
"@noble/secp256k1": "^1.7.1",
"elliptic": "^6.5.7",
"node-addon-api": "^5.0.0",
"node-gyp-build": "^4.2.0"
Expand Down
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ function testAPI (secp256k1, description) {
}

if (!process.browser) testAPI(require('../bindings'), 'secp256k1 bindings')
testAPI(require('../noble'), 'noble')
testAPI(require('../elliptic'), 'elliptic')

0 comments on commit a9356eb

Please sign in to comment.