diff --git a/index.js b/index.js index 2383625..6052807 100644 --- a/index.js +++ b/index.js @@ -1,221 +1,18 @@ -/*global process, exports*/ -const Buffer = require('buffer').Buffer; -const Stream = require('stream'); -const util = require('util'); -const base64url = require('base64url'); -const jwa = require('jwa'); +/*global exports*/ +const SignStream = require('./lib/sign-stream'); +const VerifyStream = require('./lib/verify-stream'); const ALGORITHMS = [ 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', - 'ES256', 'ES384', 'ES512', + 'ES256', 'ES384', 'ES512' ]; -function toString(obj) { - if (typeof obj === 'string') - return obj; - if (typeof obj === 'number' || Buffer.isBuffer(obj)) - return obj.toString(); - return JSON.stringify(obj); -} - -function jwsSecuredInput(header, payload) { - const encodedHeader = base64url(toString(header), 'binary'); - const encodedPayload = base64url(toString(payload), 'binary'); - return util.format('%s.%s', encodedHeader, encodedPayload); -} - -function jwsSign(opts) { - const header = opts.header; - const payload = opts.payload; - const secretOrKey = opts.secret || opts.privateKey; - const algo = jwa(header.alg); - const securedInput = jwsSecuredInput(header, payload); - const signature = algo.sign(securedInput, secretOrKey); - return util.format('%s.%s', securedInput, signature); -} - -function isObject(thing) { - return Object.prototype.toString.call(thing) === '[object Object]'; -} - -function safeJsonParse(thing) { - if (isObject(thing)) - return thing; - try { return JSON.parse(thing) } - catch (e) { return undefined } -} - -function headerFromJWS(jwsSig) { - const encodedHeader = jwsSig.split('.', 1)[0]; - return safeJsonParse(base64url.decode(encodedHeader, 'binary')); -} - -function securedInputFromJWS(jwsSig) { - return jwsSig.split('.', 2).join('.'); -} - -function algoFromJWS(jwsSig) { - var err; - const header = headerFromJWS(jwsSig); - if (typeof header != 'object') { - err = new Error("Invalid token: no header in signature '" + jwsSig + "'"); - err.code = "MISSING_HEADER"; - err.signature = jwsSig; - throw err; - } - if (!header.alg) { - err = new Error("Missing `alg` field in header for signature '"+ jwsSig +"'"); - err.code = "MISSING_ALGORITHM"; - err.header = header; - err.signature = jwsSig; - throw err; - } - return header.alg; -} - -function signatureFromJWS(jwsSig) { - return jwsSig.split('.')[2]; -} - -function payloadFromJWS(jwsSig) { - const payload = jwsSig.split('.')[1]; - return base64url.decode(payload, 'binary'); -} - -const JWS_REGEX = /^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/; -function isValidJws(string) { - if (!JWS_REGEX.test(string)) - return false; - if (!headerFromJWS(string)) - return false; - return true; -} - -function jwsVerify(jwsSig, secretOrKey) { - jwsSig = toString(jwsSig); - const signature = signatureFromJWS(jwsSig); - const securedInput = securedInputFromJWS(jwsSig); - const algo = jwa(algoFromJWS(jwsSig)); - return algo.verify(securedInput, signature, secretOrKey); -} - -function jwsDecode(jwsSig, opts) { - opts = opts || {}; - jwsSig = toString(jwsSig); - if (!isValidJws(jwsSig)) - return null; - const header = headerFromJWS(jwsSig); - if (!header) - return null; - var payload = payloadFromJWS(jwsSig); - if (header.typ === 'JWT' || opts.json) - payload = JSON.parse(payload); - return { - header: header, - payload: payload, - signature: signatureFromJWS(jwsSig), - }; -} - -function SignStream(opts) { - const secret = opts.secret||opts.privateKey||opts.key; - const secretStream = new DataStream(secret); - this.readable = true; - this.header = opts.header; - this.secret = this.privateKey = this.key = secretStream; - this.payload = new DataStream(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(SignStream, Stream); -SignStream.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 VerifyStream(opts) { - opts = opts || {}; - const secretOrKey = opts.secret||opts.publicKey||opts.key; - const secretStream = new DataStream(secretOrKey); - this.readable = true; - this.secret = this.publicKey = this.key = secretStream; - this.signature = new DataStream(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(VerifyStream, Stream); -VerifyStream.prototype.verify = function verify() { - const valid = jwsVerify(this.signature.buffer, this.key.buffer); - const obj = jwsDecode(this.signature.buffer); - this.emit('done', valid, obj); - this.emit('data', valid); - this.emit('end'); - this.readable = false; - return valid; -}; - -function DataStream(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(DataStream, Stream); - -DataStream.prototype.write = function write(data) { - this.buffer = Buffer.concat([this.buffer, Buffer(data)]); - this.emit('data', data); -}; - -DataStream.prototype.end = function end(data) { - if (data) - this.write(data); - this.emit('end', data); - this.emit('close'); - this.writable = false; - this.readable = false; -}; - exports.ALGORITHMS = ALGORITHMS; -exports.sign = jwsSign; -exports.verify = jwsVerify; -exports.decode = jwsDecode; -exports.isValid = isValidJws; +exports.sign = SignStream.sign; +exports.verify = VerifyStream.verify; +exports.decode = VerifyStream.decode; +exports.isValid = VerifyStream.isValid; exports.createSign = function createSign(opts) { return new SignStream(opts); }; diff --git a/lib/data-stream.js b/lib/data-stream.js new file mode 100644 index 0000000..a50ac04 --- /dev/null +++ b/lib/data-stream.js @@ -0,0 +1,40 @@ +/*global module, process*/ +const Buffer = require('buffer').Buffer; +const Stream = require('stream'); +const util = require('util'); + +function DataStream(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(DataStream, Stream); + +DataStream.prototype.write = function write(data) { + this.buffer = Buffer.concat([this.buffer, Buffer(data)]); + this.emit('data', data); +}; + +DataStream.prototype.end = function end(data) { + if (data) + this.write(data); + this.emit('end', data); + this.emit('close'); + this.writable = false; + this.readable = false; +}; + +module.exports = DataStream; diff --git a/lib/sign-stream.js b/lib/sign-stream.js new file mode 100644 index 0000000..4c3ab31 --- /dev/null +++ b/lib/sign-stream.js @@ -0,0 +1,59 @@ +/*global module*/ +const base64url = require('base64url'); +const DataStream = require('./data-stream'); +const jwa = require('jwa'); +const Stream = require('stream'); +const toString = require('./tostring'); +const util = require('util'); + +function jwsSecuredInput(header, payload) { + const encodedHeader = base64url(toString(header), 'binary'); + const encodedPayload = base64url(toString(payload), 'binary'); + return util.format('%s.%s', encodedHeader, encodedPayload); +} + +function jwsSign(opts) { + const header = opts.header; + const payload = opts.payload; + const secretOrKey = opts.secret || opts.privateKey; + const algo = jwa(header.alg); + const securedInput = jwsSecuredInput(header, payload); + const signature = algo.sign(securedInput, secretOrKey); + return util.format('%s.%s', securedInput, signature); +} + +function SignStream(opts) { + const secret = opts.secret||opts.privateKey||opts.key; + const secretStream = new DataStream(secret); + this.readable = true; + this.header = opts.header; + this.secret = this.privateKey = this.key = secretStream; + this.payload = new DataStream(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(SignStream, Stream); + +SignStream.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; +}; + +SignStream.sign = jwsSign; + +module.exports = SignStream; diff --git a/lib/tostring.js b/lib/tostring.js new file mode 100644 index 0000000..3709f9b --- /dev/null +++ b/lib/tostring.js @@ -0,0 +1,10 @@ +/*global module*/ +const Buffer = require('buffer').Buffer; + +module.exports = function toString(obj) { + if (typeof obj === 'string') + return obj; + if (typeof obj === 'number' || Buffer.isBuffer(obj)) + return obj.toString(); + return JSON.stringify(obj); +}; diff --git a/lib/verify-stream.js b/lib/verify-stream.js new file mode 100644 index 0000000..87cbd52 --- /dev/null +++ b/lib/verify-stream.js @@ -0,0 +1,120 @@ +/*global module*/ +const base64url = require('base64url'); +const DataStream = require('./data-stream'); +const jwa = require('jwa'); +const Stream = require('stream'); +const toString = require('./tostring'); +const util = require('util'); +const JWS_REGEX = /^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/; + +function isObject(thing) { + return Object.prototype.toString.call(thing) === '[object Object]'; +} + +function safeJsonParse(thing) { + if (isObject(thing)) + return thing; + try { return JSON.parse(thing); } + catch (e) { return undefined; } +} + +function headerFromJWS(jwsSig) { + const encodedHeader = jwsSig.split('.', 1)[0]; + return safeJsonParse(base64url.decode(encodedHeader, 'binary')); +} + +function securedInputFromJWS(jwsSig) { + return jwsSig.split('.', 2).join('.'); +} + +function algoFromJWS(jwsSig) { + var err; + const header = headerFromJWS(jwsSig); + if (typeof header != 'object') { + err = new Error("Invalid token: no header in signature '" + jwsSig + "'"); + err.code = "MISSING_HEADER"; + err.signature = jwsSig; + throw err; + } + if (!header.alg) { + err = new Error("Missing `alg` field in header for signature '"+ jwsSig +"'"); + err.code = "MISSING_ALGORITHM"; + err.header = header; + err.signature = jwsSig; + throw err; + } + return header.alg; +} + +function signatureFromJWS(jwsSig) { + return jwsSig.split('.')[2]; +} + +function payloadFromJWS(jwsSig) { + const payload = jwsSig.split('.')[1]; + return base64url.decode(payload, 'binary'); +} + +function isValidJws(string) { + return JWS_REGEX.test(string) && !!headerFromJWS(string); +} + +function jwsVerify(jwsSig, secretOrKey) { + jwsSig = toString(jwsSig); + const signature = signatureFromJWS(jwsSig); + const securedInput = securedInputFromJWS(jwsSig); + const algo = jwa(algoFromJWS(jwsSig)); + return algo.verify(securedInput, signature, secretOrKey); +} + +function jwsDecode(jwsSig, opts) { + opts = opts || {}; + jwsSig = toString(jwsSig); + if (!isValidJws(jwsSig)) + return null; + const header = headerFromJWS(jwsSig); + if (!header) + return null; + var payload = payloadFromJWS(jwsSig); + if (header.typ === 'JWT' || opts.json) + payload = JSON.parse(payload); + return { + header: header, + payload: payload, + signature: signatureFromJWS(jwsSig) + }; +} + +function VerifyStream(opts) { + opts = opts || {}; + const secretOrKey = opts.secret||opts.publicKey||opts.key; + const secretStream = new DataStream(secretOrKey); + this.readable = true; + this.secret = this.publicKey = this.key = secretStream; + this.signature = new DataStream(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(VerifyStream, Stream); +VerifyStream.prototype.verify = function verify() { + const valid = jwsVerify(this.signature.buffer, this.key.buffer); + const obj = jwsDecode(this.signature.buffer); + this.emit('done', valid, obj); + this.emit('data', valid); + this.emit('end'); + this.readable = false; + return valid; +}; + +VerifyStream.decode = jwsDecode; +VerifyStream.isValid = isValidJws; +VerifyStream.verify = jwsVerify; + +module.exports = VerifyStream;