refactor: major code cleanups :-)

first working version
This commit is contained in:
simon
2020-02-23 00:29:33 +01:00
parent c285a15bee
commit 42a05b069f
31 changed files with 1463 additions and 825 deletions

View File

@@ -1,186 +1,42 @@
import * as dgram from 'dgram';
import * as radius from 'radius';
// import * as dgram from "dgram";
// import * as fs from 'fs';
import { EAPHandler } from './eap';
import { IDeferredPromise, makeid, MAX_RADIUS_ATTRIBUTE_SIZE, newDeferredPromise } from './helpers';
import { GoogleLDAPAuth } from './auth/google-ldap';
import { AdditionalAuthHandler } from './types/Handler';
import { UDPServer } from './server/UDPServer';
import { RadiusService } from './radius/RadiusService';
const server = dgram.createSocket('udp4');
import * as config from '../config';
const { argv } = require('yargs')
.usage('RADIUS Server\nUsage: $0')
.example('$0 --port 1812 -s radiussecret')
.default({
port: 1812,
s: 'testing123',
baseDN: 'dc=hokify,dc=com',
ldapServer: 'ldap://127.0.0.1:1636'
})
.describe('baseDN', 'LDAP Base DN')
.describe('ldapServer', 'LDAP Server')
.describe('port', 'RADIUS server listener port')
.alias('s', 'secret')
.describe('secret', 'RADIUS secret')
.string(['secret', 'baseDN'])
.demand('secret');
console.log(`Listener Port: ${argv.port}`);
console.log(`RADIUS Secret: ${argv.secret}`);
console.log(`LDAP Base DN: ${argv.baseDN}`);
console.log(`LDAP Server: ${argv.ldapServer}`);
console.log(`Listener Port: ${config.port || 1812}`);
console.log(`RADIUS Secret: ${config.secret}`);
console.log(`Auth Mode: ${config.authentication}`);
// const ldap = new LDAPAuth({url: 'ldap://ldap.google.com', base: 'dc=hokify,dc=com', uid: 'uid', tlsOptions});
const ldap = new GoogleLDAPAuth(argv.ldapServer, argv.baseDN);
const ldap = new GoogleLDAPAuth(
config.authenticationOptions.url,
config.authenticationOptions.base
);
const eapHandler = new EAPHandler();
const timeout: { [key: string]: NodeJS.Timeout } = {};
const waitForNextMsg: { [key: string]: IDeferredPromise } = {};
const server = new UDPServer(config.port);
const radiusService = new RadiusService(config.secret, ldap);
function sendToClient(
msg: string | Uint8Array,
offset: number,
length: number,
port?: number,
address?: string,
callback?: (error: Error | null, bytes: number) => void,
stateForRetry?: string
): void {
let retried = 0;
(async () => {
server.on('message', async (msg, rinfo) => {
const response = await radiusService.handleMessage(msg);
function sendResponse() {
console.log(`sending response... (try: ${retried})`);
server.send(msg, offset, length, port, address, (error: Error | null, bytes: number) => {
// all good
if (callback) callback(error, bytes);
});
if (stateForRetry && retried < 3) {
// timeout[stateForRetry] = setTimeout(sendResponse, 600 * (retried+1));
}
retried++;
}
sendResponse();
}
server.on('message', async function(msg, rinfo) {
const packet = radius.decode({ packet: msg, secret: argv.secret });
if (packet.code !== 'Access-Request') {
console.log('unknown packet type: ', packet.code);
return;
}
// console.log('packet.attributes', packet.attributes);
// console.log('rinfo', rinfo);
async function checkAuth(
username: string,
password: string,
additionalAuthHandler?: AdditionalAuthHandler
) {
console.log(`Access-Request for ${username}`);
let success = false;
try {
await ldap.authenticate(username, password);
success = true;
} catch (err) {
console.error(err);
}
const attributes: any[] = [];
if (additionalAuthHandler) {
await additionalAuthHandler(success, { packet, attributes, secret: argv.secret });
}
const response = radius.encode_response({
packet,
code: success ? 'Access-Accept' : 'Access-Reject',
secret: argv.secret,
attributes
});
console.log(`Sending ${success ? 'accept' : 'reject'} for user ${username}`);
sendToClient(response, 0, response.length, rinfo.port, rinfo.address, function(err, _bytes) {
if (err) {
console.log('Error sending response to ', rinfo);
}
});
}
if (packet.attributes['EAP-Message']) {
const state = (packet.attributes.State && packet.attributes.State.toString()) || makeid(16);
if (timeout[state]) {
clearTimeout(timeout[state]);
}
const handlers = {
response: (EAPMessage: Buffer) => {
const attributes: any = [['State', Buffer.from(state)]];
let sentDataSize = 0;
do {
if (EAPMessage.length > 0) {
attributes.push([
'EAP-Message',
EAPMessage.slice(sentDataSize, sentDataSize + MAX_RADIUS_ATTRIBUTE_SIZE)
]);
sentDataSize += MAX_RADIUS_ATTRIBUTE_SIZE;
if (response) {
server.sendToClient(
response.data,
rinfo.port,
rinfo.address,
(err, _bytes) => {
if (err) {
console.log('Error sending response to ', rinfo);
}
} while (sentDataSize < EAPMessage.length);
const response = radius.encode_response({
packet,
code: 'Access-Challenge',
secret: argv.secret,
attributes
});
waitForNextMsg[state] = newDeferredPromise();
sendToClient(
response,
0,
response.length,
rinfo.port,
rinfo.address,
function(err, _bytes) {
if (err) {
console.log('Error sending response to ', rinfo);
}
},
state
);
return waitForNextMsg[state].promise;
},
checkAuth
};
if (waitForNextMsg[state]) {
const identifier = packet.attributes['EAP-Message'].slice(1, 2).readUInt8(0); // .toString('hex');
waitForNextMsg[state].resolve({ response: handlers.response, identifier });
},
response.expectAcknowledgment
);
}
});
// EAP MESSAGE
eapHandler.handleEAPMessage(packet.attributes['EAP-Message'], state, handlers);
} else {
const username = packet.attributes['User-Name'];
const password = packet.attributes['User-Password'];
checkAuth(username, password);
}
});
server.on('listening', function() {
const address = server.address();
console.log(`radius server listening ${address.address}:${address.port}`);
});
server.bind(argv.port);
// start server
await server.start();
})();

View File

@@ -1,6 +1,6 @@
import * as NodeCache from 'node-cache';
import { createClient, Client } from 'ldapjs';
import { Client, createClient } from 'ldapjs';
import { IAuthentication } from '../types/Authentication';
const usernameFields = ['posixUid', 'mail'];
@@ -58,7 +58,7 @@ export class GoogleLDAPAuth implements IAuthentication {
});
res.on('end', result => {
console.log(`status: ${result?.status}`);
console.log(`ldap status: ${result?.status}`);
// replace with new dns
this.allValidDNsCache = dns;

View File

@@ -1,219 +0,0 @@
// https://tools.ietf.org/html/rfc3748#section-4.1
// https://tools.ietf.org/html/rfc5281 TTLS v0
// https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented)
import { IResponseHandlers, ResponseHandler } from './types/Handler';
import { EAPTTLS } from './eap/eap-ttls';
import { MAX_RADIUS_ATTRIBUTE_SIZE } from './helpers';
export class EAPHandler {
eapTTLS: EAPTTLS;
constructor() {
this.eapTTLS = new EAPTTLS(this.sendEAPResponse);
}
/**
*
* @param data
* @param type 1 = identity, 21 = EAP-TTLS, 2 = notification, 4 = md5-challenge, 3 = NAK
*/
private async sendEAPResponse(
response: ResponseHandler,
identifier: number,
data?: Buffer,
msgType = 21,
msgFlags = 0x00
) {
let i = 0;
const maxSize = (MAX_RADIUS_ATTRIBUTE_SIZE - 5) * 4;
let sentDataSize = 0;
let currentIdentifier = identifier;
let currentResponse = response;
do {
// SLICE
// const fragmentMaxPart =
// data && (i + 1) * maxFragmentSize > data.length ? (i + 1) * maxFragmentSize : undefined;
const dataPart = data && data.length > 0 && data.slice(sentDataSize, sentDataSize + maxSize);
/* it's the first one and we have more, therefore include length */
const includeLength = data && i === 0 && sentDataSize < data.length;
sentDataSize += maxSize;
i += 1;
/*
0 1 2 3 4 5 6 7 8
+-+-+-+-+-+-+-+-+
|L M R R R R R R|
+-+-+-+-+-+-+-+-+
L = Length included
M = More fragments
R = Reserved
The L bit (length included) is set to indicate the presence of the
four-octet TLS Message Length field, and MUST be set for the first
fragment of a fragmented TLS message or set of messages. The M
bit (more fragments) is set on all but the last fragment.
Implementations of this specification MUST set the reserved bits
to zero, and MUST ignore them on reception.
*/
const flags =
msgFlags +
(includeLength ? 0b10000000 : 0) + // set L bit
(data && sentDataSize < data.length /* we have more */ ? 0b01000000 : 0); // set M bit
currentIdentifier++;
let buffer = Buffer.from([
1, // request
currentIdentifier,
0, // length (1/2)
0, // length (2/2)
msgType, // 1 = identity, 21 = EAP-TTLS, 2 = notificaiton, 4 = md5-challenge, 3 = NAK
flags // flags: 000000 (L include lenghts, M .. more to come)
]);
// append length
if (includeLength && data) {
const length = Buffer.alloc(4);
length.writeInt32BE(data.byteLength, 0);
buffer = Buffer.concat([buffer, length]);
}
const resBuffer = dataPart ? Buffer.concat([buffer, dataPart]) : buffer;
resBuffer.writeUInt16BE(resBuffer.byteLength, 2);
console.log('<<<<<<<<<<<< EAP RESPONSE TO CLIENT', {
code: 1,
currentIdentifier,
includeLength,
length: (data && data.byteLength) || 0,
msgType: msgType.toString(10),
flags: `00000000${flags.toString(2)}`.substr(-8),
data
});
// uffer.from([1,identifier, 0, 0, 21, 0]);
// buffer.writeUInt16BE(sslResponse.length, 2); // length
// buffer.writeInt8(21, 4); // eap-ttls
// buffer.writeInt8(0, 5); // flags
console.log('sending message with identifier', currentIdentifier);
({ identifier: currentIdentifier, response: currentResponse } = await currentResponse(
resBuffer
));
console.log('next message got identifier', currentIdentifier);
} while (data && sentDataSize < data.length);
console.log('DONE', sentDataSize, data && data.length);
}
handleEAPMessage(msg: Buffer, state: string, handlers: IResponseHandlers) {
// const b = Buffer.from([2,0x242,0x0,0x18,0x1,0x115,0x105,0x109,0x111,0x110,0x46,0x116,0x114,0x101,0x116,0x116,0x101,0x114]);
// const msg = Buffer.from([2, 162, 0, 18, 1, 115, 105, 109, 111, 110, 46, 116, 114, 101, 116, 116, 101, 114])
/*
1 Request
2 Response
3 Success
4 Failure
*/
const code = msg.slice(0, 1).readUInt8(0);
const identifier = msg.slice(1, 2).readUInt8(0); // .toString('hex');
// const length = msg.slice(2, 4).readInt16BE(0); // .toString('binary');
const type = msg.slice(4, 5).readUInt8(0); // .slice(3,0x5).toString('hex');
/*
console.log("CODE", code);
console.log('ID', identifier);
console.log('length', length);
*/
switch (code) {
case 1: // for request
case 2: // for response
switch (type) {
case 1: // identifiy
/**
* The EAP-TTLS packet format is shown below. The fields are
transmitted left to right.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Code | Identifier | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Flags | Message Length
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Message Length | Data...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: IDENTIFY', {});
this.sendEAPResponse(handlers.response, identifier, undefined, 21, 0x20);
/*
handlers.response(
Buffer.from([
1, // request
identifier + 1,
0, // length (1/2)
6, // length (2/2)
21, // EAP-TTLS
0x20 // flags: 001000 start flag
])
); */
break;
case 21: // EAP TTLS
this.eapTTLS.handleMessage(msg, state, handlers, identifier);
break;
case 3: // nak
// this.sendEAPResponse(handlers.response, identifier, undefined, 3);
break;
case 2: // notification
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: notification', {});
console.info('notification');
break;
case 4: // md5-challenge
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: md5-challenge', {});
console.info('md5-challenge');
break;
case 254: // expanded type
console.error('not implemented type', type);
break;
default:
// we do not support this auth type, ask for TTLS
console.error('unsupported type', type, 'requesting TTLS (21)');
this.sendEAPResponse(handlers.response, identifier, Buffer.from([21]), 3);
break;
}
break;
case 3:
console.log('Client Auth Success');
break;
case 4:
console.log('Client Auth FAILURE');
break;
default:
break;
// silently ignor;
}
}
}

View File

@@ -1,233 +0,0 @@
/* eslint-disable no-bitwise */
import * as events from 'events';
import * as tls from 'tls';
import { encodeTunnelPW, openTLSSockets, startTLSServer } from '../tls/crypt';
import { AdditionalAuthHandler, ResponseAuthHandler } from '../types/Handler';
import { PAPChallenge } from './challenges/pap';
import { IEAPType } from '../types/EAPType';
interface IEAPResponseHandlers {
response: (respData?: Buffer, msgType?: number) => void;
checkAuth: ResponseAuthHandler;
}
export class EAPTTLS implements IEAPType {
papChallenge: PAPChallenge;
constructor(private sendEAPResponse) {
this.papChallenge = new PAPChallenge();
}
decode(msg: Buffer) {
const flags = msg.slice(5, 6).readUInt8(0); // .toString('hex');
// if (flags)
// @todo check if "L" flag is set in flags
const decodedFlags = {
lengthIncluded: flags & 0b010000000,
moreFragments: flags & 0b001000000,
start: flags & 0b000100000,
reserved: flags & 0b000011000,
version: flags & 0b010000111
};
let msglength;
if (decodedFlags.lengthIncluded) {
msglength = msg.slice(6, 10).readInt32BE(0); // .readDoubleLE(0); // .toString('hex');
}
const data = msg.slice(decodedFlags.lengthIncluded ? 10 : 6, msg.length);
return {
decodedFlags,
msglength,
data
};
}
handleMessage(msg: Buffer, state: string, handlers, identifier: number) {
const { decodedFlags, msglength, data } = this.decode(msg);
// check if no data package is there and we have something in the queue, if so.. empty the queue first
if (!data || data.length === 0) {
// @todo: queue processing
console.warn(
`>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS, ACK / NACK (no data, just a confirmation, ID: ${identifier})`
);
return;
}
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS', {
// flags: `00000000${flags.toString(2)}`.substr(-8),
decodedFlags,
identifier,
/*
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| L | M | S | R | R | V |
+---+---+---+---+---+---+---+---+
L = Length included
M = More fragments
S = Start
R = Reserved
V = Version (000 for EAP-TTLSv0)
*/
msglength,
data,
dataStr: data.toString()
});
let currentConnection = openTLSSockets.get(state) as
| { events: events.EventEmitter; tls: tls.TLSSocket; currentHandlers: IEAPResponseHandlers }
| undefined;
if (!currentConnection) {
const connection = startTLSServer();
currentConnection = {
events: connection.events,
tls: connection.tls,
currentHandlers: handlers
};
openTLSSockets.set(state, currentConnection);
// register event listeners
currentConnection.events.on('incoming', (incomingData: Buffer) => {
const type = incomingData.slice(3, 4).readUInt8(0);
// const code = data.slice(4, 5).readUInt8(0);
switch (type) {
case 1: // PAP / CHAP
try {
const { username, password } = this.papChallenge.decode(incomingData);
currentConnection!.currentHandlers.checkAuth(username, password);
} catch (err) {
// pwd not found..
console.error('pwd not found', err);
// NAK
currentConnection!.currentHandlers.response(undefined, 3);
/*
this.sendEAPResponse(
currentConnection!.currentHandlers.response,
identifier,
undefined,
3
); */
currentConnection!.events.emit('end');
throw new Error(`pwd not found`);
}
break;
default:
console.log('data', incomingData);
console.log('data str', incomingData.toString());
// currentConnection!.events.emit('end');
console.log('UNSUPPORTED AUTH TYPE, requesting PAP');
// throw new Error(`unsupported auth type${type}`);
currentConnection!.currentHandlers.response(Buffer.from([1]), 3);
/*
this.sendEAPResponse(
currentConnection!.currentHandlers.response,
identifier,
Buffer.from([1]),
3
); */
}
});
currentConnection.events.on('response', (responseData: Buffer) => {
// console.log('sending encrypted data back to client', responseData);
// send back...
currentConnection!.currentHandlers.response(responseData);
// this.sendEAPResponse(currentConnection!.currentHandlers.response, identifier, responseData);
// this.sendMessage(TYPE.PRELOGIN, data, false);
});
currentConnection.events.on('end', () => {
// cleanup socket
console.log('ENDING SOCKET');
openTLSSockets.del(state);
});
} /* else {
console.log('using existing socket');
} */
// update handlers
currentConnection.currentHandlers = {
response: (respData?: Buffer, msgType?: number) =>
this.sendEAPResponse(handlers.response, identifier, respData, msgType),
checkAuth: (username: string, password: string) => {
const additionalAuthHandler: AdditionalAuthHandler = (success, params) => {
const buffer = Buffer.from([
success ? 3 : 4, // 3.. success, 4... failure
identifier,
0, // length (1/2)
4 // length (2/2)
]);
params.attributes.push(['EAP-Message', buffer]);
if (params.packet.attributes && params.packet.attributes['User-Name']) {
// reappend username to response
params.attributes.push(['User-Name', params.packet.attributes['User-Name']]);
}
/*
if (sess->eap_if->eapKeyDataLen > 64) {
len = 32;
} else {
len = sess->eap_if->eapKeyDataLen / 2;
}
*/
const keyingMaterial = (currentConnection?.tls as any).exportKeyingMaterial(
128,
'ttls keying material'
);
// console.log('keyingMaterial', keyingMaterial);
// eapKeyData + len
params.attributes.push([
'Vendor-Specific',
311,
[
[
16,
encodeTunnelPW(
keyingMaterial.slice(64),
(params.packet as any).authenticator,
// params.packet.attributes['Message-Authenticator'],
params.secret
)
]
]
]); // MS-MPPE-Send-Key
// eapKeyData
params.attributes.push([
'Vendor-Specific',
311,
[
[
17,
encodeTunnelPW(
keyingMaterial.slice(0, 64),
(params.packet as any).authenticator,
// params.packet.attributes['Message-Authenticator'],
params.secret
)
]
]
]); // MS-MPPE-Recv-Key
};
return handlers.checkAuth(username, password, additionalAuthHandler);
}
};
// emit data to tls server
currentConnection.events.emit('send', data);
}
}

View File

@@ -8,6 +8,8 @@ export function makeid(length) {
return result;
}
// by RFC Radius attributes have a max length
// https://tools.ietf.org/html/rfc6929#section-1.2
export const MAX_RADIUS_ATTRIBUTE_SIZE = 253;
export interface IDeferredPromise {

View File

@@ -0,0 +1,95 @@
import * as radius from 'radius';
import { IAuthentication } from '../types/Authentication';
import { EAPPacketHandler } from './handler/EAPPacketHandler';
import { DefaultPacketHandler } from './handler/DefaultPacketHandler';
import { IPacketHandler, IPacketHandlerResult, PacketResponseCode } from '../types/PacketHandler';
export class RadiusService {
radiusPacketHandlers: IPacketHandler[] = [];
constructor(private secret: string, private authentication: IAuthentication) {
this.radiusPacketHandlers.push(new EAPPacketHandler(authentication));
this.radiusPacketHandlers.push(new DefaultPacketHandler(authentication));
}
async handleMessage(
msg: Buffer
): Promise<{ data: Buffer; expectAcknowledgment?: boolean } | undefined> {
const packet = radius.decode({ packet: msg, secret: this.secret });
if (packet.code !== 'Access-Request') {
console.log('unknown packet type: ', packet.code);
return undefined;
}
// console.log('packet.attributes', packet.attributes);
// console.log('rinfo', rinfo);
/*
const checkAuth = async (
username: string,
password: string,
additionalAuthHandler?: AdditionalAuthHandler
) => {
console.log(`Access-Request for ${username}`);
let success = false;
try {
await this.authentication.authenticate(username, password);
success = true;
} catch (err) {
console.error(err);
}
const attributes: any[] = [];
if (additionalAuthHandler) {
await additionalAuthHandler(success, { packet, attributes, secret: this.secret });
}
const response = radius.encode_response({
packet,
code: success ? 'Access-Accept' : 'Access-Reject',
secret: this.secret,
attributes
});
console.log(`Sending ${success ? 'accept' : 'reject'} for user ${username}`);
this.server.sendToClient(response, rinfo.port, rinfo.address, function(err, _bytes) {
if (err) {
console.log('Error sending response to ', rinfo);
}
});
}; */
let response: IPacketHandlerResult;
let i = 0;
if (!this.radiusPacketHandlers[i]) {
throw new Error('no packet handlers registered');
}
// process packet handlers until we get a response
do {
/* response is of type IPacketHandlerResult */
response = await this.radiusPacketHandlers[i].handlePacket(packet.attributes, packet);
i++;
} while (this.radiusPacketHandlers[i] && (!response || !response.code));
// still no response, we are done here
if (!response || !response.code) {
return undefined;
}
// all fine, return radius encoded response
return {
data: radius.encode_response({
packet,
code: response.code,
secret: this.secret,
attributes: response.attributes
}),
// if message is accept or reject, we conside this as final message
// this means we do not expect a reponse from the client again (acknowledgement for package)
expectAcknowledgment: response.code === PacketResponseCode.AccessChallenge
};
}
}

View File

@@ -0,0 +1,36 @@
import { IAuthentication } from '../../types/Authentication';
import {
IPacketHandler,
IPacketHandlerResult,
PacketResponseCode
} from '../../types/PacketHandler';
export class DefaultPacketHandler implements IPacketHandler {
constructor(private authentication: IAuthentication) {}
async handlePacket(attributes: { [key: string]: Buffer }): Promise<IPacketHandlerResult> {
const username = attributes['User-Name'];
const password = attributes['User-Password'];
if (!username || !password) {
// params missing, this handler cannot continue...
return {};
}
const authenticated = await this.authentication.authenticate(
username.toString(),
password.toString()
);
if (authenticated) {
// success
return {
code: PacketResponseCode.AccessAccept
};
}
// Failed
return {
code: PacketResponseCode.AccessReject
};
}
}

View File

@@ -0,0 +1,193 @@
// https://tools.ietf.org/html/rfc3748#section-4.1
import * as NodeCache from 'node-cache';
import { RadiusPacket } from 'radius';
import { EAPTTLS } from './eapMethods/EAPTTLS';
import { makeid } from '../../helpers';
import {
IPacketHandler,
IPacketHandlerResult,
PacketResponseCode
} from '../../types/PacketHandler';
import { IAuthentication } from '../../types/Authentication';
import { IEAPMethod } from '../../types/EAPMethod';
export class EAPPacketHandler implements IPacketHandler {
private eapMethods: IEAPMethod[] = [];
// private eapConnectionStates: { [key: string]: { validMethods: IEAPMethod[] } } = {};
private eapConnectionStates = new NodeCache({ useClones: false, stdTTL: 3600 }); // max for one hour
constructor(authentication: IAuthentication) {
this.eapMethods.push(new EAPTTLS(authentication));
}
/**
*
* @param data
* @param msgType 1 = identity, 21 = EAP-TTLS, 2 = notification, 4 = md5-challenge, 3 = NAK
*/
private async buildEAPResponse(
identifier: number,
msgType: number,
data?: Buffer
): Promise<IPacketHandlerResult> {
/** build a package according to this:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Code | Identifier | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Type-Data ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
*/
const buffer = Buffer.from([
1, // request
identifier,
0, // length (1/2)
0, // length (2/2)
msgType // 1 = identity, 21 = EAP-TTLS, 2 = notificaiton, 4 = md5-challenge, 3 = NAK
]);
const resBuffer = data ? Buffer.concat([buffer, data]) : buffer;
// set EAP length header
resBuffer.writeUInt16BE(resBuffer.byteLength, 2);
return {
code: PacketResponseCode.AccessChallenge,
attributes: [['EAP-Message', buffer]]
};
}
private decodeEAPHeader(msg: Buffer) {
/**
* parse msg according to this:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Code | Identifier | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Type-Data ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
*/
/*
code:
1 Request
2 Response
3 Success
4 Failure
*/
const code = msg.slice(0, 1).readUInt8(0);
/* identifier is a number */
const identifier = msg.slice(1, 2).readUInt8(0);
const length = msg.slice(2, 4).readInt16BE(0);
/* EAP type */
const type = msg.slice(4, 5).readUInt8(0);
const data = msg.slice(5);
return {
code,
identifier,
length,
type,
data
};
}
async handlePacket(
attributes: { [key: string]: Buffer },
orgRadiusPacket: RadiusPacket
): Promise<IPacketHandlerResult> {
if (!attributes['EAP-Message']) {
// not an EAP message
return {};
}
const stateID = (attributes.State && attributes.State.toString()) || makeid(16);
if (!this.eapConnectionStates.get(stateID)) {
this.eapConnectionStates.set(stateID, {
validMethods: this.eapMethods // on init all registered eap methods are valid, we kick them out in case we get a NAK response
});
}
// EAP MESSAGE
const msg = attributes['EAP-Message'];
const { code, type, identifier, data } = this.decodeEAPHeader(msg);
const currentState = this.eapConnectionStates.get(stateID) as { validMethods: IEAPMethod[] };
switch (code) {
case 1: // for request
case 2: // for response
switch (type) {
case 1: // identifiy
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: IDENTIFY', {});
// start identify
if (currentState.validMethods.length > 0) {
return currentState.validMethods[0].identify(identifier, stateID);
}
return this.buildEAPResponse(identifier, 3);
case 2: // notification
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: notification', {});
console.info('notification');
break;
case 4: // md5-challenge
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: md5-challenge', {});
console.info('md5-challenge');
break;
case 254: // expanded type
console.error('not implemented type', type);
break;
case 3: // nak
if (data) {
const supportedEAPMethods: number[] = [];
for (const supportedMethod of data) {
supportedEAPMethods.push(supportedMethod);
}
this.eapConnectionStates.set(stateID, {
...currentState,
validMethods: currentState.validMethods.filter(method => {
return supportedEAPMethods.includes(method.getEAPType()); // kick it out?
})
});
}
// continue with responding a NAK and add rest of supported methods
// eslint-disable-next-line no-fallthrough
default: {
const eapMethod = currentState.validMethods.find(method => {
return type === method.getEAPType();
});
if (eapMethod) {
return eapMethod.handleMessage(identifier, stateID, msg, orgRadiusPacket);
}
// we do not support this auth type, ask for something we support
const serverSupportedMethods = currentState.validMethods.map(
method => method.getEAPType
);
console.error('unsupported type', type, `requesting: ${serverSupportedMethods}`);
return this.buildEAPResponse(identifier, 3, Buffer.from(serverSupportedMethods));
}
}
break;
case 3:
console.log('Client Auth Success');
break;
case 4:
console.log('Client Auth FAILURE');
break;
default:
}
// silently ignore;
return {};
}
}

View File

@@ -0,0 +1,460 @@
/* eslint-disable no-bitwise */
import * as tls from 'tls';
import * as NodeCache from 'node-cache';
import { RadiusPacket } from 'radius';
import { encodeTunnelPW, ITLSServer, startTLSServer } from '../../../tls/crypt';
import { ResponseAuthHandler } from '../../../types/Handler';
import { PAPChallenge } from './challenges/PAPChallenge';
import { IPacketHandlerResult, PacketResponseCode } from '../../../types/PacketHandler';
import { MAX_RADIUS_ATTRIBUTE_SIZE, newDeferredPromise } from '../../../helpers';
import { IEAPMethod } from '../../../types/EAPMethod';
import { IAuthentication } from '../../../types/Authentication';
import { secret } from '../../../../config';
interface IEAPResponseHandlers {
response: (respData?: Buffer, msgType?: number) => void;
checkAuth: ResponseAuthHandler;
}
/* const handlers = {
response: (EAPMessage: Buffer) => {
const attributes: any = [['State', Buffer.from(state)]];
let sentDataSize = 0;
do {
if (EAPMessage.length > 0) {
attributes.push([
'EAP-Message',
EAPMessage.slice(sentDataSize, sentDataSize + MAX_RADIUS_ATTRIBUTE_SIZE)
]);
sentDataSize += MAX_RADIUS_ATTRIBUTE_SIZE;
}
} while (sentDataSize < EAPMessage.length);
const response = radius.encode_response({
packet,
code: 'Access-Challenge',
secret: this.secret,
attributes
});
waitForNextMsg[state] = newDeferredPromise();
server.sendToClient(
response,
rinfo.port,
rinfo.address,
function(err, _bytes) {
if (err) {
console.log('Error sending response to ', rinfo);
}
},
state
);
return waitForNextMsg[state].promise;
},
checkAuth
};
const attributes: any = [['State', Buffer.from(stateID)]];
let sentDataSize = 0;
do {
if (EAPMessage.length > 0) {
attributes.push([
'EAP-Message',
EAPMessage.slice(sentDataSize, sentDataSize + MAX_RADIUS_ATTRIBUTE_SIZE)
]);
sentDataSize += MAX_RADIUS_ATTRIBUTE_SIZE;
}
} while (sentDataSize < EAPMessage.length);
const response = radius.encode_response({
packet,
code: 'Access-Challenge',
secret: this.secret,
attributes
});
waitForNextMsg[stateID] = newDeferredPromise();
server.sendToClient(
response,
rinfo.port,
rinfo.address,
function(err, _bytes) {
if (err) {
console.log('Error sending response to ', rinfo);
}
},
stateID
);
return waitForNextMsg[stateID].promise;
*/
/* if (waitForNextMsg[state]) {
const identifier = attributes['EAP-Message'].slice(1, 2).readUInt8(0); // .toString('hex');
waitForNextMsg[state].resolve({ response: handlers.response, identifier });
} */
function tlsHasExportKeyingMaterial(
tlsSocket: any
): tlsSocket is {
exportKeyingMaterial: (length: number, label: string, context?: Buffer) => Buffer;
} {
return typeof (tlsSocket as any).exportKeyingMaterial === 'function';
}
export class EAPTTLS implements IEAPMethod {
private papChallenge: PAPChallenge = new PAPChallenge();
// { [key: string]: Buffer } = {};
private queueData = new NodeCache({ useClones: false, stdTTL: 60 }); // queue data maximum for 60 seconds
private openTLSSockets = new NodeCache({ useClones: false, stdTTL: 3600 }); // keep sockets for about one hour
getEAPType(): number {
return 21;
}
identify(identifier: number, stateID: string): IPacketHandlerResult {
return this.buildEAPTTLSResponse(identifier, 21, 0x20, stateID);
}
constructor(private authentication: IAuthentication) {}
private buildEAPTTLSResponse(
identifier: number,
msgType = 21,
msgFlags = 0x00,
stateID: string,
data?: Buffer,
newResponse = true
): IPacketHandlerResult {
const maxSize = (MAX_RADIUS_ATTRIBUTE_SIZE - 5) * 4;
console.log('maxSize', maxSize);
/* it's the first one and we have more, therefore include length */
const includeLength = data && newResponse && data.length > maxSize;
// extract data party
const dataToSend = data && data.length > 0 && data.slice(0, maxSize);
const dataToQueue = data && data.length > maxSize && data.slice(maxSize);
/*
0 1 2 3 4 5 6 7 8
+-+-+-+-+-+-+-+-+
|L M R R R R R R|
+-+-+-+-+-+-+-+-+
L = Length included
M = More fragments
R = Reserved
The L bit (length included) is set to indicate the presence of the
four-octet TLS Message Length field, and MUST be set for the first
fragment of a fragmented TLS message or set of messages. The M
bit (more fragments) is set on all but the last fragment.
Implementations of this specification MUST set the reserved bits
to zero, and MUST ignore them on reception.
*/
const flags =
msgFlags +
(includeLength ? 0b10000000 : 0) + // set L bit
(dataToQueue && dataToQueue.length > 0 ? 0b01000000 : 0); // we have more data to come, set M bit
let buffer = Buffer.from([
1, // request
identifier + 1, // increase id by 1
0, // length (1/2)
0, // length (2/2)
msgType, // 1 = identity, 21 = EAP-TTLS, 2 = notificaiton, 4 = md5-challenge, 3 = NAK
flags // flags: 000000 (L include lenghts, M .. more to come)
]);
// append length
if (includeLength && data) {
const length = Buffer.alloc(4);
length.writeInt32BE(data.byteLength, 0);
buffer = Buffer.concat([buffer, length]);
}
// build final buffer with data
const resBuffer = dataToSend ? Buffer.concat([buffer, dataToSend]) : buffer;
// set EAP length header
resBuffer.writeUInt16BE(resBuffer.byteLength, 2);
console.log('<<<<<<<<<<<< EAP RESPONSE TO CLIENT', {
code: 1,
identifier: identifier + 1,
includeLength,
dataLength: (data && data.byteLength) || 0,
msgType: msgType.toString(10),
flags: `00000000${flags.toString(2)}`.substr(-8),
data
});
if (dataToQueue) {
// we couldn't send all at once, queue the rest and send later
this.queueData.set(stateID, dataToQueue);
} else {
this.queueData.del(stateID);
}
const attributes: any = [['State', Buffer.from(stateID)]];
let sentDataSize = 0;
do {
if (resBuffer.length > 0) {
attributes.push([
'EAP-Message',
resBuffer.slice(sentDataSize, sentDataSize + MAX_RADIUS_ATTRIBUTE_SIZE)
]);
sentDataSize += MAX_RADIUS_ATTRIBUTE_SIZE;
}
} while (sentDataSize < resBuffer.length);
return {
code: PacketResponseCode.AccessChallenge,
attributes
};
}
decodeTTLSMessage(msg: Buffer) {
/**
* The EAP-TTLS packet format is shown below. The fields are
transmitted left to right.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Code | Identifier | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Flags | Message Length
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Message Length | Data...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
const flags = msg.slice(5, 6).readUInt8(0); // .toString('hex');
/*
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| L | M | S | R | R | V |
+---+---+---+---+---+---+---+---+
L = Length included
M = More fragments
S = Start
R = Reserved
V = Version (000 for EAP-TTLSv0)
*/
const decodedFlags = {
// L
lengthIncluded: flags & 0b010000000,
// M
moreFragments: flags & 0b001000000,
// S
start: flags & 0b000100000,
// R
// reserved: flags & 0b000011000,
// V
version: flags & 0b010000111
};
let msglength;
if (decodedFlags.lengthIncluded) {
msglength = msg.slice(6, 10).readInt32BE(0); // .readDoubleLE(0); // .toString('hex');
}
const data = msg.slice(decodedFlags.lengthIncluded ? 10 : 6, msg.length);
return {
decodedFlags,
msglength,
data
};
}
authResponse(
identifier: number,
success: boolean,
socket: tls.TLSSocket,
packet: RadiusPacket
): IPacketHandlerResult {
const buffer = Buffer.from([
success ? 3 : 4, // 3.. success, 4... failure
identifier + 1,
0, // length (1/2)
4 // length (2/2)
]);
const attributes: any[] = [];
attributes.push(['EAP-Message', buffer]);
if (packet.attributes && packet.attributes['User-Name']) {
// reappend username to response
attributes.push(['User-Name', packet.attributes['User-Name']]);
}
/*
if (sess->eap_if->eapKeyDataLen > 64) {
len = 32;
} else {
len = sess->eap_if->eapKeyDataLen / 2;
}
*/
if (tlsHasExportKeyingMaterial(socket)) {
const keyingMaterial = (socket as any).exportKeyingMaterial(128, 'ttls keying material');
// console.log('keyingMaterial', keyingMaterial);
// eapKeyData + len
attributes.push([
'Vendor-Specific',
311,
[
[
16,
encodeTunnelPW(
keyingMaterial.slice(64),
(packet as any).authenticator,
// params.packet.attributes['Message-Authenticator'],
secret
)
]
]
]); // MS-MPPE-Send-Key
// eapKeyData
attributes.push([
'Vendor-Specific',
311,
[
[
17,
encodeTunnelPW(
keyingMaterial.slice(0, 64),
(packet as any).authenticator,
// params.packet.attributes['Message-Authenticator'],
secret
)
]
]
]); // MS-MPPE-Recv-Key
} else {
console.error(
'FATAL: no exportKeyingMaterial method available!!!, you need latest NODE JS, see https://github.com/nodejs/node/pull/31814'
);
}
return {
code: success ? PacketResponseCode.AccessAccept : PacketResponseCode.AccessReject,
attributes
};
}
async handleMessage(
identifier: number,
stateID: string,
msg: Buffer,
orgRadiusPacket: RadiusPacket
): Promise<IPacketHandlerResult> {
const { decodedFlags, msglength, data } = this.decodeTTLSMessage(msg);
// check if no data package is there and we have something in the queue, if so.. empty the queue first
if (!data || data.length === 0) {
console.warn(
`>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS, ACK / NACK (no data, just a confirmation, ID: ${identifier})`
);
const queuedData = this.queueData.get(stateID);
if (queuedData instanceof Buffer && queuedData.length > 0) {
return this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, queuedData, false);
}
return {};
}
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS', {
// flags: `00000000${flags.toString(2)}`.substr(-8),
decodedFlags,
identifier,
msglength
// data,
// dataStr: data.toString()
});
let connection = this.openTLSSockets.get(stateID) as ITLSServer;
if (!connection) {
connection = startTLSServer();
this.openTLSSockets.set(stateID, connection);
connection.events.on('end', () => {
// cleanup socket
console.log('ENDING SOCKET');
this.openTLSSockets.del(stateID);
});
}
const sendResponsePromise = newDeferredPromise();
const incomingMessageHandler = async (incomingData: Buffer) => {
const type = incomingData.slice(3, 4).readUInt8(0);
// const code = data.slice(4, 5).readUInt8(0);
switch (type) {
case 1: // PAP / CHAP
try {
const { username, password } = this.papChallenge.decode(incomingData);
const authResult = await this.authentication.authenticate(username, password);
sendResponsePromise.resolve(
this.authResponse(identifier, authResult, connection.tls, orgRadiusPacket)
);
} catch (err) {
// pwd not found..
console.error('pwd not found', err);
connection.events.emit('end');
// NAK
sendResponsePromise.resolve(this.buildEAPTTLSResponse(identifier, 3, 0, stateID));
}
break;
default:
console.log('data', incomingData);
console.log('data str', incomingData.toString());
// currentConnection!.events.emit('end');
console.log('UNSUPPORTED AUTH TYPE, requesting PAP');
// throw new Error(`unsupported auth type${type}`);
sendResponsePromise.resolve(
this.buildEAPTTLSResponse(identifier, 3, 0, stateID, Buffer.from([1]))
);
}
};
const responseHandler = (encryptedResponseData: Buffer) => {
// send back...
sendResponsePromise.resolve(
this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, encryptedResponseData)
);
};
// register event listeners
connection.events.on('incoming', incomingMessageHandler);
connection.events.on('response', responseHandler);
// emit data to tls server
connection.events.emit('send', data);
const responseData = await sendResponsePromise.promise;
// cleanup
connection.events.off('incoming', incomingMessageHandler);
connection.events.off('response', responseHandler);
// send response
return responseData; // this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, encryptedResponseData);
}
}

View File

@@ -1,4 +1,4 @@
import { IEAPChallenge } from '../../types/EAPChallenge';
import { IEAPChallenge } from '../../../../types/EAPChallenge';
export class PAPChallenge implements IEAPChallenge {
// i couldn't find any documentation about it, therefore best guess how this is processed...

80
src/server/UDPServer.ts Normal file
View File

@@ -0,0 +1,80 @@
import * as dgram from 'dgram';
import { SocketType } from 'dgram';
import * as events from 'events';
import { EventEmitter } from 'events';
import { newDeferredPromise } from '../helpers';
import { IServer } from '../types/Server';
export class UDPServer extends events.EventEmitter implements IServer {
static MAX_RETRIES = 3;
private timeout: { [key: string]: NodeJS.Timeout } = {};
private server: dgram.Socket;
constructor(private port: number, type: SocketType = 'udp4') {
super();
this.server = dgram.createSocket(type);
}
sendToClient(
msg: string | Uint8Array,
port?: number,
address?: string,
callback?: (error: Error | null, bytes: number) => void,
expectAcknowledgment = true
): void {
let retried = 0;
const sendResponse = (): void => {
if (retried > 0) {
console.warn(
`no confirmation of last message from ${address}:${port}, re-sending response... (bytes: ${msg.length}, try: ${retried}/${UDPServer.MAX_RETRIES})`
);
}
// send message to client
this.server.send(msg, 0, msg.length, port, address, callback);
// retry up to MAX_RETRIES to send this message,
// we automatically retry if there is no confirmation (=any incoming message from client)
// if expectAcknowledgment (e.g. Access-Accept or Access-Reject) is set, we do not retry
const identifierForRetry = `${address}:${port}`;
if (expectAcknowledgment && retried < UDPServer.MAX_RETRIES) {
this.timeout[identifierForRetry] = setTimeout(sendResponse, 600 * (retried + 1));
}
retried += 1;
};
sendResponse();
}
async start(): Promise<EventEmitter> {
const startServer = newDeferredPromise();
this.server.on('listening', () => {
const address = this.server.address();
console.log(`radius server listening ${address.address}:${address.port}`);
this.setupListeners();
startServer.resolve();
});
this.server.on('message', (_msg, rinfo) => {
console.log('incoming message 2');
// message retrieved, reset timeout handler
const identifierForRetry = `${rinfo.address}:${rinfo.port}`;
if (this.timeout[identifierForRetry]) {
clearTimeout(this.timeout[identifierForRetry]);
}
});
this.server.bind(this.port);
return startServer.promise;
}
private setupListeners() {
this.server.on('message', (message, rinfo) => this.emit('message', message, rinfo));
}
}

View File

@@ -1,8 +1,6 @@
import * as NodeCache from 'node-cache';
import * as events from 'events';
import * as tls from 'tls';
import { createSecureContext } from 'tls';
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as DuplexPair from 'native-duplexpair';
import * as constants from 'constants';
@@ -21,16 +19,20 @@ const tlsOptions: tls.SecureContextOptions = {
};
console.log('tlsOptions', tlsOptions);
const secureContext = createSecureContext(tlsOptions);
export const openTLSSockets = new NodeCache({ useClones: false, stdTTL: 3600 }); // keep sockets for about one hour
export function startTLSServer(): { events: events.EventEmitter; tls: tls.TLSSocket } {
export interface ITLSServer {
events: events.EventEmitter;
tls: tls.TLSSocket;
}
export function startTLSServer(): ITLSServer {
const duplexpair = new DuplexPair();
const emitter = new events.EventEmitter();
const cleartext = new tls.TLSSocket(duplexpair.socket1, {
secureContext,
isServer: true,
enableTrace: true,
// enableTrace: true,
rejectUnauthorized: false,
// handshakeTimeout: 10,
requestCert: false

View File

@@ -1,3 +1,3 @@
export interface IAuthentication {
authenticate(username: string, password: string): Promise<string>;
authenticate(username: string, password: string): Promise<boolean>;
}

15
src/types/EAPMethod.ts Normal file
View File

@@ -0,0 +1,15 @@
import { RadiusPacket } from 'radius';
import { IPacketHandlerResult } from './PacketHandler';
export interface IEAPMethod {
getEAPType(): number;
identify(identifier: number, stateID: string): IPacketHandlerResult;
handleMessage(
identifier: number,
stateID: string,
msg: Buffer,
orgRadiusPacket?: RadiusPacket
): Promise<IPacketHandlerResult>;
}

View File

@@ -1,3 +0,0 @@
export interface IEAPType {
handleMessage(msg: Buffer, state: string, handlers, identifier: number);
}

View File

@@ -0,0 +1,19 @@
import { RadiusPacket } from 'radius';
export enum PacketResponseCode {
AccessChallenge = 'Access-Challenge',
AccessAccept = 'Access-Accept',
AccessReject = 'Access-Reject'
}
export interface IPacketHandlerResult {
code?: PacketResponseCode;
attributes?: [string, Buffer][];
}
export interface IPacketHandler {
handlePacket(
attributes: { [key: string]: Buffer },
orgRadiusPacket: RadiusPacket
): Promise<IPacketHandlerResult>;
}

32
src/types/Server.ts Normal file
View File

@@ -0,0 +1,32 @@
import { RemoteInfo } from 'dgram';
/**
* @fires IServer#message
*/
export interface IServer {
/**
*
* @param msg
* @param port
* @param address
* @param callback
* @param expectAcknowledgment: if set to false, message is not retried to send again if there is no confirmation
*/
sendToClient(
msg: string | Uint8Array,
port?: number,
address?: string,
callback?: (error: Error | null, bytes: number) => void,
expectAcknowledgment?: boolean
): void;
/**
* Message event.
*
* @event IServer#message
* @type {object}
* @property {message} data - the data of the incoming message
* @property {rinfo} optionally remote information
*/
on(event: 'message', listener: (msg: Buffer, rinfo?: RemoteInfo) => void): this;
}