diff --git a/package.json b/package.json index 157544c..b83c208 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "pem-jwk": "^1.5.1", "protocol-buffers": "^3.2.1", "rsa-pem-to-jwk": "^1.1.3", + "tweetnacl": "^0.14.5", "webcrypto-shim": "github:dignifiedquire/webcrypto-shim#master" }, "devDependencies": { @@ -75,4 +76,4 @@ "Richard Littauer ", "nikuda " ] -} \ No newline at end of file +} diff --git a/src/crypto.js b/src/crypto.js index f0db016..c83c0c6 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -5,3 +5,4 @@ exports.hmac = require('./crypto/hmac') exports.ecdh = require('./crypto/ecdh') exports.aes = require('./crypto/aes') exports.rsa = require('./crypto/rsa') +exports.ed25519 = require('./crypto/ed25519') diff --git a/src/crypto.proto.js b/src/crypto.proto.js index b6c44cb..96568eb 100644 --- a/src/crypto.proto.js +++ b/src/crypto.proto.js @@ -2,6 +2,7 @@ module.exports = `enum KeyType { RSA = 0; + Ed25519 = 1; } message PublicKey { diff --git a/src/crypto/ed25519.js b/src/crypto/ed25519.js new file mode 100644 index 0000000..9bf4edd --- /dev/null +++ b/src/crypto/ed25519.js @@ -0,0 +1,34 @@ +'use strict' + +const nacl = require('tweetnacl') +const setImmediate = require('async/setImmediate') + +exports.publicKeyLength = nacl.sign.publicKeyLength +exports.privateKeyLength = nacl.sign.secretKeyLength + +exports.generateKey = function (callback) { + const done = (err, res) => setImmediate(() => { + callback(err, res) + }) + + let keys + try { + keys = nacl.sign.keyPair() + } catch (err) { + done(err) + return + } + done(null, keys) +} + +exports.hashAndSign = function (key, msg, callback) { + setImmediate(() => { + callback(null, Buffer.from(nacl.sign.detached(msg, key))) + }) +} + +exports.hashAndVerify = function (key, sig, msg, callback) { + setImmediate(() => { + callback(null, nacl.sign.detached.verify(msg, sig, key)) + }) +} diff --git a/src/index.js b/src/index.js index fb7e50e..11bf144 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,8 @@ exports.aes = c.aes exports.webcrypto = c.webcrypto const keys = exports.keys = require('./keys') +const KEY_TYPES = ['rsa', 'ed25519'] + exports.keyStretcher = require('./key-stretcher') exports.generateEphemeralKeyPair = require('./ephemeral-keys') @@ -30,6 +32,8 @@ exports.unmarshalPublicKey = (buf) => { switch (decoded.Type) { case pbm.KeyType.RSA: return keys.rsa.unmarshalRsaPublicKey(decoded.Data) + case pbm.KeyType.Ed25519: + return keys.ed25519.unmarshalEd25519PublicKey(decoded.Data) default: throw new Error('invalid or unsupported key type') } @@ -38,9 +42,7 @@ exports.unmarshalPublicKey = (buf) => { // Converts a public key object into a protobuf serialized public key exports.marshalPublicKey = (key, type) => { type = (type || 'rsa').toLowerCase() - - // for now only rsa is supported - if (type !== 'rsa') { + if (KEY_TYPES.indexOf(type) < 0) { throw new Error('invalid or unsupported key type') } @@ -55,6 +57,8 @@ exports.unmarshalPrivateKey = (buf, callback) => { switch (decoded.Type) { case pbm.KeyType.RSA: return keys.rsa.unmarshalRsaPrivateKey(decoded.Data, callback) + case pbm.KeyType.Ed25519: + return keys.ed25519.unmarshalEd25519PrivateKey(decoded.Data, callback) default: callback(new Error('invalid or unsupported key type')) } @@ -63,9 +67,7 @@ exports.unmarshalPrivateKey = (buf, callback) => { // Converts a private key object into a protobuf serialized private key exports.marshalPrivateKey = (key, type) => { type = (type || 'rsa').toLowerCase() - - // for now only rsa is supported - if (type !== 'rsa') { + if (KEY_TYPES.indexOf(type) < 0) { throw new Error('invalid or unsupported key type') } diff --git a/src/keys/ed25519.js b/src/keys/ed25519.js new file mode 100644 index 0000000..b0e74f0 --- /dev/null +++ b/src/keys/ed25519.js @@ -0,0 +1,141 @@ +'use strict' + +const multihashing = require('multihashing-async') +const protobuf = require('protocol-buffers') + +const crypto = require('../crypto').ed25519 +const pbm = protobuf(require('../crypto.proto')) + +class Ed25519PublicKey { + constructor (key) { + this._key = ensureKey(key, crypto.publicKeyLength) + } + + verify (data, sig, callback) { + ensure(callback) + crypto.hashAndVerify(this._key, sig, data, callback) + } + + marshal () { + return Buffer.from(this._key) + } + + get bytes () { + return pbm.PublicKey.encode({ + Type: pbm.KeyType.Ed25519, + Data: this.marshal() + }) + } + + equals (key) { + return this.bytes.equals(key.bytes) + } + + hash (callback) { + ensure(callback) + multihashing(this.bytes, 'sha2-256', callback) + } +} + +class Ed25519PrivateKey { + // key - 64 byte Uint8Array or Buffer containing private key + // publicKey - 32 byte Uint8Array or Buffer containing public key + constructor (key, publicKey) { + this._key = ensureKey(key, crypto.privateKeyLength) + this._publicKey = ensureKey(publicKey, crypto.publicKeyLength) + } + + sign (message, callback) { + ensure(callback) + crypto.hashAndSign(this._key, message, callback) + } + + get public () { + if (!this._publicKey) { + throw new Error('public key not provided') + } + + return new Ed25519PublicKey(this._publicKey) + } + + marshal () { + return Buffer.concat([Buffer.from(this._key), Buffer.from(this._publicKey)]) + } + + get bytes () { + return pbm.PrivateKey.encode({ + Type: pbm.KeyType.Ed25519, + Data: this.marshal() + }) + } + + equals (key) { + return this.bytes.equals(key.bytes) + } + + hash (callback) { + ensure(callback) + multihashing(this.bytes, 'sha2-256', callback) + } +} + +function unmarshalEd25519PrivateKey (bytes, callback) { + try { + bytes = ensureKey(bytes, crypto.privateKeyLength + crypto.publicKeyLength) + } catch (err) { + return callback(err) + } + const privateKeyBytes = bytes.slice(0, crypto.privateKeyLength) + const publicKeyBytes = bytes.slice(crypto.privateKeyLength, bytes.length) + callback(null, new Ed25519PrivateKey(privateKeyBytes, publicKeyBytes)) +} + +function unmarshalEd25519PublicKey (bytes) { + bytes = ensureKey(bytes, crypto.publicKeyLength) + return new Ed25519PublicKey(bytes) +} + +function generateKeyPair (_bits, cb) { + if (cb === undefined && typeof _bits === 'function') { + cb = _bits + } + + crypto.generateKey((err, keys) => { + if (err) { + return cb(err) + } + let privkey + try { + privkey = new Ed25519PrivateKey(keys.secretKey, keys.publicKey) + } catch (err) { + cb(err) + return + } + + cb(null, privkey) + }) +} + +function ensure (cb) { + if (typeof cb !== 'function') { + throw new Error('callback is required') + } +} + +function ensureKey (key, length) { + if (Buffer.isBuffer(key)) { + key = new Uint8Array(key) + } + if (!(key instanceof Uint8Array) || key.length !== length) { + throw new Error('Key must be a Uint8Array or Buffer of length ' + length) + } + return key +} + +module.exports = { + Ed25519PublicKey, + Ed25519PrivateKey, + unmarshalEd25519PrivateKey, + unmarshalEd25519PublicKey, + generateKeyPair +} diff --git a/src/keys/index.js b/src/keys/index.js index 3e74b19..9604037 100644 --- a/src/keys/index.js +++ b/src/keys/index.js @@ -1,5 +1,6 @@ 'use strict' module.exports = { - rsa: require('./rsa') + rsa: require('./rsa'), + ed25519: require('./ed25519') } diff --git a/test/ed25519.spec.js b/test/ed25519.spec.js new file mode 100644 index 0000000..cb90bb7 --- /dev/null +++ b/test/ed25519.spec.js @@ -0,0 +1,194 @@ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect + +const crypto = require('../src') +const ed25519 = crypto.keys.ed25519 +const fixtures = require('./fixtures/go-key-ed25519') + +describe('ed25519', () => { + let key + before((done) => { + crypto.generateKeyPair('Ed25519', 512, (err, _key) => { + if (err) return done(err) + key = _key + done() + }) + }) + + it('generates a valid key', (done) => { + expect( + key + ).to.be.an.instanceof( + ed25519.Ed25519PrivateKey + ) + + key.hash((err, digest) => { + if (err) { + return done(err) + } + + expect(digest).to.have.length(34) + done() + }) + }) + + it('signs', (done) => { + const text = crypto.randomBytes(512) + + key.sign(text, (err, sig) => { + if (err) { + return done(err) + } + + key.public.verify(text, sig, (err, res) => { + if (err) { + return done(err) + } + + expect(res).to.be.eql(true) + done() + }) + }) + }) + + it('encoding', (done) => { + const keyMarshal = key.marshal() + ed25519.unmarshalEd25519PrivateKey(keyMarshal, (err, key2) => { + if (err) { + return done(err) + } + const keyMarshal2 = key2.marshal() + + expect( + keyMarshal + ).to.be.eql( + keyMarshal2 + ) + + const pk = key.public + const pkMarshal = pk.marshal() + const pk2 = ed25519.unmarshalEd25519PublicKey(pkMarshal) + const pkMarshal2 = pk2.marshal() + + expect( + pkMarshal + ).to.be.eql( + pkMarshal2 + ) + done() + }) + }) + + describe('key equals', () => { + it('equals itself', () => { + expect( + key.equals(key) + ).to.be.eql( + true + ) + + expect( + key.public.equals(key.public) + ).to.be.eql( + true + ) + }) + + it('not equals other key', (done) => { + crypto.generateKeyPair('Ed25519', 512, (err, key2) => { + if (err) return done(err) + + expect( + key.equals(key2) + ).to.be.eql( + false + ) + + expect( + key2.equals(key) + ).to.be.eql( + false + ) + + expect( + key.public.equals(key2.public) + ).to.be.eql( + false + ) + + expect( + key2.public.equals(key.public) + ).to.be.eql( + false + ) + done() + }) + }) + }) + + it('sign and verify', (done) => { + const data = new Buffer('hello world') + key.sign(data, (err, sig) => { + if (err) { + return done(err) + } + + key.public.verify(data, sig, (err, valid) => { + if (err) { + return done(err) + } + expect(valid).to.be.eql(true) + done() + }) + }) + }) + + it('fails to verify for different data', (done) => { + const data = new Buffer('hello world') + key.sign(data, (err, sig) => { + if (err) { + return done(err) + } + + key.public.verify(new Buffer('hello'), sig, (err, valid) => { + if (err) { + return done(err) + } + expect(valid).to.be.eql(false) + done() + }) + }) + }) + + describe('go interop', () => { + let privateKey + before((done) => { + crypto.unmarshalPrivateKey(fixtures.verify.privateKey, (err, key) => { + expect(err).to.not.exist + privateKey = key + done() + }) + }) + + it('verifies with data from go', (done) => { + const key = crypto.unmarshalPublicKey(fixtures.verify.publicKey) + + key.verify(fixtures.verify.data, fixtures.verify.signature, (err, ok) => { + if (err) throw err + expect(err).to.not.exist + expect(ok).to.be.eql(true) + done() + }) + }) + + it('generates the same signature as go', (done) => { + privateKey.sign(fixtures.verify.data, (err, sig) => { + expect(err).to.not.exist + expect(sig).to.deep.equal(fixtures.verify.signature) + done() + }) + }) + }) +}) diff --git a/test/fixtures/go-key-ed25519.js b/test/fixtures/go-key-ed25519.js new file mode 100644 index 0000000..756bf0e --- /dev/null +++ b/test/fixtures/go-key-ed25519.js @@ -0,0 +1,28 @@ +'use strict' + +module.exports = { + // These were generated in a gore (https://github.com/motemen/gore) repl session: + // + // :import github.com/libp2p/go-libp2p-crypto + // :import crypto/rand + // priv, pub, err := crypto.GenerateEd25519Key(rand.Reader) + // pubkeyBytes, err := pub.Bytes() + // privkeyBytes, err := priv.Bytes() + // data := []byte("hello! and welcome to some awesome crypto primitives") + // sig, err := priv.Sign(data) + // + // :import io/ioutil + // ioutil.WriteFile("/tmp/pubkey_go.bin", pubkeyBytes, 0644) + // // etc.. + // + // Then loaded into a node repl and dumped to arrays with: + // + // var pubkey = Array.from(fs.readFileSync('/tmp/pubkey_go.bin')) + // console.log(JSON.stringify(pubkey)) + verify: { + privateKey: Buffer.from([8, 1, 18, 96, 201, 208, 1, 110, 176, 16, 230, 37, 66, 184, 149, 252, 78, 56, 206, 136, 2, 38, 118, 152, 226, 197, 117, 200, 54, 189, 156, 218, 184, 7, 118, 57, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37]), + publicKey: Buffer.from([8, 1, 18, 32, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37]), + data: Buffer.from([104, 101, 108, 108, 111, 33, 32, 97, 110, 100, 32, 119, 101, 108, 99, 111, 109, 101, 32, 116, 111, 32, 115, 111, 109, 101, 32, 97, 119, 101, 115, 111, 109, 101, 32, 99, 114, 121, 112, 116, 111, 32, 112, 114, 105, 109, 105, 116, 105, 118, 101, 115]), + signature: Buffer.from([7, 230, 175, 164, 228, 58, 78, 208, 62, 243, 73, 142, 83, 195, 176, 217, 166, 62, 41, 165, 168, 164, 75, 179, 163, 86, 102, 32, 18, 84, 150, 237, 39, 207, 213, 20, 134, 237, 50, 41, 176, 183, 229, 133, 38, 255, 42, 228, 68, 186, 100, 14, 175, 156, 243, 118, 125, 125, 120, 212, 124, 103, 252, 12]) + } +}