From 167eaceb61a779904ff006602ce58d7065d126b7 Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Thu, 21 Oct 2021 15:58:04 +0100 Subject: [PATCH] feat: use noble-secp256k1 and noble-ed25519 (#202) BREAKING CHANGE: keys function hashAndVerify returns boolean false when fail, instead of throwing error --- package.json | 7 ++-- src/keys/ed25519.js | 72 +++++++++++++++++++++++++++++-------- src/keys/rsa-class.js | 1 - src/keys/secp256k1-class.js | 2 +- src/keys/secp256k1.js | 68 +++++++++++++++++++++++------------ src/webcrypto.js | 4 +-- test/keys/secp256k1.spec.js | 25 +++++++------ 7 files changed, 122 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index ca179343..801a9a4d 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "release-minor": "aegir release --type minor", "release-major": "aegir release --type major", "coverage": "aegir coverage --ignore src/keys/keys.proto.js", - "size": "aegir build --bundlesize", + "size": "aegir build --bundlesize --no-types", "test:types": "npx tsc" }, "keywords": [ @@ -44,10 +44,11 @@ "iso-random-stream": "^2.0.0", "keypair": "^1.0.4", "multiformats": "^9.4.5", + "noble-ed25519": "^1.2.6", + "noble-secp256k1": "^1.2.10", "node-forge": "^0.10.0", "pem-jwk": "^2.0.0", "protobufjs": "^6.11.2", - "secp256k1": "^4.0.0", "uint8arrays": "^3.0.0", "ursa-optional": "^0.10.1" }, @@ -60,7 +61,7 @@ }, "aegir": { "build": { - "bundlesizeMax": "117kB" + "bundlesizeMax": "71kB" } }, "engines": { diff --git a/src/keys/ed25519.js b/src/keys/ed25519.js index 75b6671c..f0c5446c 100644 --- a/src/keys/ed25519.js +++ b/src/keys/ed25519.js @@ -1,24 +1,68 @@ 'use strict' -require('node-forge/lib/ed25519') -const forge = require('node-forge/lib/forge') -exports.publicKeyLength = forge.pki.ed25519.constants.PUBLIC_KEY_BYTE_LENGTH -exports.privateKeyLength = forge.pki.ed25519.constants.PRIVATE_KEY_BYTE_LENGTH +const ed = require('noble-ed25519') -exports.generateKey = async function () { // eslint-disable-line require-await - return forge.pki.ed25519.generateKeyPair() +const PUBLIC_KEY_BYTE_LENGTH = 32 +const PRIVATE_KEY_BYTE_LENGTH = 64 // private key is actually 32 bytes but for historical reasons we concat private and public keys +const KEYS_BYTE_LENGTH = 32 + +exports.publicKeyLength = PUBLIC_KEY_BYTE_LENGTH +exports.privateKeyLength = PRIVATE_KEY_BYTE_LENGTH + +exports.generateKey = async function () { + // the actual private key (32 bytes) + const privateKeyRaw = ed.utils.randomPrivateKey() + const publicKey = await ed.getPublicKey(privateKeyRaw) + + // concatenated the public key to the private key + const privateKey = concatKeys(privateKeyRaw, publicKey) + + return { + privateKey, + publicKey + } +} + +/** + * Generate keypair from a seed + * + * @param {Uint8Array} seed - seed should be a 32 byte uint8array + * @returns + */ +exports.generateKeyFromSeed = async function (seed) { + if (seed.length !== KEYS_BYTE_LENGTH) { + throw new TypeError('"seed" must be 32 bytes in length.') + } else if (!(seed instanceof Uint8Array)) { + throw new TypeError('"seed" must be a node.js Buffer, or Uint8Array.') + } + + // based on node forges algorithm, the seed is used directly as private key + const privateKeyRaw = seed + const publicKey = await ed.getPublicKey(privateKeyRaw) + + const privateKey = concatKeys(privateKeyRaw, publicKey) + + return { + privateKey, + publicKey + } } -// seed should be a 32 byte uint8array -exports.generateKeyFromSeed = async function (seed) { // eslint-disable-line require-await - return forge.pki.ed25519.generateKeyPair({ seed }) +exports.hashAndSign = function (privateKey, msg) { + const privateKeyRaw = privateKey.slice(0, KEYS_BYTE_LENGTH) + + return ed.sign(msg, privateKeyRaw) } -exports.hashAndSign = async function (key, msg) { // eslint-disable-line require-await - return forge.pki.ed25519.sign({ message: msg, privateKey: key }) - // return Uint8Array.from(nacl.sign.detached(msg, key)) +exports.hashAndVerify = function (publicKey, sig, msg) { + return ed.verify(sig, msg, publicKey) } -exports.hashAndVerify = async function (key, sig, msg) { // eslint-disable-line require-await - return forge.pki.ed25519.verify({ signature: sig, message: msg, publicKey: key }) +function concatKeys (privateKeyRaw, publicKey) { + const privateKey = new Uint8Array(exports.privateKeyLength) + for (let i = 0; i < KEYS_BYTE_LENGTH; i++) { + privateKey[i] = privateKeyRaw[i] + privateKey[KEYS_BYTE_LENGTH + i] = publicKey[i] + } + return privateKey } diff --git a/src/keys/rsa-class.js b/src/keys/rsa-class.js index 67b39ff6..597a3b57 100644 --- a/src/keys/rsa-class.js +++ b/src/keys/rsa-class.js @@ -6,7 +6,6 @@ const { equals: uint8ArrayEquals } = require('uint8arrays/equals') const { toString: uint8ArrayToString } = require('uint8arrays/to-string') require('node-forge/lib/sha512') -require('node-forge/lib/ed25519') const forge = require('node-forge/lib/forge') const crypto = require('./rsa') diff --git a/src/keys/secp256k1-class.js b/src/keys/secp256k1-class.js index 4112f74f..9d5239f1 100644 --- a/src/keys/secp256k1-class.js +++ b/src/keys/secp256k1-class.js @@ -8,7 +8,7 @@ const { toString: uint8ArrayToString } = require('uint8arrays/to-string') const exporter = require('./exporter') module.exports = (keysProtobuf, randomBytes, crypto) => { - crypto = crypto || require('./secp256k1')(randomBytes) + crypto = crypto || require('./secp256k1')() class Secp256k1PublicKey { constructor (key) { diff --git a/src/keys/secp256k1.js b/src/keys/secp256k1.js index 9ca046d4..e111070b 100644 --- a/src/keys/secp256k1.js +++ b/src/keys/secp256k1.js @@ -1,57 +1,79 @@ 'use strict' -const secp256k1 = require('secp256k1') +const errcode = require('err-code') +const secp = require('noble-secp256k1') const { sha256 } = require('multiformats/hashes/sha2') -module.exports = (randomBytes) => { +module.exports = () => { const privateKeyLength = 32 function generateKey () { - let privateKey - do { - privateKey = randomBytes(32) - } while (!secp256k1.privateKeyVerify(privateKey)) - return privateKey + return secp.utils.randomPrivateKey() } + /** + * Hash and sign message with private key + * + * @param {number | bigint | (string | Uint8Array)} key + * @param {Uint8Array} msg + */ async function hashAndSign (key, msg) { const { digest } = await sha256.digest(msg) - const sig = secp256k1.ecdsaSign(digest, key) - return secp256k1.signatureExport(sig.signature) + try { + return await secp.sign(digest, key) + } catch (err) { + throw errcode(err, 'ERR_INVALID_INPUT') + } } + /** + * Hash message and verify signature with public key + * + * @param {secp.Point | (string | Uint8Array)} key + * @param {(string | Uint8Array) | secp.Signature} sig + * @param {Uint8Array} msg + */ async function hashAndVerify (key, sig, msg) { - const { digest } = await sha256.digest(msg) - sig = secp256k1.signatureImport(sig) - return secp256k1.ecdsaVerify(sig, digest, key) + try { + const { digest } = await sha256.digest(msg) + return secp.verify(sig, digest, key) + } catch (err) { + throw errcode(err, 'ERR_INVALID_INPUT') + } } function compressPublicKey (key) { - if (!secp256k1.publicKeyVerify(key)) { - throw new Error('Invalid public key') - } - return secp256k1.publicKeyConvert(key, true) + const point = secp.Point.fromHex(key).toRawBytes(true) + return point } function decompressPublicKey (key) { - return secp256k1.publicKeyConvert(key, false) + const point = secp.Point.fromHex(key).toRawBytes(false) + return point } function validatePrivateKey (key) { - if (!secp256k1.privateKeyVerify(key)) { - throw new Error('Invalid private key') + try { + secp.getPublicKey(key, true) + } catch (err) { + throw errcode(err, 'ERR_INVALID_PRIVATE_KEY') } } function validatePublicKey (key) { - if (!secp256k1.publicKeyVerify(key)) { - throw new Error('Invalid public key') + try { + secp.Point.fromHex(key) + } catch (err) { + throw errcode(err, 'ERR_INVALID_PUBLIC_KEY') } } function computePublicKey (privateKey) { - validatePrivateKey(privateKey) - return secp256k1.publicKeyCreate(privateKey) + try { + return secp.getPublicKey(privateKey, true) + } catch (err) { + throw errcode(err, 'ERR_INVALID_PRIVATE_KEY') + } } return { diff --git a/src/webcrypto.js b/src/webcrypto.js index 1663369a..f08d34e2 100644 --- a/src/webcrypto.js +++ b/src/webcrypto.js @@ -4,8 +4,8 @@ // Check native crypto exists and is enabled (In insecure context `self.crypto` // exists but `self.crypto.subtle` does not). -exports.get = (win = self) => { - const nativeCrypto = win.crypto || win.msCrypto +exports.get = (win = globalThis) => { + const nativeCrypto = win.crypto if (!nativeCrypto || !nativeCrypto.subtle) { throw Object.assign( diff --git a/test/keys/secp256k1.spec.js b/test/keys/secp256k1.spec.js index 55d6883f..4ae10f39 100644 --- a/test/keys/secp256k1.spec.js +++ b/test/keys/secp256k1.spec.js @@ -6,7 +6,7 @@ const crypto = require('../../src') const secp256k1 = crypto.keys.supportedKeys.secp256k1 const keysPBM = crypto.keys.keysPBM const randomBytes = crypto.randomBytes -const secp256k1Crypto = require('../../src/keys/secp256k1')(randomBytes) +const secp256k1Crypto = require('../../src/keys/secp256k1')() const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') const fixtures = require('../fixtures/go-key-secp256k1') @@ -155,7 +155,7 @@ describe('handles generation of invalid key', () => { try { await secp256k1.generateKeyPair() } catch (err) { - return expect(err.message).to.equal('Expected private key to be an Uint8Array with length 32') + return expect(err.code).to.equal('ERR_INVALID_PRIVATE_KEY') } throw new Error('Expected error to be thrown') }) @@ -188,8 +188,15 @@ describe('crypto functions', () => { expect(valid).to.equal(true) }) + it('does not validate when validating a message with an invalid signature', async () => { + const result = await secp256k1Crypto.hashAndVerify(pubKey, uint8ArrayFromString('invalid-sig'), uint8ArrayFromString('hello')) + + expect(result).to.be.false() + }) + it('errors if given a null Uint8Array to sign', async () => { try { + // @ts-ignore await secp256k1Crypto.hashAndSign(privKey, null) } catch (err) { return // expected @@ -201,7 +208,7 @@ describe('crypto functions', () => { try { await secp256k1Crypto.hashAndSign(uint8ArrayFromString('42'), uint8ArrayFromString('Hello')) } catch (err) { - return expect(err.message).to.equal('Expected private key to be an Uint8Array with length 32') + return expect(err.code).to.equal('ERR_INVALID_INPUT') } throw new Error('Expected error to be thrown') }) @@ -210,6 +217,7 @@ describe('crypto functions', () => { const sig = await secp256k1Crypto.hashAndSign(privKey, uint8ArrayFromString('hello')) try { + // @ts-ignore await secp256k1Crypto.hashAndVerify(privKey, sig, null) } catch (err) { return // expected @@ -217,20 +225,11 @@ describe('crypto functions', () => { throw new Error('Expected error to be thrown') }) - it('errors when validating a message with an invalid signature', async () => { - try { - await secp256k1Crypto.hashAndVerify(pubKey, uint8ArrayFromString('invalid-sig'), uint8ArrayFromString('hello')) - } catch (err) { - return expect(err.message).to.equal('Signature could not be parsed') - } - throw new Error('Expected error to be thrown') - }) - it('errors when signing with an invalid key', async () => { try { await secp256k1Crypto.hashAndSign(uint8ArrayFromString('42'), uint8ArrayFromString('Hello')) } catch (err) { - return expect(err.message).to.equal('Expected private key to be an Uint8Array with length 32') + return expect(err.code).to.equal('ERR_INVALID_INPUT') } throw new Error('Expected error to be thrown') })