Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add noble-based impl for fallback when BigInt is available #210

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
}
289 changes: 289 additions & 0 deletions lib/noble.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
const secp256k1 = require('@noble/secp256k1')
const { sha256 } = require('@noble/hashes/sha256')
const { hmac } = require('@noble/hashes/hmac')

/* global BigInt */

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 readPublicKey (pubkey) {
try {
return secp256k1.Point.fromHex(pubkey)
} catch (err) {
return undefined
}
}

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) {
const P = readPublicKey(pubkey)
return P ? 0 : 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) {
const P = readPublicKey(pubkey)
if (!P) return 1
return writePublicKey(output, P)
},

publicKeyNegate (output, pubkey) {
const P = readPublicKey(pubkey)
if (!P) 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) {
points[i] = readPublicKey(pubkeys[i])
if (!points[i]) 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) {
const P = readPublicKey(pubkey)
if (!P) 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) {
const P = readPublicKey(pubkey)
if (!P) 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) {
let buf
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

const P = readPublicKey(pubkey)
if (!P) return 2

return secp256k1.verify(sig, msg32, P) ? 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) {
const P = readPublicKey(pubkey)
if (!P) return 1

const compressed = hashfn === undefined

let point
try {
point = secp256k1.getSharedSecret(seckey, P, 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')
Loading