Skip to content

Instantly share code, notes, and snippets.

@myndzi
Last active August 29, 2015 14:01
Show Gist options
  • Save myndzi/49cde89af1ce37f11811 to your computer and use it in GitHub Desktop.
Save myndzi/49cde89af1ce37f11811 to your computer and use it in GitHub Desktop.
/* jshint node: true */
"use strict";
var Promise = require('bluebird');
var crypto = require('crypto');
var dgram = require('dgram');
var events = require('events');
var fs = require('fs');
var srp = require('srp');
var util = require('util');
require('date-utils');
var DBConn = Promise.promisify(require('./dbconn'));
var proto = require('./proto');
var readFile = Promise.promisify(fs.readFile, fs);
// AuthApp
//
// This object encompasses all functionality dealing with the UDP endpoint of
// charon. More specifically, the socket server lives here.
function AuthApp(config) {
// AuthApp is an EventEmitter
events.EventEmitter.call(this);
// AuthApp needs to be called from within a promise, so we should be throwing errors
if (!(this instanceof AuthApp))
throw new Error('Constructor called as a function');
['dbConnection', 'dbOptions', 'authPort'].forEach(function (prop) {
if (!(prop in config)) {
throw new Error('Missing ' + prop + ' in AuthApp configuration');
}
});
this.socket = null;
this.db = null;
}
util.inherits(AuthApp, EventEmitter);
// avoid using constructor
AuthApp.create = Promise.method(function (config) {
var app = new AuthApp();
return app.init({
dbConnection: config.dbConnection,
dbOptions: config.dbOptions
}, dbImport: config.dbImport)
.then(function () {
return app.listen(config.authPort);
});
});
// open the database and load any stuff you wanted to import
AuthApp.prototype.init = Promise.method(function (dbOpts, fileNames) {
var db = this.dbconn = new DBConn(dbOpts);
// DBConn.connect() should return a promise for the connected db object,
// i.e. the one you can call .query on, after it has been run through
// Promise.promisifyAll(db)
db.connect().then(function () {
if (!fileNames) { return; }
// load each file -- note that 'readFile' is a promisified function here
return Promise.map(fileNames, readFile).map(function (fileData) {
return data.split(';\n');
})
// get rid of empties and merge the statements from each file in order left to right
.reduce(function (allStatements, statement) {
if (statement.length) { allStatements.push(statement); }
return allStatements;
}, [ ])
// run all the queries, failing at first failure (should this be in a transaction?)
.all(function (statement) {
retutrn db.queryAsync(statement);
});
}).then(function () {
return this._listen();
});
});
AuthApp.prototype.listen = Promise.method(function (authPort) {
// doing this instead of promisifying every socket we make
// prefer promisifying sockets if you use their callback methods elsewhere though
var deferred = Promise.defer();
this.socket = dgram.createSocket('udp4');
// this.socket = Promise.promisifyAll(dgram.createSocket('udp4'));
this.socket.on('message', self.router.bind(self));
this.socket.bind(authPort, deferred.callback);
return deferred.promise;
});
// it's event handlers below here, so I stop using Promise.method()
// Router.
//
// Routes incoming requests to the proper function.
AuthApp.prototype.router = function(msg, rinfo) {
if (msg.length < 4) {
this.emit('error', new Error('Message is too small'));
return;
}
var packetType = msg.readUInt32LE(0);
switch (packetType) {
case proto.SERVER_NEGOTIATE:
this.serverNegotiate(msg, rinfo);
break;
case proto.SERVER_EPHEMERAL:
this.serverEphemeral(msg, rinfo);
break;
case proto.SERVER_PROOF:
this.serverProof(msg, rinfo);
break;
default:
this.emit('error', new Error('Message has invalid packet type'));
break;
}
};
// Server Negotiate Route
//
// This is the initial route that creates an authentication session.
AuthApp.prototype.serverNegotiate = function(msg, rinfo) {
var self = this;
// Unmarshall the server negotiation packet
var packet = proto.serverNegotiate.unmarshall(msg);
var username = packet.username;
// Create a new session for given user.
this.dbconn.newSession(username).then(function (data) {
// Write the response packet
return proto.authNegotiate.marshall({
session: data.session,
salt: data.salt,
username: data.username
});
}).catch(err) {
self.emit('error', err);
// FIXME: Specifically check for a "User does not exist" error.
return proto.userError.marshall({
username: username,
error: proto.USER_NO_EXIST
});
}).then(function (response) {
// Send the response packet to the sender
self.socket.send(response, 0, response.length, rinfo.port, rinfo.address);
})
};
// Server Ephemeral Route
//
// With the client ephemeral A value, generate an ephemeral number B to be sent
// back to the client.
AuthApp.prototype.serverEphemeral = function(msg, rinfo) {
var self = this;
// Unmarshall the server negotiation packet
var packet = proto.serverEphemeral.unmarshall(msg);
self.dbconn.findSession(packet.session, self.sessionTimeout)
.then(function (session) {
return srp.genKey(32).then(function () {
var srpServer = new srp.Server(
srp.params['2048'],
session.salt,
new Buffer(session.username, 'ascii'),
session.verifier,
secret
);
srpServer.setA(packet.ephemeral);
var serverEphemeral = srpServer.computeB();
// setEphemeral needs to return a promise in our new promisy thing
return self.dbconn.setEphemeral(
packet.session,
packet.ephemeral,
secret
).then(function () {
var response = proto.authEphemeral.marshall({
session: packet.session,
ephemeral: serverEphemeral
});
// we don't send an error reply to the socket here but we do above? is this correct?
self.socket.send(response, 0, response.length, rinfo.port, rinfo.address);
})
});
}).catch(self.emit.bind(self, 'error'));
};
// Server Proof Route
//
// Using the client M1, attempts to verify that the client is legitimate, and
// if so send the client an M2 that the client can use to verify that the auth
// server is who he says he is.
AuthApp.prototype.serverProof = function(msg, rinfo) {
var self = this;
// Unmarshall the server negotiation packet
var packet = proto.serverProof.unmarshall(msg);
self.dbconn.findSession(packet.session, self.sessionTimeout)
.then(function (session) {
// Recreate the necessary SRP state to check the client's proof.
var srpServer = new srp.Server(
srp.params['2048'],
session.salt,
new Buffer(session.username, 'ascii'),
session.verifier,
session.secret
);
var proof;
srpServer.setA(session.ephemeral);
proof = srpServer.checkM1(packet.proof);
return proto.authProof.marshall({
session: packet.session,
proof: proof
});
}).catch(function (err) {
return proto.sessionError.marshall({
session: packet.session,
error: proto.SESSION_AUTH_FAILED
});
}).then(function (response) {
self.socket.send(response, 0, response.length, rinfo.port, rinfo.address);
});
};
module.exports = AuthApp;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment