Skip to content
This repository has been archived by the owner on Jul 21, 2023. It is now read-only.

Commit

Permalink
feat: use noble-secp256k1 and noble-ed25519 (#202)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: keys function hashAndVerify returns boolean false when fail, instead of throwing error
  • Loading branch information
hugomrdias authored Oct 21, 2021
1 parent 2e40aea commit 167eace
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 57 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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"
},
Expand All @@ -60,7 +61,7 @@
},
"aegir": {
"build": {
"bundlesizeMax": "117kB"
"bundlesizeMax": "71kB"
}
},
"engines": {
Expand Down
72 changes: 58 additions & 14 deletions src/keys/ed25519.js
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 0 additions & 1 deletion src/keys/rsa-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion src/keys/secp256k1-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
68 changes: 45 additions & 23 deletions src/keys/secp256k1.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/webcrypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
25 changes: 12 additions & 13 deletions test/keys/secp256k1.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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')
})
Expand Down Expand Up @@ -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
Expand All @@ -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')
})
Expand All @@ -210,27 +217,19 @@ describe('crypto functions', () => {
const sig = await secp256k1Crypto.hashAndSign(privKey, uint8ArrayFromString('hello'))

try {
// @ts-ignore
await secp256k1Crypto.hashAndVerify(privKey, sig, null)
} catch (err) {
return // expected
}
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')
})
Expand Down

0 comments on commit 167eace

Please sign in to comment.