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

feat: use noble-secp256k1 and noble-ed25519 #202

Merged
merged 12 commits into from
Oct 21, 2021
Merged
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
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.1",
"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 () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why removing this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i just moved the test up a bit check 'does not validate when validating a message with an invalid signature', because it doesnt throw now just returns false for an invalid sig

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, so this now returns a boolean false instead of throwing an error. Can we flag that breaking change in the commit message?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you do it when you squash and merge ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, I can do

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