Skip to content

Commit

Permalink
pulled functionality out into unique files
Browse files Browse the repository at this point in the history
  • Loading branch information
fearphage committed Jan 29, 2015
1 parent d72fc3f commit 7880050
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 211 deletions.
219 changes: 8 additions & 211 deletions index.js
Original file line number Diff line number Diff line change
@@ -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);
};
Expand Down
40 changes: 40 additions & 0 deletions lib/data-stream.js
Original file line number Diff line number Diff line change
@@ -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;
59 changes: 59 additions & 0 deletions lib/sign-stream.js
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions lib/tostring.js
Original file line number Diff line number Diff line change
@@ -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);
};
Loading

0 comments on commit 7880050

Please sign in to comment.