serveros
Version:
Auth for networks of applications.
448 lines (426 loc) • 18.7 kB
JavaScript
var crypto = require('crypto')
, Encrypter = require('./Encrypter')
, MasterError = require('../errors/master')
, AuthError = require('../errors/auth')
;
/**
* A Serveros Authentication Master.
* @extends Serveros.Encrypter
* @class Serveros.ServerosMaster
*
* @param {Object} options
* @param {string} options.privateKey The Private Key for the Authentication Master, as a PEM encoded string. The matching
* Public Key should be distrubuted to Consumers and Service Providers alike.
* @param {Serveros.ServerosMaster~publicKeyFunction} options.publicKeyFunction A function by which the Authentication
* Master can ask for Public Keys.
* @param {string[]} [options.hashes={@link Serveros.Encrypter~hashes}] A list of acceptable hashes, in order of descending preference
* @param {string[]} [options.ciphers={@link Serveros.Encrypter~ciphers}] A list of acceptable Ciphers, in order of descending preference
*/
function ServerosMaster(options) { //myPrivateKey, publicKeyFunction, hashArr, cipherArr) {
var that = this;
this.privateKey = options.privateKey;
if (!!(options.hashes)) {
this.hashPrefs = options.hashes instanceof Array ? options.hashes : [options.hashes];
this.hashPrefs = this.hashPrefs.filter(function(hash) {
return that.hashCheck[hash];
});
}
if (!!(options.ciphers)) {
this.cipherPrefs = options.ciphers instanceof Array ? options.ciphers : [options.ciphers];
this.cipherPrefs = this.cipherPrefs.filter(function(cipher) {
return that.cipherCheck[cipher];
});
}
/**
* A function to ask for public Keys.
* @callback Serveros.ServerosMaster~publicKeyFunction
*
* @param {mixed} id The ID of the application whose public key is needed.
* @param {mixed} for The ID of the service this application wants to use.
* @param {Serveros.ServerosMaster~publicKeyResponse} callback a callback for the key data.
*/
this.publicKeyFunction = options.publicKeyFunction;
}
ServerosMaster.prototype = Object.create(Encrypter.prototype);
Object.defineProperty(ServerosMaster.prototype, 'constructor', {
enumerable: false
, value: ServerosMaster
});
/**
* Authenticate a consumer, and give it a ticket.
*
* @param {object} authentication message from over the wire.
* @param {Serveros.ServerosMaster~authenticateCallback} callback a callback for the authentication intormation.
*/
ServerosMaster.prototype.authenticate = function(authenticationMessage, callback) {
var that = this;
this.idecrypt(authenticationMessage.message, function(err, decrypted) {
if(err) {
callback(err);
return;
}
if(that.isStale(decrypted.ts)) {
callback(new AuthError.StaleError());
return;
}
/**
* A callback for the {@link SeverosMaster.publicKeyFunction}
* @callback Serveros.ServerosMaster~publicKeyResponse
*
* @param {object} error
* @param {object} data App Data.
* @param {mixed} data.id The id of the application.
* @param {mixed} data.publicKey The public key, or public keys of the requested application.
* @param {string[]} [data.hashes={@link Serveros.Encrypter~hashes}] A list of acceptable hashes, in order of descending preference
* @param {string[]} [data.ciphers={@link Serveros.Encrypter~ciphers}] A list of acceptable Ciphers, in order of descending preference
* @param {integer} data.keysLast The amount of time, in milliseconds, this application wants to honor keys.
* @param {mixed} data.authData Any additional data about the application that should be passed on to the service.
*/
try {
that.publicKeyFunction(decrypted.requester, decrypted.requested, function(err, requester) {
if (err || !requester) {
callback(new MasterError.ApplicationResolutionError("requester", err));
return;
}
that.iverify(requester.publicKey
, authenticationMessage.message
, decrypted.hash
, authenticationMessage.signature
, function(err, verified) {
if (err || !verified) {
callback(err || "Verification Returned False");
return;
}
that.getTicket(decrypted, requester, decrypted.chosen, function(err, ticket) {
if (err) {
callback(err);
return;
}
that.prepResponse(ticket, requester, verified.chosen, decrypted.chosen, function(err, response) {
if (err)
callback(err);
else
/**
* Passing back the authentcation information.
* @callback Serveros.ServerosMaster~authenticateCallback
*
* @param {Error.ServerosError} err any error preventing authentication.
* @param {object} response
*/
callback(null, response);
});
});
});
});
} catch (err) {
callback(new MasterError.PublicKeyFunctionError(err));
}
});
};
/**
* Build the ticket for response.
*
* @param {Object} request The decrypted request from over the wire.
* @param {Object} requested The Service being requested.
* @param {Object} requester The Consumer requesting access.
* @param {Serveros.ServerosMaster~buildTicketCallback} callback a callback for the ticket.
* @todo This function needs to be rewritten to support more interesting credentials. Specifically,
* these should include a key and IV for a cipher supported by both the requested and the requester,
* as well as an id and key suitable for use with HAWK, the chosen method for continued
* authentication between Consumer and Provider.
*/
ServerosMaster.prototype.buildTicket = function(request, requested, requester, callback) {
var that = this
, cipher = this.choose(requester.ciphers, requested.ciphers);
;
this.getOneTimeCredentials(cipher, function(err, credentials) {
if (err) {
if (callback) callback(err);
return;
}
that.shortUseKey(function(err, key) {
if (err) {
callback(err);
return;
}
that.shortUseKey(function(err, secret) {
if (err) {
callback(err);
return;
}
var ticketData = {
requester: request.requester
, requested: requested.id
, serverNonce: that.nonce()
, requesterNonce: request.nonce
, id: key.toString('base64')
, secret: secret.toString('base64')
, oneTimeCredentials: {
key: credentials.key.toString('base64')
, iv: credentials.iv.toString('base64')
, cipher: cipher
, hash: that.choose(requester.hashes, requested.hashes)
}
, hash: that.choose(requester.hashes, requested.hashes)
, "ts": new Date().getTime()
, expires: request.ts + requested.keysLast
, authData: requester.authData
}
/**
* Return ticket information.
* @callback Serveros.ServerosMaster~buildTicketCallback
*
* @param {Error.ServerosError} error any error that prevents ticket generation.
* @param {object} ticketData the ticket.
* @param {object} ticketData.requester The consumer asking for access.
* @param {object} ticketData.requested The service for which access is needed.
* @param {mixed} ticketData.serverNonce A new nonce.
* @param {mixed} ticketData.requesterNonce the nonce from the request.
* @param {string} ticketData.key A Key for consumer/service communication.
* @param {string} ticketData.secret A secret for consumer/service communication.
* @param {number} ticketData.ts The timestamp of ticket creation, in millis since the epoch.
* @param {string} ticketData.cipher The name of the selected Cipher algorithm.
* @param {string} ticketData.hash the name of the selected Hash algorithm.
* @param {number} ticketData.expires the expiration date of the keys contained herein.
* @param {mixed} ticketData.authData Authentication/Authorization data from the master application.
*/
callback(null, ticketData);
});
});
});
};
/**
* Get a ticket, encrypted and signed.
*
* @param {Object} request From the wire, decrypted.
* @param {Object} requester The application making the request.
* @param {Serveros.ServerosMaster~getTicketCallback} callback A callback for the eventual ticket.
*/
ServerosMaster.prototype.getTicket = function(request, requester, privateKeyNum, callback) {
if (typeof privateKeyNum != 'number')
privateKeyNum = 0;
var that = this
, privateKey = this.privateKey instanceof Array ? this.privateKey[privateKeyNum] : this.privateKey
;
this.publicKeyFunction(request.requested, null, function(err, requested) {
if (err || !requested) {
callback(new MasterError.ApplicationResolutionError("requested", err));
return;
}
that.buildTicket(request, requested, requester, function(err, ticket) {
if (err) {
callback(err);
return;
}
that.encryptAndSign(requested.publicKey
, privateKey
, JSON.stringify(ticket)
, that.chooseCipher(requested.ciphers)
, ticket.hash
, function(err, message) {
if (err)
callback(err);
else
/**
* Return the ticket.
* @callback Serveros.ServerosMaster~getTicketCallback
*
* @param {Error.ServerosError} err any error to prevent ticket generation.
* @param {object} ticket
* @param {object} ticket.raw The raw ticket.
* @param {object} ticket.ready The ticket, ecrypted and signed.
*/
callback(null, { raw: ticket
, ready: message
});
});
});
});
};
/**
* Prep a response to the server.
*
* @param {Object} ticket A ticket from {@link Serveros.ServerosMaster.getTicket}
* @param {Object} requester The application requesting the ticket
* @param {Serveros.ServerosMaster~prepReponseCallback} callback a callback for the signed, encrypted response.
*/
ServerosMaster.prototype.prepResponse = function(ticket, requester, publicKeyNum, privateKeyNum, callback) {
if (typeof publicKeyNum != 'number')
publicKeyNum = 0;
if (typeof privateKeyNum != 'number')
privateKeyNum = 0;
var that = this
, response = {
requester: ticket.raw.requester
, requested: ticket.raw.requested
, serverNonce: ticket.raw.serverNonce
, requesterNonce: ticket.raw.requesterNonce
, id: ticket.raw.id
, secret: ticket.raw.secret
, ts: ticket.raw.ts
, oneTimeCredentials: ticket.raw.oneTimeCredentials
, hash: this.chooseHash(requester.hashes)
, expires: ticket.raw.expires
, ticket: ticket.ready
}
, publicKey = requester.publicKey instanceof Array ? requester.publicKey[publicKeyNum] : requester.publicKey
, privateKey = this.privateKey instanceof Array ? this.privateKey[privateKeyNum] : this.privateKey
;
this.encryptAndSign(publicKey
, privateKey
, JSON.stringify(response)
, this.chooseCipher(requester.ciphers)
, response.hash
, function(err, encrypted) {
if (err)
callback(err);
else
/**
* Returns the desired response, encrypted and signed.
* @callback Serveros.ServerosMaster~prepResponseCallback
*
* @param {Error.ServerosError} err Any error
* @param {Object} response The signed and encrypted response.
*/
callback(null, encrypted);
});
};
/**
* Add an authentication endpoint (GET /authenticate) to an Express Application.
*
* @param {ExpressApplication} application an Express application.
*/
ServerosMaster.prototype.addAuthenticationEndpoint = function(application) {
var that = this;
application.get('/authenticate', function(req, res, next) {
var authRequest = JSON.parse(req.query.authRequest);
that.authenticate(authRequest, function(err, response) {
if (err) {
res.status(err.statusCode).json(err.prepResponseBody());
console.error(err.prepResponseBody());
if (err.err)
console.error(err.err.stack);
} else
res.json(response);
});
});
};
/**
* Choose the best entry between two sets.
*
* Attempts to choose the highest ranked (lowest indexed) entry in each set.
*
* @param {Array} setA The first (larger) set.
* @param {Array} setB The second (smaller) set.
*
* @return {mixed} The best choice between the two sets.
*/
ServerosMaster.prototype.choose = function(setA, setB) {
if (!(setA.length) && !(setB.length))
return null;
if (!(setA.length))
return setB[0];
if (!(setB.length))
return setA[0];
if (setA[0] == setB[0])
return setA[0];
if (setA.length < setB.length)
return this.choose(setB, setA); //Ensure that setB is strictly smaller than setA
var chosen = null
, score = setB.length + setA.length + 1;
for (var i = 0; i < setB.length; i++) {
var index = setA.indexOf(setB[i])
, currScore = index + i;
if (index > -1 && currScore < score) {
score = currScore;
chosen = setB[i];
}
}
return chosen;
}
/**
* Choose the best hash.
*
* @param {string[]} supported A list of desired Hashes.
*/
ServerosMaster.prototype.chooseHash = function(supported) {
return this.choose(this.hashPrefs, supported);
};
/**
* Choose the best ciphers.
*
* @param {string[]} supported A list of desired Ciphers.
*/
ServerosMaster.prototype.chooseCipher = function(supported) {
return this.choose(this.cipherPrefs, supported);
};
/**
* A simple wrapper around {@link Serveros.Encrypter~decrypt Lib.decrypt}
*
* @param {Buffer|String} data The output of a previous call to Encrypt
* @param {Serveros.Encrypter~decryptCallback} callback A callback for the eventual error or plaintext
*/
ServerosMaster.prototype.idecrypt = function(message, callback) {
this.decrypt(this.privateKey, message, callback);
};
/**
* A simple wrapper around {@link Serveros.Encrypter~encrypt Lib.decrypt}
*
* @param {Buffer|String} publicKey A PEM Encoded RSA Key (Public Key)
* @param {Object} message A Json Object to be encrypted.
* @param {String} cipher The cipher algorithm to use while enciphering.
* @param {Serveros.Encrypter~encryptCallback} callback A callback for the eventual error or ciphertext.
*/
ServerosMaster.prototype.iencrypt = function(publicKey, message, cipher, callback) {
this.encrypt(publicKey, JSON.stringify(message), cipher, callback);
};
/**
* A simple wrapper around {@link Serveros.Encrypter~sign Lib.decrypt}
*
* @param {Buffer|String} data The data to be signed.
* @param {String} hash The Hash algorithm to use whilst calculating the HMAC
* @param {Serveros.Encrypter~signCallback} callback A callback for the eventual error or signature
*/
ServerosMaster.prototype.isign = function(data, hash, callback) {
this.sign(this.privateKey, data, hash, callback);
};
/**
* A simple wrapper around {@link Serveros.Encrypter~verify Lib.decrypt}
*
* @param {Buffer|String} rsaKey A PEM Encoded RSA Key (Public Key)
* @param {Buffer|String} data The previously signed data.
* @param {String} algorithm The Hash algorithm to use whilst calculating the HMAC
* @param {Buffer|String} signature The previously generated Signature - as a buffer or base64
* encoded String.
* @param {Serveros.Encrypter~verifyCallback} callback A callback for the eventual error or verification Status
*/
ServerosMaster.prototype.iverify = function(rsaKey, data, algorithm, signature, callback) {
this.verify(rsaKey, data, algorithm, signature, callback);
};
/**
* A small wrapper around {@link Serveros.Encrypter~encryptAndSign Lib.encryptAndSign} which provides the correct
* local arguments.
*
* @param {Buffer|String} publicKey A PEM Encoded RSA Key (Public Key)
* @param {Object} message A JSON message to be encrypted.
* @param {String} cipher The cipher algorithm to use while enciphering.
* @param {String} hash The Hash algorithm to use whilst calculating the HMAC
* @param {Serveros.Encrypter~encryptAndSignCallback} callback A callback for the eventual error or encrypted/signed message.
*/
ServerosMaster.prototype.iencryptAndSign = function(rsaKey, message, cipher, hash, callback) {
try {
this.encryptAndSign(rsaKey
, this.privateKey
, JSON.stringify(message)
, cipher
, hash
, callback
);
} catch (err) {
if (callback)
process.nextTick(function() {
callback(new AuthError.JSONError(err));
});
}
};
module.exports = exports = ServerosMaster;