UNPKG

radius

Version:
887 lines (721 loc) 24 kB
var fs = require("fs"); var util = require("util"); var crypto = require("crypto"); var path = require("path"); var Radius = {}; var attributes_map = {}, vendor_name_to_id = {}; var dictionary_locations = [path.normalize(__dirname + "/../dictionaries")]; const NOT_LOADED = 1; const LOADING = 2; const LOADED = 3; var dictionaries_state = NOT_LOADED; const NO_VENDOR = -1; const ATTR_ID = 0; const ATTR_NAME = 1; const ATTR_TYPE = 2; const ATTR_ENUM = 3; const ATTR_REVERSE_ENUM = 4; const ATTR_MODIFIERS = 5; const AUTH_START = 4; const AUTH_END = 20; const AUTH_LENGTH = 16; const MESSAGE_AUTHENTICATOR_LENGTH = 16; Radius.InvalidSecretError = function(msg, decoded, constr) { Error.captureStackTrace(this, constr || this); this.message = msg || 'Error'; this.decoded = decoded; }; util.inherits(Radius.InvalidSecretError, Error); Radius.InvalidSecretError.prototype.name = 'Invalid Secret Error'; Radius.add_dictionary = function(file) { dictionary_locations.push(path.resolve(file)); }; var load_dictionaries_cbs = []; Radius.load_dictionaries = function() { var self = this; if (dictionaries_state == LOADED) { return; } dictionary_locations.forEach(function(file) { if (!fs.existsSync(file)) { throw new Error("Invalid dictionary location: " + file); } if (fs.statSync(file).isDirectory()) { var files = fs.readdirSync(file); for (var j = 0; j < files.length; j++) { self.load_dictionary(file + "/" + files[j]); } } else { self.load_dictionary(file); } }); dictionaries_state = LOADED; }; Radius.load_dictionary = function(file, seen_files) { file = path.normalize(file); var self = this; if (seen_files === undefined) { seen_files = {}; } if (seen_files[file]) { return; } seen_files[file] = true; var includes = self._load_dictionary(fs.readFileSync(file, "ascii")); includes.forEach(function (i) { self.load_dictionary(path.join(path.dirname(file), i), seen_files); }); }; Radius._load_dictionary = function(content) { var lines = content.split("\n"); var vendor = NO_VENDOR, includes = [], attr_vendor; for (var i = 0; i < lines.length; i++) { var line = lines[i]; line = line.replace(/#.*/, "").replace(/\s+/g, " "); var match = line.match(/^\s*VENDOR\s+(\S+)\s+(\d+)/); if (match) { vendor_name_to_id[match[1]] = match[2]; continue; } if ((match = line.match(/^\s*BEGIN-VENDOR\s+(\S+)/))) { vendor = vendor_name_to_id[match[1]]; continue; } if (line.match(/^\s*END-VENDOR/)) { vendor = NO_VENDOR; continue; } var init_entry = function(vendor, attr_id) { if (!attributes_map[vendor]) { attributes_map[vendor] = {}; } if (!attributes_map[vendor][attr_id]) { attributes_map[vendor][attr_id] = [null, null, null, {}, {}, {}]; } }; match = line.match(/^\s*(?:VENDORATTR\s+(\d+)|ATTRIBUTE)\s+(\S+)\s+(\d+)\s+(\S+)\s*(.+)?/); if (match) { attr_vendor = vendor; if (match[1] !== undefined) { attr_vendor = match[1]; } var modifiers = {}; if (match[5] !== undefined) { match[5].replace(/\s*/g, "").split(",").forEach(function(m) { modifiers[m] = true; }); } init_entry(attr_vendor, match[3]); attributes_map[attr_vendor][match[3]][ATTR_ID] = match[3]; attributes_map[attr_vendor][match[3]][ATTR_NAME] = match[2]; attributes_map[attr_vendor][match[3]][ATTR_TYPE] = match[4].toLowerCase(); attributes_map[attr_vendor][match[3]][ATTR_MODIFIERS] = modifiers; var by_name = attributes_map[attr_vendor][match[2]]; if (by_name !== undefined) { var by_index = attributes_map[attr_vendor][match[3]]; [ATTR_ENUM, ATTR_REVERSE_ENUM].forEach(function(field) { for (var name in by_name[field]) { by_index[field][name] = by_name[field][name]; } }); } attributes_map[attr_vendor][match[2]] = attributes_map[attr_vendor][match[3]]; continue; } match = line.match(/^\s*(?:VENDOR)?VALUE\s+(\d+)?\s*(\S+)\s+(\S+)\s+(\d+)/); if (match) { attr_vendor = vendor; if (match[1] !== undefined) { attr_vendor = match[1]; } init_entry(attr_vendor, match[2]); attributes_map[attr_vendor][match[2]][ATTR_ENUM][match[4]] = match[3]; attributes_map[attr_vendor][match[2]][ATTR_REVERSE_ENUM][match[3]] = match[4]; continue; } if ((match = line.match(/^\s*\$INCLUDE\s+(.*)/))) { includes.push(match[1]); } } return includes; }; Radius.unload_dictionaries = function() { attributes_map = {}; vendor_name_to_id = {}; dictionaries_state = NOT_LOADED; }; Radius.attr_name_to_id = function(attr_name, vendor_id) { return this._attr_to(attr_name, vendor_id, ATTR_ID); }; Radius.attr_id_to_name = function(attr_name, vendor_id) { return this._attr_to(attr_name, vendor_id, ATTR_NAME); }; Radius.vendor_name_to_id = function(vendor_name) { return vendor_name_to_id[vendor_name]; }; Radius._attr_to = function(attr, vendor_id, target) { if (vendor_id === undefined) { vendor_id = NO_VENDOR; } if (!attributes_map[vendor_id]) { return; } var attr_info = attributes_map[vendor_id][attr]; if (!attr_info) { return; } return attr_info[target]; }; var code_map = { 1: "Access-Request", 2: "Access-Accept", 3: "Access-Reject", 4: "Accounting-Request", 5: "Accounting-Response", 6: "Interim-Accounting", 7: "Password-Request", 8: "Password-Ack", 9: "Password-Reject", 10: "Accounting-Message", 11: "Access-Challenge", 12: "Status-Server", 13: "Status-Client", 21: "Resource-Free-Request", 22: "Resource-Free-Response", 23: "Resource-Query-Request", 24: "Resource-Query-Response", 25: "Alternate-Resource-Reclaim-Request", 26: "NAS-Reboot-Request", 27: "NAS-Reboot-Response", 29: "Next-Passcode", 30: "New-Pin", 31: "Terminate-Session", 32: "Password-Expired", 33: "Event-Request", 34: "Event-Response", 40: "Disconnect-Request", 41: "Disconnect-ACK", 42: "Disconnect-NAK", 43: "CoA-Request", 44: "CoA-ACK", 45: "CoA-NAK", 50: "IP-Address-Allocate", 51: "IP-Address-Release" }; var uses_random_authenticator = { "Access-Request": true, "Status-Server": true }; var is_request_code = { "Status-Server": true }; var reverse_code_map = {}; for (var code in code_map) { reverse_code_map[code_map[code]] = code; if (code_map[code].match(/Request/)) { is_request_code[code_map[code]] = true; } } Radius.error = function(error_msg) { var err = error_msg; if (typeof(error_msg) === 'string') { err = new Error(error_msg); } throw err; }; // this is a convenience method, "decode({..., no_secret: true})" will also do the job Radius.decode_without_secret = function(args) { // copy args' fields without modifiying the orginal var nargs = {no_secret: true}; for (var p in args) { nargs[p] = args[p]; } return this.decode(nargs, this._decode); }; Radius.decode = function(args) { this.load_dictionaries(); var packet = args.packet; if (!packet || packet.length < 4) { this.error("decode: packet too short"); return; } var ret = {}; ret.code = code_map[packet.readUInt8(0)]; if (!ret.code) { this.error("decode: invalid packet code '" + packet.readUInt8(0) + "'"); return; } ret.identifier = packet.readUInt8(1); ret.length = packet.readUInt16BE(2); if (packet.length < ret.length) { this.error("decode: incomplete packet"); return; } this.authenticator = ret.authenticator = packet.slice(AUTH_START, AUTH_END); this.no_secret = args.no_secret; this.secret = args.secret; var attrs = packet.slice(AUTH_END, ret.length); ret.attributes = {}; ret.raw_attributes = []; try { this.decode_attributes(attrs, ret.attributes, NO_VENDOR, ret.raw_attributes); } catch(err) { this.error(err); return; } if (!uses_random_authenticator[ret.code] && is_request_code[ret.code] && !args.no_secret) { var orig_authenticator = new Buffer(AUTH_LENGTH); packet.copy(orig_authenticator, 0, AUTH_START, AUTH_END); packet.fill(0, AUTH_START, AUTH_END); var checksum = this.calculate_packet_checksum(packet, args.secret); orig_authenticator.copy(packet, AUTH_START); if (checksum.toString() != this.authenticator.toString()) { this.error(new Radius.InvalidSecretError("decode: authenticator mismatch (possible shared secret mismatch)", ret)); return; } } if (is_request_code[ret.code] && ret.attributes["Message-Authenticator"] && !args.no_secret) { this._verify_request_message_authenticator(args, ret); } return ret; }; Radius.zero_out_message_authenticator = function(attributes) { var ma_id = this.attr_name_to_id("Message-Authenticator"); var new_attrs = attributes.slice(0); for (var i = 0; i < new_attrs.length; i++) { var attr = new_attrs[i]; if (attr[0] == ma_id) { new_attrs[i] = [ma_id, new Buffer(MESSAGE_AUTHENTICATOR_LENGTH)]; new_attrs[i][1].fill(0x00); break; } } return new_attrs; }; Radius._verify_request_message_authenticator = function(args, request) { var reencoded = this.encode({ code: request.code, attributes: this.zero_out_message_authenticator(request.raw_attributes), identifier: request.identifier, secret: args.secret }); request.authenticator.copy(reencoded, AUTH_START); var orig_ma = request.attributes["Message-Authenticator"]; var expected_ma = this.calculate_message_authenticator(reencoded, args.secret); if (orig_ma.toString() != expected_ma.toString()) { this.error(new Radius.InvalidSecretError("decode: Message-Authenticator mismatch (possible shared secret mismatch)", request)); } }; Radius.verify_response = function(args) { this.load_dictionaries(); if (!args || !Buffer.isBuffer(args.request) || !Buffer.isBuffer(args.response)) { this.error("verify_response: must provide raw request and response packets"); return; } if (args.secret == null) { this.error("verify_response: must specify shared secret"); return; } // first verify authenticator var got_checksum = new Buffer(AUTH_LENGTH); args.response.copy(got_checksum, 0, AUTH_START, AUTH_END); args.request.copy(args.response, AUTH_START, AUTH_START, AUTH_END); var expected_checksum = this.calculate_packet_checksum(args.response, args.secret); got_checksum.copy(args.response, AUTH_START); if (expected_checksum.toString() != args.response.slice(AUTH_START, AUTH_END).toString()) { return false; } return this._verify_response_message_authenticator(args); }; Radius._verify_response_message_authenticator = function(args) { var parsed_request = this.decode({ packet: args.request, secret: args.secret }); if (parsed_request.attributes["Message-Authenticator"]) { var parsed_response = this.decode({ packet: args.response, secret: args.secret }); var got_ma = parsed_response.attributes["Message-Authenticator"]; if (!got_ma) { return false; } var expected_response = this.encode({ secret: args.secret, code: parsed_response.code, identifier: parsed_response.identifier, attributes: this.zero_out_message_authenticator(parsed_response.raw_attributes) }); parsed_request.authenticator.copy(expected_response, AUTH_START); var expected_ma = this.calculate_message_authenticator(expected_response, args.secret); if (expected_ma.toString() != got_ma.toString()) { return false; } } return true; }; Radius.decode_attributes = function(data, attr_hash, vendor, raw_attrs) { var type, length, value, tag; while (data.length > 0) { type = data.readUInt8(0); length = data.readUInt8(1); value = data.slice(2, length); tag = undefined; if (length < 2) { throw new Error("invalid attribute length: " + length); } if (raw_attrs) { raw_attrs.push([type, value]); } data = data.slice(length); var attr_info = attributes_map[vendor] && attributes_map[vendor][type]; if (!attr_info) { continue; } if (attr_info[ATTR_MODIFIERS]["has_tag"]) { var first_byte = value.readUInt8(0); if (first_byte <= 0x1F) { tag = first_byte; value = value.slice(1); } } if (attr_info[ATTR_MODIFIERS]["encrypt=1"]) { value = this.decrypt_field(value); } else { switch (attr_info[ATTR_TYPE]) { case "string": case "text": // assumes utf8 encoding for strings value = value.toString("utf8"); break; case "ipaddr": var octets = []; for (var i = 0; i < value.length; i++) { octets.push(value[i]); } value = octets.join("."); break; case "date": value = new Date(value.readUInt32BE(0) * 1000); break; case "time": case "integer": if (attr_info[ATTR_MODIFIERS]["has_tag"]) { var buf = new Buffer([0, 0, 0, 0]); value.copy(buf, 1); value = buf; } value = value.readUInt32BE(0); value = attr_info[ATTR_ENUM][value] || value; break; } if (attr_info[ATTR_NAME] == "Vendor-Specific") { if (value[0] !== 0x00) { throw new Error("Invalid vendor id"); } var vendor_attrs = attr_hash["Vendor-Specific"]; if (!vendor_attrs) { vendor_attrs = attr_hash["Vendor-Specific"] = {}; } this.decode_attributes(value.slice(4), vendor_attrs, value.readUInt32BE(0)); continue; } } if (tag !== undefined) { value = [tag, value]; } if (attr_hash[attr_info[ATTR_NAME]] !== undefined) { if (!(attr_hash[attr_info[ATTR_NAME]] instanceof Array)) { attr_hash[attr_info[ATTR_NAME]] = [attr_hash[attr_info[ATTR_NAME]]]; } attr_hash[attr_info[ATTR_NAME]].push(value); } else { attr_hash[attr_info[ATTR_NAME]] = value; } } }; Radius.decrypt_field = function(field) { if (this.no_secret) { return null; } if (field.length < 16) { throw new Error("Invalid password: too short"); } if (field.length > 128) { throw new Error("Invalid password: too long"); } if (field.length % 16 != 0) { throw new Error("Invalid password: not padded"); } var decrypted = this._crypt_field(field, true); if (decrypted === null) return null; return decrypted.toString("utf8"); }; Radius.encrypt_field = function(field) { var len = Buffer.byteLength(field, 'utf8'); var buf = new Buffer(len + 15 - ((15 + len) % 16)); buf.write(field, 0, len); // null-out the padding for (var i = len; i < buf.length; i++) { buf[i] = 0x00; } return this._crypt_field(buf, false); }; Radius._crypt_field = function(field, is_decrypt) { var ret = new Buffer(0); var second_part_to_be_hashed = this.authenticator; if (this.secret === undefined) { throw new Error("Must provide RADIUS shared secret"); } for (var i = 0; i < field.length; i = i + 16) { var hasher = crypto.createHash("md5"); hasher.update(this.secret); hasher.update(second_part_to_be_hashed); var hash = new Buffer(hasher.digest("binary"), "binary"); var xor_result = new Buffer(16); for (var j = 0; j < 16; j++) { xor_result[j] = field[i + j] ^ hash[j]; if (is_decrypt && xor_result[j] == 0x00) { xor_result = xor_result.slice(0, j); break; } } ret = Buffer.concat([ret, xor_result]); second_part_to_be_hashed = is_decrypt ? field.slice(i, i + 16) : xor_result; } return ret; }; Radius.encode_response = function(args) { this.load_dictionaries(); var packet = args.packet; if (!packet) { this.error("encode_response: must provide packet"); return; } if (!args.attributes) { args.attributes = []; } var proxy_state_id = this.attr_name_to_id("Proxy-State"); for (var i = 0; i < packet.raw_attributes.length; i++) { var attr = packet.raw_attributes[i]; if (attr[0] == proxy_state_id) { args.attributes.push(attr); } } var response = this.encode({ code: args.code, identifier: packet.identifier, authenticator: packet.authenticator, attributes: args.attributes, secret: args.secret, add_message_authenticator: packet.attributes["Message-Authenticator"] != null }); return response; }; Radius.encode = function(args) { this.load_dictionaries(); if (!args || args.code === undefined) { this.error("encode: must specify code"); return; } if (args.secret === undefined) { this.error("encode: must provide RADIUS shared secret"); return; } var packet = new Buffer(4096); var offset = 0; var code = reverse_code_map[args.code]; if (code === undefined) { this.error("encode: invalid packet code '" + args.code + "'"); return; } packet.writeUInt8(+code, offset++); var identifier = args.identifier; if (identifier === undefined) { identifier = Math.floor(Math.random() * 256); } if (identifier > 255) { this.error("encode: identifier too large"); return; } packet.writeUInt8(identifier, offset++); // save room for length offset += 2; var authenticator = args.authenticator; if (!authenticator) { if (uses_random_authenticator[args.code]) { authenticator = crypto.randomBytes(AUTH_LENGTH); } else { authenticator = new Buffer(AUTH_LENGTH); authenticator.fill(0x00); } } return this._encode_with_authenticator(args, packet, offset, authenticator); }; Radius._encode_with_authenticator = function(args, packet, offset, authenticator) { authenticator.copy(packet, offset); offset += AUTH_LENGTH; this.secret = args.secret; this.no_secret = false; this.authenticator = authenticator; args.attributes = this.ensure_array_attributes(args.attributes); var add_message_authenticator = args.add_message_authenticator; if (add_message_authenticator == null) { var eap_id = this.attr_name_to_id("EAP-Message"); var ma_id = this.attr_name_to_id("Message-Authenticator"); for (var i = 0; i < args.attributes.length; i++) { var attr_id = args.attributes[i][0]; if (attr_id == eap_id || attr_id == "EAP-Message") { add_message_authenticator = true; } else if (attr_id == ma_id || attr_id == "Message-Authenticator") { add_message_authenticator = false; break; } } if (add_message_authenticator == null && args.code == "Status-Server") { add_message_authenticator = true; } } if (add_message_authenticator) { var empty_authenticator = new Buffer(MESSAGE_AUTHENTICATOR_LENGTH); empty_authenticator.fill(0x00); args.attributes.push(["Message-Authenticator", empty_authenticator]); } try { offset += this.encode_attributes(packet.slice(offset), args.attributes, NO_VENDOR); } catch (err) { this.error(err); return; } // now write the length in packet.writeUInt16BE(offset, 2); packet = packet.slice(0, offset); var message_authenticator; if (add_message_authenticator && !is_request_code[args.code]) { message_authenticator = this.calculate_message_authenticator(packet, args.secret); message_authenticator.copy(packet, offset - MESSAGE_AUTHENTICATOR_LENGTH); } if (!uses_random_authenticator[args.code]) { this.calculate_packet_checksum(packet, args.secret).copy(packet, AUTH_START); } if (add_message_authenticator && is_request_code[args.code]) { message_authenticator = this.calculate_message_authenticator(packet, args.secret); message_authenticator.copy(packet, offset - MESSAGE_AUTHENTICATOR_LENGTH); } return packet; }; Radius.calculate_message_authenticator = function(packet, secret) { var hmac = crypto.createHmac('md5', secret); hmac.update(packet); return new Buffer(hmac.digest('binary'), 'binary'); }; Radius.calculate_packet_checksum = function(packet, secret) { var hasher = crypto.createHash("md5"); hasher.update(packet); hasher.update(secret); return new Buffer(hasher.digest("binary"), "binary"); }; Radius.ensure_array_attributes = function(attributes) { if (!attributes) { return []; } if (typeof(attributes) == 'object' && !Array.isArray(attributes)) { var array_attributes = []; for (var name in attributes) { var val = attributes[name]; if (typeof(val) == 'object') { throw new Error("Cannot have nested attributes when using hash syntax. Use array syntax instead"); } array_attributes.push([name, val]); } return array_attributes; } return attributes; }; Radius.encode_attributes = function(packet, attributes, vendor) { var offset = 0; for (var i = 0; i < attributes.length; i++) { var attr = attributes[i]; var attr_info = attributes_map[vendor] && attributes_map[vendor][attr[0]]; if (!attr_info && !(attr[1] instanceof Buffer)) { throw new Error("encode: invalid attributes - must give Buffer for " + "unknown attribute '" + attr[0] + "'"); } var out_value, in_value = attr[1]; if (in_value instanceof Buffer) { out_value = in_value; } else { var has_tag = attr_info[ATTR_MODIFIERS]["has_tag"] && attr.length == 3; if (has_tag) { in_value = attr[2]; } if (attr_info[ATTR_MODIFIERS]["encrypt=1"]) { out_value = this.encrypt_field(in_value); } else { switch (attr_info[ATTR_TYPE]) { case "string": case "text": if (in_value.length == 0) { continue; } out_value = new Buffer(in_value + "", "utf8"); break; case "ipaddr": out_value = new Buffer(in_value.split(".")); if (out_value.length != 4) { throw new Error("encode: invalid IP: " + in_value); } break; case "date": in_value = Math.floor(in_value.getTime() / 1000); case "time": case "integer": out_value = new Buffer(4); in_value = attr_info[ATTR_REVERSE_ENUM][in_value] || in_value; if (isNaN(in_value)) { throw new Error("envode: invalid attribute value: " + in_value); } out_value.writeUInt32BE(+in_value, 0); if (has_tag) { out_value = out_value.slice(1); } break; default: if (attr_info[ATTR_NAME] != "Vendor-Specific") { throw new Error("encode: must provide Buffer for attribute '" + attr_info[ATTR_NAME] + "'"); } } // handle VSAs specially if (attr_info[ATTR_NAME] == "Vendor-Specific") { var vendor_id = isNaN(attr[1]) ? vendor_name_to_id[attr[1]] : attr[1]; if (vendor_id === undefined) { throw new Error("encode: unknown vendor '" + attr[1] + "'"); } // write the attribute id packet.writeUInt8(+attr_info[ATTR_ID], offset++); var length = this.encode_attributes(packet.slice(offset + 5), attr[2], vendor_id); // write in the length packet.writeUInt8(2 + 4 + length, offset++); // write in the vendor id packet.writeUInt32BE(+vendor_id, offset); offset += 4; offset += length; continue; } } } // write the attribute id packet.writeUInt8(attr_info ? +attr_info[ATTR_ID] : +attr[0], offset++); // write in the attribute length packet.writeUInt8(2 + out_value.length + (has_tag ? 1 : 0), offset++); if (has_tag) { packet.writeUInt8(attr[1], offset++); } // copy in the attribute value out_value.copy(packet, offset); offset += out_value.length; } return offset; }; module.exports = Radius;