diff --git a/index.js b/index.js index 159c5dc..38275d8 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ +const Stream = require('stream'); const util = require('util'); const base64url = require('base64url'); const crypto = require('crypto'); @@ -59,6 +60,7 @@ function isValidJws(string) { } function jwsVerify(jwsSig, secretOrKey) { + jwsSig = toString(jwsSig); const signature = signatureFromJWS(jwsSig); const securedInput = securedInputFromJWS(jwsSig); const algo = jwa(algoFromJWS(jwsSig)); @@ -82,16 +84,101 @@ exports.sign = jwsSign; exports.verify = jwsVerify; exports.decode = jwsDecode; - -function algorithmFromSecret(secretOrKey) { - secretOrKey = secretOrKey.toString(); - const RSA_INDICATOR = '-----BEGIN RSA PRIVATE KEY-----'; - const EC_INDICATOR = '-----BEGIN EC PRIVATE KEY-----'; - if (secretOrKey.indexOf(RSA_INDICATOR) > -1) - return 'RS'; - if (secretOrKey.indexOf(EC_INDICATOR) > -1) - return 'EC'; - return 'HS'; +exports.createSign = function createSign(opts) { + return new StreamSign(opts); +}; +exports.createVerify = function createVerify(opts) { + return new StreamVerify(opts); +}; + +function StreamSign(opts) { + const secret = opts.secret||opts.privateKey||opts.key; + const secretStream = new StreamData(secret); + this.readable = true; + this.header = opts.header; + this.secret = this.privateKey = this.key = secretStream; + this.payload = new StreamData(opts.payload); + this.secret.once('close', function () { + if (!this.payload.writable && this.readable) + this.sign(); + }.bind(this)); + + this.payload.once('close', function () { + if (!this.secret.writable && this.readable) + this.sign(); + }.bind(this)); } - - +util.inherits(StreamSign, Stream); +StreamSign.prototype.sign = function sign() { + const signature = jwsSign({ + header: this.header, + payload: this.payload.buffer, + secret: this.secret.buffer, + }); + this.emit('done', signature); + this.emit('data', signature); + this.emit('end'); + this.readable = false; + return signature; +}; + +function StreamVerify(opts) { + opts = opts || {}; + const secretOrKey = opts.secret||opts.publicKey||opts.key; + const secretStream = new StreamData(secretOrKey); + this.readable = true; + this.secret = this.publicKey = this.key = secretStream; + this.signature = new StreamData(opts.signature); + this.secret.once('close', function () { + if (!this.signature.writable && this.readable) + this.verify(); + }.bind(this)); + + this.signature.once('close', function () { + if (!this.secret.writable && this.readable) + this.verify(); + }.bind(this)); +} +util.inherits(StreamVerify, Stream); +StreamVerify.prototype.verify = function verify() { + const verified = jwsVerify(this.signature.buffer, this.key.buffer); + this.emit('done', verified); + this.emit('data', verified); + this.emit('end'); + this.readable = false; + return verified; +}; + +function StreamData(data) { + this.buffer = Buffer(data||0); + this.writable = true; + this.readable = true; + if (!data) + return this; + if (typeof data.pipe === 'function') + data.pipe(this); + else if (data.length) { + this.writable = false; + process.nextTick(function () { + this.buffer = data; + this.emit('end', data); + this.readable = false; + this.emit('close'); + }.bind(this)); + } +}; +util.inherits(StreamData, Stream); + +StreamData.prototype.write = function write(data) { + this.buffer = Buffer.concat([this.buffer, Buffer(data)]); + this.emit('data', data); +}; + +StreamData.prototype.end = function end(data) { + if (data) + this.write(data); + this.emit('end', data); + this.emit('close'); + this.writable = false; + this.readable = false; +}; \ No newline at end of file diff --git a/test/data.txt b/test/data.txt new file mode 100644 index 0000000..65073b2 --- /dev/null +++ b/test/data.txt @@ -0,0 +1 @@ +one, two, three diff --git a/test/jws.test.js b/test/jws.test.js index 02be50a..9b029d4 100644 --- a/test/jws.test.js +++ b/test/jws.test.js @@ -8,6 +8,10 @@ function readfile(path) { return fs.readFileSync(__dirname + '/' + path).toString(); } +function readstream(path) { + return fs.createReadStream(__dirname + '/' + path); +} + const rsaPrivateKey = readfile('rsa-private.pem'); const rsaPublicKey = readfile('rsa-public.pem'); const rsaWrongPublicKey = readfile('rsa-wrong-public.pem'); @@ -97,16 +101,109 @@ BITS.forEach(function (bits) { }); test('No digital signature or MAC value included', function (t) { - const header = { alg: 'none' }; - const payload = 'oh hey'; - const jwsObj = jws.sign({ - header: header, - payload: payload, - }); - const parts = jws.decode(jwsObj); - t.ok(jws.verify(jwsObj), 'should verify'); - t.ok(jws.verify(jwsObj, 'anything'), 'should still verify'); - t.same(parts.payload, payload, 'should match payload'); - t.same(parts.header, header, 'should match header'); + const header = { alg: 'none' }; + const payload = 'oh hey'; + const jwsObj = jws.sign({ + header: header, + payload: payload, + }); + const parts = jws.decode(jwsObj); + t.ok(jws.verify(jwsObj), 'should verify'); + t.ok(jws.verify(jwsObj, 'anything'), 'should still verify'); + t.same(parts.payload, payload, 'should match payload'); + t.same(parts.header, header, 'should match header'); + t.end(); +}); + +test('Streaming sign: HMAC', function (t) { + const dataStream = readstream('data.txt'); + const secret = 'shhhhh'; + const sig = jws.createSign({ + header: { alg: 'HS256' }, + secret: secret + }); + dataStream.pipe(sig.payload); + sig.on('done', function (signature) { + t.ok(jws.verify(signature, secret), 'should verify'); t.end(); + }); +}); + +test('Streaming sign: RSA', function (t) { + const dataStream = readstream('data.txt'); + const privateKeyStream = readstream('rsa-private.pem'); + const publicKey = rsaPublicKey; + const wrongPublicKey = rsaWrongPublicKey; + const sig = jws.createSign({ + header: { alg: 'RS256' }, + }); + dataStream.pipe(sig.payload); + + process.nextTick(function () { + privateKeyStream.pipe(sig.key); + }); + + sig.on('done', function (signature) { + t.ok(jws.verify(signature, publicKey), 'should verify'); + t.notOk(jws.verify(signature, wrongPublicKey), 'should not verify'); + t.same(jws.decode(signature).payload, readfile('data.txt'), 'got all the data'); + t.end(); + }); +}); + +test('Streaming sign: RSA, predefined streams', function (t) { + const dataStream = readstream('data.txt'); + const privateKeyStream = readstream('rsa-private.pem'); + const publicKey = rsaPublicKey; + const wrongPublicKey = rsaWrongPublicKey; + const sig = jws.createSign({ + header: { alg: 'RS256' }, + payload: dataStream, + privateKey: privateKeyStream + }); + sig.on('done', function (signature) { + t.ok(jws.verify(signature, publicKey), 'should verify'); + t.notOk(jws.verify(signature, wrongPublicKey), 'should not verify'); + t.same(jws.decode(signature).payload, readfile('data.txt'), 'got all the data'); + t.end(); + }); +}); + +test('Streaming verify: ECDSA', function (t) { + const dataStream = readstream('data.txt'); + const privateKeyStream = readstream('ec512-private.pem'); + const publicKeyStream = readstream('ec512-public.pem'); + const sigStream = jws.createSign({ + header: { alg: 'ES512' }, + payload: dataStream, + privateKey: privateKeyStream + }); + const verifier = jws.createVerify(); + sigStream.pipe(verifier.signature); + publicKeyStream.pipe(verifier.key); + + verifier.on('done', function (verified) { + t.ok(verified, 'should verify'); + t.end(); + }); +}); + +test('Streaming verify: ECDSA, with invalid key', function (t) { + const dataStream = readstream('data.txt'); + const privateKeyStream = readstream('ec512-private.pem'); + const publicKeyStream = readstream('ec512-wrong-public.pem'); + const sigStream = jws.createSign({ + header: { alg: 'ES512' }, + payload: dataStream, + privateKey: privateKeyStream + }); + const verifier = jws.createVerify({ + signature: sigStream, + publicKey: publicKeyStream, + }); + + verifier.on('done', function (verified) { + t.notOk(verified, 'should not verify'); + t.end(); + }); });