diff --git a/README.md b/README.md index ed50879..362acb2 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,21 @@ Depending on the environment this is either an instance of [node-webcrypto-ossl] ### `keys` +#### Supported Key Types + +The [`generateKeyPair`](#generatekeypairtype-bits-callback), [`marshalPublicKey`](#marshalpublickeykey-type-callback), and +[`marshalPrivateKey`](#marshalprivatekeykey-type) functions accept a string `type` argument. + +Currently the `'RSA'` and `'ed25519'` types are supported, although ed25519 keys support only signing and +verification of messages. For encryption / decryption support, RSA keys should be used. + +Installing the [libp2p-crypto-secp256k1](https://github.com/libp2p/js-libp2p-crypto-secp256k1) module adds support for the +`'secp256k1'` type, which supports ECDSA signatures using the secp256k1 elliptic curve popularized by Bitcoin. This module +is not installed by default, and should be explicitly depended on if your project requires secp256k1 support. + ### `generateKeyPair(type, bits, callback)` -- `type: String`, only `'RSA'` is currently supported +- `type: String`, see [Supported Key Types](#supported-key-types) above. - `bits: Number` Minimum of 1024 - `callback: Function` @@ -160,8 +172,8 @@ Calls back with an object of the form ### `marshalPublicKey(key[, type], callback)` -- `key: crypto.rsa.RsaPublicKey` -- `type: String`, only `'RSA'` is currently supported +- `key: keys.rsa.RsaPublicKey | keys.ed25519.Ed25519PublicKey | require('libp2p-crypto-secp256k1').Secp256k1PublicKey` +- `type: String`, see [Supported Key Types](#supported-key-types) above. Converts a public key object into a protobuf serialized public key. @@ -173,8 +185,8 @@ Converts a protobuf serialized public key into its representative object. ### `marshalPrivateKey(key[, type])` -- `key: crypto.rsa.RsaPrivateKey` -- `type: String`, only `'RSA'` is currently supported +- `key: keys.rsa.RsaPrivateKey | keys.ed25519.Ed25519PrivateKey | require('libp2p-crypto-secp256k1').Secp256k1PrivateKey` +- `type: String`, see [Supported Key Types](#supported-key-types) above. Converts a private key object into a protobuf serialized private key. diff --git a/package.json b/package.json index 27d0c13..4631642 100644 --- a/package.json +++ b/package.json @@ -80,4 +80,4 @@ "dryajov ", "nikuda " ] -} \ No newline at end of file +} diff --git a/src/crypto.proto.js b/src/crypto.proto.js index 96568eb..2b7f08c 100644 --- a/src/crypto.proto.js +++ b/src/crypto.proto.js @@ -3,6 +3,7 @@ module.exports = `enum KeyType { RSA = 0; Ed25519 = 1; + Secp256k1 = 2; } message PublicKey { diff --git a/src/index.js b/src/index.js index 12652a5..6f1ad0b 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,10 @@ exports.aes = c.aes exports.webcrypto = c.webcrypto const keys = exports.keys = require('./keys') -const KEY_TYPES = ['rsa', 'ed25519'] +function isValidKeyType (keyType) { + const key = keys[keyType.toLowerCase()] + return key !== undefined +} exports.keyStretcher = require('./key-stretcher') exports.generateEphemeralKeyPair = require('./ephemeral-keys') @@ -35,6 +38,12 @@ exports.unmarshalPublicKey = (buf) => { return keys.rsa.unmarshalRsaPublicKey(decoded.Data) case pbm.KeyType.Ed25519: return keys.ed25519.unmarshalEd25519PublicKey(decoded.Data) + case pbm.KeyType.Secp256k1: + if (keys.secp256k1) { + return keys.secp256k1.unmarshalSecp256k1PublicKey(decoded.Data) + } else { + throw new Error('secp256k1 support requires libp2p-crypto-secp256k1 package') + } default: throw new Error('invalid or unsupported key type') } @@ -43,7 +52,7 @@ exports.unmarshalPublicKey = (buf) => { // Converts a public key object into a protobuf serialized public key exports.marshalPublicKey = (key, type) => { type = (type || 'rsa').toLowerCase() - if (KEY_TYPES.indexOf(type) < 0) { + if (!isValidKeyType(type)) { throw new Error('invalid or unsupported key type') } @@ -60,6 +69,12 @@ exports.unmarshalPrivateKey = (buf, callback) => { return keys.rsa.unmarshalRsaPrivateKey(decoded.Data, callback) case pbm.KeyType.Ed25519: return keys.ed25519.unmarshalEd25519PrivateKey(decoded.Data, callback) + case pbm.KeyType.Secp256k1: + if (keys.secp256k1) { + return keys.secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data, callback) + } else { + return callback(new Error('secp256k1 support requires libp2p-crypto-secp256k1 package')) + } default: callback(new Error('invalid or unsupported key type')) } @@ -68,7 +83,7 @@ exports.unmarshalPrivateKey = (buf, callback) => { // Converts a private key object into a protobuf serialized private key exports.marshalPrivateKey = (key, type) => { type = (type || 'rsa').toLowerCase() - if (KEY_TYPES.indexOf(type) < 0) { + if (!isValidKeyType(type)) { throw new Error('invalid or unsupported key type') } diff --git a/src/keys/index.js b/src/keys/index.js index 9604037..0f1baad 100644 --- a/src/keys/index.js +++ b/src/keys/index.js @@ -4,3 +4,8 @@ module.exports = { rsa: require('./rsa'), ed25519: require('./ed25519') } + +try { + module.exports.secp256k1 = require('libp2p-crypto-secp256k1') +} catch (err) { +} diff --git a/test/fixtures/secp256k1.js b/test/fixtures/secp256k1.js new file mode 100644 index 0000000..9b10c84 --- /dev/null +++ b/test/fixtures/secp256k1.js @@ -0,0 +1,11 @@ +'use strict' + +const Buffer = require('safe-buffer').Buffer + +module.exports = { + + // protobuf marshaled key pair generated with libp2p-crypto-secp256k1 + // and marshaled with libp2p-crypto.marshalPublicKey / marshalPrivateKey + pbmPrivateKey: Buffer.from('08021220e0600103010000000100000000000000be1dc82c2e000000e8d6030301000000', 'hex'), + pbmPublicKey: Buffer.from('0802122103a9a7272a726fa083abf31ba44037f8347fbc5e5d3113d62a7c6bc26752fd8ee1', 'hex') +} diff --git a/test/index.spec.js b/test/index.spec.js index ac24faa..2465b4d 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -23,9 +23,17 @@ describe('libp2p-crypto', () => { const key2 = crypto.unmarshalPublicKey(crypto.marshalPublicKey(key.public)) expect(key2.equals(key.public)).to.be.eql(true) + + expect(() => { + crypto.marshalPublicKey(key.public, 'invalid-key-type') + }).to.throw() }) it('marshalPrivateKey and unmarshalPrivateKey', (done) => { + expect(() => { + crypto.marshalPrivateKey(key, 'invalid-key-type') + }).to.throw() + crypto.unmarshalPrivateKey(crypto.marshalPrivateKey(key), (err, key2) => { if (err) { return done(err) @@ -103,7 +111,7 @@ describe('libp2p-crypto', () => { it('throws with no number passed', () => { expect(() => { crypto.randomBytes() - }).to.throw + }).to.throw() }) it('generates different random things', () => { diff --git a/test/secp256k1.spec.js b/test/secp256k1.spec.js new file mode 100644 index 0000000..f6e4ac8 --- /dev/null +++ b/test/secp256k1.spec.js @@ -0,0 +1,104 @@ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect +const fixtures = require('./fixtures/secp256k1') +const crypto = require('../src') + +const mockPublicKey = { + bytes: fixtures.pbmPublicKey +} + +const mockPrivateKey = { + bytes: fixtures.pbmPrivateKey, + public: mockPublicKey +} + +const mockSecp256k1Module = { + generateKeyPair (bits, callback) { + callback(null, mockPrivateKey) + }, + + unmarshalSecp256k1PrivateKey (buf, callback) { + callback(null, mockPrivateKey) + }, + + unmarshalSecp256k1PublicKey (buf) { + return mockPublicKey + } +} + +describe('with libp2p-crypto-secp256k1 module present', () => { + let key + + before((done) => { + crypto.keys['secp256k1'] = mockSecp256k1Module + crypto.generateKeyPair('secp256k1', 256, (err, _key) => { + if (err) return done(err) + key = _key + done() + }) + }) + + after((done) => { + delete crypto.keys['secp256k1'] + done() + }) + + it('generates a valid key', (done) => { + expect( + key + ).to.exist + done() + }) + + it('protobuf encoding', (done) => { + const keyMarshal = crypto.marshalPrivateKey(key) + crypto.unmarshalPrivateKey(keyMarshal, (err, key2) => { + if (err) return done(err) + const keyMarshal2 = crypto.marshalPrivateKey(key2) + + expect( + keyMarshal + ).to.be.eql( + keyMarshal2 + ) + + const pk = key.public + const pkMarshal = crypto.marshalPublicKey(pk) + const pk2 = crypto.unmarshalPublicKey(pkMarshal) + const pkMarshal2 = crypto.marshalPublicKey(pk2) + + expect( + pkMarshal + ).to.be.eql( + pkMarshal2 + ) + done() + }) + }) +}) + +describe('without libp2p-crypto-secp256k1 module present', () => { + it('fails to generate a secp256k1 key', (done) => { + crypto.generateKeyPair('secp256k1', 256, (err, key) => { + expect(err).to.exist + expect(key).to.not.exist + done() + }) + }) + + it('fails to unmarshal a secp256k1 private key', (done) => { + crypto.unmarshalPrivateKey(fixtures.pbmPrivateKey, (err, key) => { + expect(err).to.exist + expect(key).to.not.exist + done() + }) + }) + + it('fails to unmarshal a secp256k1 public key', () => { + expect(() => { + crypto.unmarshalPublicKey(fixtures.pbmPublicKey) + }).to.throw(Error) + }) +})