refactor: improve ttls tunnel stuff and fix some byte sizes for AVPs

This commit is contained in:
simon 2020-02-25 11:54:57 +01:00
parent 6aa4b9f92e
commit 60ec84ae8e
13 changed files with 296 additions and 159 deletions

View File

@ -5,12 +5,16 @@ Basic RADIUS Server for node.js for Google LDAP Service and WPA2 Enteprise WLAN
Protect your WIFI access with a username and password by a credential provider you already use! Protect your WIFI access with a username and password by a credential provider you already use!
Authenticiation tested with Windows, Linux, Android and Apple devices.
## Known Issues / Disclaimer ## Known Issues / Disclaimer
This is a first implementation draft, which is currently only working with a nodejs fork (see https://github.com/nodejs/node/pull/31814). This is a first implementation, which is currently only working with node js nightly (node 13+) (see https://github.com/nodejs/node/pull/31814).
- PAP / CHAP RFC not found to implement this correctly - MD5 Challenge not implenented, but RFC says this is mandatory ;-)
- a lot of bugs - Inner Tunnel does not act differently, even though spec says that EAP-message are not allowed to get fragmented,
this is not a problem right now, as the messages of the inner tunnel are small enough, but it could be a bug in the future.
ways to approach this: refactor that the inner tunnel can set max fragment size, or rebuild eap fragments in ttls after inner tunnel response
CONTRIBUTIONS WELCOME! If you are willing to help, just open a PR or contact me via bug system or simon.tretter@hokify.com. CONTRIBUTIONS WELCOME! If you are willing to help, just open a PR or contact me via bug system or simon.tretter@hokify.com.

View File

@ -39,7 +39,6 @@ export class LDAPAuth implements IAuthentication {
} }
async authenticate(username: string, password: string) { async authenticate(username: string, password: string) {
// console.log('AUTH', this.ldap);
const authResult: boolean = await new Promise((resolve, reject) => { const authResult: boolean = await new Promise((resolve, reject) => {
this.ldap.authenticate(username, password, function(err, user) { this.ldap.authenticate(username, password, function(err, user) {
if (err) { if (err) {

View File

@ -0,0 +1,40 @@
import { IPacket, IPacketHandler, IPacketHandlerResult } from '../types/PacketHandler';
import { IAuthentication } from '../types/Authentication';
import { EAPPacketHandler } from './handler/EAPPacketHandler';
import { EAPTTLS } from './handler/eap/eapMethods/EAP-TTLS';
import { EAPGTC } from './handler/eap/eapMethods/EAP-GTC';
import { EAPMD5 } from './handler/eap/eapMethods/EAP-MD5';
import { UserPasswordPacketHandler } from './handler/UserPasswordPacketHandler';
export class PacketHandler implements IPacketHandler {
packetHandlers: IPacketHandler[] = [];
constructor(authentication: IAuthentication) {
this.packetHandlers.push(
new EAPPacketHandler([
new EAPTTLS(authentication, this),
new EAPGTC(authentication),
new EAPMD5(authentication)
])
);
this.packetHandlers.push(new UserPasswordPacketHandler(authentication));
}
async handlePacket(packet: IPacket, handlingType?: number) {
let response: IPacketHandlerResult;
let i = 0;
if (!this.packetHandlers[i]) {
throw new Error('no packet handlers registered');
}
// process packet handlers until we get a response from one
do {
/* response is of type IPacketHandlerResult */
response = await this.packetHandlers[i].handlePacket(packet, handlingType);
i++;
} while (this.packetHandlers[i] && (!response || !response.code));
return response;
}
}

View File

@ -1,25 +1,14 @@
import * as radius from 'radius'; import * as radius from 'radius';
import { IAuthentication } from '../types/Authentication'; import { IAuthentication } from '../types/Authentication';
import { EAPPacketHandler } from './handler/EAPPacketHandler'; import { IPacketHandlerResult, PacketResponseCode } from '../types/PacketHandler';
import { DefaultPacketHandler } from './handler/DefaultPacketHandler';
import { IPacketHandler, IPacketHandlerResult, PacketResponseCode } from '../types/PacketHandler';
import { EAPTTLS } from './handler/eap/eapMethods/EAP-TTLS'; import { PacketHandler } from './PacketHandler';
import { EAPMD5 } from './handler/eap/eapMethods/EAP-MD5';
import { EAPGTC } from './handler/eap/eapMethods/EAP-GTC';
export class RadiusService { export class RadiusService {
radiusPacketHandlers: IPacketHandler[] = []; private packetHandler: PacketHandler;
constructor(private secret: string, private authentication: IAuthentication) { constructor(private secret: string, authentication: IAuthentication) {
this.radiusPacketHandlers.push( this.packetHandler = new PacketHandler(authentication);
new EAPPacketHandler([
new EAPTTLS(authentication),
new EAPGTC(authentication),
new EAPMD5(authentication)
])
);
this.radiusPacketHandlers.push(new DefaultPacketHandler(authentication));
} }
async handleMessage( async handleMessage(
@ -32,19 +21,7 @@ export class RadiusService {
return undefined; return undefined;
} }
let response: IPacketHandlerResult; const response: IPacketHandlerResult = await this.packetHandler.handlePacket(packet);
let i = 0;
if (!this.radiusPacketHandlers[i]) {
throw new Error('no packet handlers registered');
}
// process packet handlers until we get a response from one
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 // still no response, we are done here
if (!response || !response.code) { if (!response || !response.code) {

View File

@ -1,40 +1,38 @@
// https://tools.ietf.org/html/rfc3748#section-4.1 // https://tools.ietf.org/html/rfc3748#section-4.1
import * as NodeCache from 'node-cache'; import * as NodeCache from 'node-cache';
import { RadiusPacket } from 'radius';
import debug from 'debug'; import debug from 'debug';
import { makeid } from '../../helpers'; import { makeid } from '../../helpers';
import { IPacketHandler, IPacketHandlerResult } from '../../types/PacketHandler'; import { IPacket, IPacketHandler, IPacketHandlerResult } from '../../types/PacketHandler';
import { IEAPMethod } from '../../types/EAPMethod'; import { IEAPMethod } from '../../types/EAPMethod';
import { buildEAPResponse, decodeEAPHeader } from './eap/EAPHelper'; import { buildEAPResponse, decodeEAPHeader } from './eap/EAPHelper';
const log = debug('radius:eap'); const log = debug('radius:eap');
export class EAPPacketHandler implements IPacketHandler { export class EAPPacketHandler implements IPacketHandler {
private identities = new NodeCache({ useClones: false, stdTTL: 60 }); // queue data maximum for 60 seconds
// private eapConnectionStates: { [key: string]: { validMethods: IEAPMethod[] } } = {}; // private eapConnectionStates: { [key: string]: { validMethods: IEAPMethod[] } } = {};
private eapConnectionStates = new NodeCache({ useClones: false, stdTTL: 3600 }); // max for one hour private eapConnectionStates = new NodeCache({ useClones: false, stdTTL: 3600 }); // max for one hour
constructor(private eapMethods: IEAPMethod[]) {} constructor(private eapMethods: IEAPMethod[]) {}
async handlePacket( async handlePacket(packet: IPacket, handlingType?: number): Promise<IPacketHandlerResult> {
attributes: { [key: string]: Buffer | string }, if (!packet.attributes['EAP-Message']) {
orgRadiusPacket: RadiusPacket
): Promise<IPacketHandlerResult> {
if (!attributes['EAP-Message']) {
// not an EAP message // not an EAP message
return {}; return {};
} }
const stateID = (attributes.State && attributes.State.toString()) || makeid(16); const stateID = (packet.attributes.State && packet.attributes.State.toString()) || makeid(16);
if (!this.eapConnectionStates.get(stateID)) { if (!this.eapConnectionStates.get(stateID)) {
this.eapConnectionStates.set(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 validMethods: this.eapMethods.filter(eap => eap.getEAPType() !== handlingType) // on init all registered eap methods are valid, we kick them out in case we get a NAK response
}); });
} }
// EAP MESSAGE // EAP MESSAGE
const msg = attributes['EAP-Message'] as Buffer; const msg = packet.attributes['EAP-Message'] as Buffer;
const { code, type, identifier, data } = decodeEAPHeader(msg); const { code, type, identifier, data } = decodeEAPHeader(msg);
@ -45,7 +43,13 @@ export class EAPPacketHandler implements IPacketHandler {
case 2: // for response case 2: // for response
switch (type) { switch (type) {
case 1: // identifiy case 1: // identifiy
log('>>>>>>>>>>>> REQUEST FROM CLIENT: IDENTIFY', {}); log('>>>>>>>>>>>> REQUEST FROM CLIENT: IDENTIFY', stateID, data.toString());
if (data) {
this.identities.set(stateID, data); // use token til binary 0.);
} else {
log('no msg');
}
// start identify // start identify
if (currentState.validMethods.length > 0) { if (currentState.validMethods.length > 0) {
return currentState.validMethods[0].identify(identifier, stateID, data); return currentState.validMethods[0].identify(identifier, stateID, data);
@ -65,6 +69,7 @@ export class EAPPacketHandler implements IPacketHandler {
console.error('not implemented type', type); console.error('not implemented type', type);
break; break;
case 3: // nak case 3: // nak
// console.log('got NAK', data);
if (data) { if (data) {
// if there is data, each data octect reprsents a eap method the clients supports, // if there is data, each data octect reprsents a eap method the clients supports,
// kick out all unsupported ones // kick out all unsupported ones
@ -73,27 +78,38 @@ export class EAPPacketHandler implements IPacketHandler {
supportedEAPMethods.push(supportedMethod); supportedEAPMethods.push(supportedMethod);
} }
this.eapConnectionStates.set(stateID, { currentState.validMethods = currentState.validMethods.filter(method => {
...currentState,
validMethods: currentState.validMethods.filter(method => {
return supportedEAPMethods.includes(method.getEAPType()); // kick it out? return supportedEAPMethods.includes(method.getEAPType()); // kick it out?
})
}); });
// save
this.eapConnectionStates.set(stateID, currentState);
// new identidy request
// start identify
if (currentState.validMethods.length > 0) {
return currentState.validMethods[0].identify(identifier, stateID, data);
}
} }
// continue with responding a NAK and add rest of supported methods // continue with responding a NAK and add rest of supported methods
// eslint-disable-next-line no-fallthrough // eslint-disable-next-line no-fallthrough
default: { default: {
const eapMethod = currentState.validMethods.find(method => { const eapMethod = this.eapMethods.find(method => {
return type === method.getEAPType(); return type === method.getEAPType();
}); });
if (eapMethod) { if (eapMethod) {
return eapMethod.handleMessage(identifier, stateID, msg, orgRadiusPacket); return eapMethod.handleMessage(
identifier,
stateID,
msg,
packet,
this.identities.get(stateID)
);
} }
// we do not support this auth type, ask for something we support // we do not support this auth type, ask for something we support
const serverSupportedMethods = currentState.validMethods.map( const serverSupportedMethods = currentState.validMethods.map(method =>
method => method.getEAPType method.getEAPType()
); );
console.error('unsupported type', type, `requesting: ${serverSupportedMethods}`); console.error('unsupported type', type, `requesting: ${serverSupportedMethods}`);

View File

@ -1,22 +1,29 @@
import debug from 'debug';
import { IAuthentication } from '../../types/Authentication'; import { IAuthentication } from '../../types/Authentication';
import { import {
IPacket,
IPacketHandler, IPacketHandler,
IPacketHandlerResult, IPacketHandlerResult,
PacketResponseCode PacketResponseCode
} from '../../types/PacketHandler'; } from '../../types/PacketHandler';
export class DefaultPacketHandler implements IPacketHandler { const log = debug('radius:user-pwd');
export class UserPasswordPacketHandler implements IPacketHandler {
constructor(private authentication: IAuthentication) {} constructor(private authentication: IAuthentication) {}
async handlePacket(attributes: { [key: string]: Buffer }): Promise<IPacketHandlerResult> { async handlePacket(packet: IPacket): Promise<IPacketHandlerResult> {
const username = attributes['User-Name']; const username = packet.attributes['User-Name'];
const password = attributes['User-Password']; const password = packet.attributes['User-Password'];
if (!username || !password) { if (!username || !password) {
// params missing, this handler cannot continue... // params missing, this handler cannot continue...
return {}; return {};
} }
log('username', username, username.toString());
log('token', password, password.toString());
const authenticated = await this.authentication.authenticate( const authenticated = await this.authentication.authenticate(
username.toString(), username.toString(),
password.toString() password.toString()
@ -24,7 +31,8 @@ export class DefaultPacketHandler implements IPacketHandler {
if (authenticated) { if (authenticated) {
// success // success
return { return {
code: PacketResponseCode.AccessAccept code: PacketResponseCode.AccessAccept,
attributes: [['User-Name', username]]
}; };
} }

View File

@ -1,7 +1,6 @@
// https://tools.ietf.org/html/rfc5281 TTLS v0 // https://tools.ietf.org/html/rfc5281 TTLS v0
// https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented) // https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented)
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
import * as NodeCache from 'node-cache';
import debug from 'debug'; import debug from 'debug';
import { IPacketHandlerResult, PacketResponseCode } from '../../../../types/PacketHandler'; import { IPacketHandlerResult, PacketResponseCode } from '../../../../types/PacketHandler';
import { IEAPMethod } from '../../../../types/EAPMethod'; import { IEAPMethod } from '../../../../types/EAPMethod';
@ -11,24 +10,19 @@ import { buildEAPResponse, decodeEAPHeader } from '../EAPHelper';
const log = debug('radius:eap:gtc'); const log = debug('radius:eap:gtc');
export class EAPGTC implements IEAPMethod { export class EAPGTC implements IEAPMethod {
private loginData = new NodeCache({ useClones: false, stdTTL: 60 }); // queue data maximum for 60 seconds
getEAPType(): number { getEAPType(): number {
return 6; return 6;
} }
identify(identifier: number, stateID: string, msg?: Buffer): IPacketHandlerResult { extractValue(msg: Buffer) {
if (msg) { let tillBinary0 = msg.findIndex(v => v === 0) || msg.length;
const parsedMsg = msg.slice( if (tillBinary0 < 0) {
0, tillBinary0 = msg.length - 1;
msg.findIndex(v => v === 0) }
); return msg.slice(0, tillBinary0 + 1); // use token til binary 0.
log('identify', parsedMsg, parsedMsg.toString());
this.loginData.set(stateID, parsedMsg); // use token til binary 0.);
} else {
log('no msg');
} }
identify(identifier: number, _stateID: string): IPacketHandlerResult {
return buildEAPResponse(identifier, 6, Buffer.from('Password: ')); return buildEAPResponse(identifier, 6, Buffer.from('Password: '));
} }
@ -36,18 +30,16 @@ export class EAPGTC implements IEAPMethod {
async handleMessage( async handleMessage(
_identifier: number, _identifier: number,
stateID: string, _stateID: string,
msg: Buffer msg: Buffer,
_,
identity?: string
): Promise<IPacketHandlerResult> { ): Promise<IPacketHandlerResult> {
const username = this.loginData.get(stateID) as Buffer | undefined; const username = identity; // this.loginData.get(stateID) as Buffer | undefined;
const { data } = decodeEAPHeader(msg); const { data } = decodeEAPHeader(msg);
let tillBinary0 = data.findIndex(v => v === 0) || data.length; const token = this.extractValue(data);
if (tillBinary0 < 0) {
tillBinary0 = data.length - 1;
}
const token = data.slice(0, tillBinary0 + 1); // use token til binary 0.
if (!username) { if (!username) {
throw new Error('no username'); throw new Error('no username');
@ -59,7 +51,8 @@ export class EAPGTC implements IEAPMethod {
const success = await this.authentication.authenticate(username.toString(), token.toString()); const success = await this.authentication.authenticate(username.toString(), token.toString());
return { return {
code: success ? PacketResponseCode.AccessAccept : PacketResponseCode.AccessReject code: success ? PacketResponseCode.AccessAccept : PacketResponseCode.AccessReject,
attributes: (success && [['User-Name', username]]) || undefined
}; };
} }
} }

View File

@ -8,8 +8,6 @@ import { IPacketHandlerResult } from '../../../../types/PacketHandler';
import { IEAPMethod } from '../../../../types/EAPMethod'; import { IEAPMethod } from '../../../../types/EAPMethod';
import { IAuthentication } from '../../../../types/Authentication'; import { IAuthentication } from '../../../../types/Authentication';
const log = debug('radius:eap:md5');
interface IEAPResponseHandlers { interface IEAPResponseHandlers {
response: (respData?: Buffer, msgType?: number) => void; response: (respData?: Buffer, msgType?: number) => void;
checkAuth: ResponseAuthHandler; checkAuth: ResponseAuthHandler;

View File

@ -3,18 +3,23 @@
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
import * as tls from 'tls'; import * as tls from 'tls';
import * as NodeCache from 'node-cache'; import * as NodeCache from 'node-cache';
import { RadiusPacket } from 'radius'; // @ts-ignore
import { attr_id_to_name, attr_name_to_id } from 'radius';
import debug from 'debug'; import debug from 'debug';
import { encodeTunnelPW, ITLSServer, startTLSServer } from '../../../../tls/crypt'; import { encodeTunnelPW, ITLSServer, startTLSServer } from '../../../../tls/crypt';
import { ResponseAuthHandler } from '../../../../types/Handler'; import { ResponseAuthHandler } from '../../../../types/Handler';
import { PAPChallenge } from './challenges/PAPChallenge'; import {
import { IPacketHandlerResult, PacketResponseCode } from '../../../../types/PacketHandler'; IPacket,
IPacketAttributes,
IPacketHandler,
IPacketHandlerResult,
PacketResponseCode
} from '../../../../types/PacketHandler';
import { MAX_RADIUS_ATTRIBUTE_SIZE, newDeferredPromise } from '../../../../helpers'; import { MAX_RADIUS_ATTRIBUTE_SIZE, newDeferredPromise } from '../../../../helpers';
import { IEAPMethod } from '../../../../types/EAPMethod'; import { IEAPMethod } from '../../../../types/EAPMethod';
import { IAuthentication } from '../../../../types/Authentication'; import { IAuthentication } from '../../../../types/Authentication';
import { secret } from '../../../../../config'; import { secret } from '../../../../../config';
import { EAPPacketHandler } from '../../EAPPacketHandler';
import { EAPGTC } from './EAP-GTC';
const log = debug('radius:eap:ttls'); const log = debug('radius:eap:ttls');
@ -31,17 +36,24 @@ function tlsHasExportKeyingMaterial(
return typeof (tlsSocket as any).exportKeyingMaterial === 'function'; return typeof (tlsSocket as any).exportKeyingMaterial === 'function';
} }
export class EAPTTLS implements IEAPMethod { interface IAVPEntry {
private papChallenge: PAPChallenge = new PAPChallenge(); type: number;
flags: string;
decodedFlags: {
V: boolean;
M: boolean;
};
length: number;
vendorId?: number;
data: Buffer;
}
export class EAPTTLS implements IEAPMethod {
// { [key: string]: Buffer } = {}; // { [key: string]: Buffer } = {};
private queueData = new NodeCache({ useClones: false, stdTTL: 60 }); // queue data maximum for 60 seconds 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 private openTLSSockets = new NodeCache({ useClones: false, stdTTL: 3600 }); // keep sockets for about one hour
// EAP TUNNEL
tunnelEAP = new EAPPacketHandler([new EAPGTC(this.authentication)]); // tunnel with GTC support
getEAPType(): number { getEAPType(): number {
return 21; return 21;
} }
@ -50,7 +62,7 @@ export class EAPTTLS implements IEAPMethod {
return this.buildEAPTTLSResponse(identifier, 21, 0x20, stateID); return this.buildEAPTTLSResponse(identifier, 21, 0x20, stateID);
} }
constructor(private authentication: IAuthentication) {} constructor(private authentication: IAuthentication, private innerTunnel: IPacketHandler) {}
private buildEAPTTLS( private buildEAPTTLS(
identifier: number, identifier: number,
@ -179,7 +191,7 @@ export class EAPTTLS implements IEAPMethod {
Message Length | Data... Message Length | Data...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/ */
const identifier = msg.slice(1, 2).readUInt8(0); // const identifier = msg.slice(1, 2).readUInt8(0);
const flags = msg.slice(5, 6).readUInt8(0); // .toString('hex'); const flags = msg.slice(5, 6).readUInt8(0); // .toString('hex');
/* /*
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
@ -215,7 +227,7 @@ export class EAPTTLS implements IEAPMethod {
log('>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS', { log('>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS', {
flags: `00000000${flags.toString(2)}`.substr(-8), flags: `00000000${flags.toString(2)}`.substr(-8),
decodedFlags, decodedFlags,
identifier, // identifier,
msglength, msglength,
data data
// dataStr: data.toString() // dataStr: data.toString()
@ -232,7 +244,7 @@ export class EAPTTLS implements IEAPMethod {
identifier: number, identifier: number,
success: boolean, success: boolean,
socket: tls.TLSSocket, socket: tls.TLSSocket,
packet: RadiusPacket packet: IPacket
): IPacketHandlerResult { ): IPacketHandlerResult {
const buffer = Buffer.from([ const buffer = Buffer.from([
success ? 3 : 4, // 3.. success, 4... failure success ? 3 : 4, // 3.. success, 4... failure
@ -246,22 +258,26 @@ export class EAPTTLS implements IEAPMethod {
if (packet.attributes && packet.attributes['User-Name']) { if (packet.attributes && packet.attributes['User-Name']) {
// reappend username to response // reappend username to response
attributes.push(['User-Name', packet.attributes['User-Name']]); attributes.push(['User-Name', packet.attributes['User-Name'].toString()]);
} }
if (tlsHasExportKeyingMaterial(socket)) { if (tlsHasExportKeyingMaterial(socket)) {
const keyingMaterial = (socket as any).exportKeyingMaterial(128, 'ttls keying material'); const keyingMaterial = socket.exportKeyingMaterial(128, 'ttls keying material');
if (!packet.authenticator) {
throw new Error('FATAL: no packet authenticator variable set');
}
attributes.push([ attributes.push([
'Vendor-Specific', 'Vendor-Specific',
311, 311,
[[16, encodeTunnelPW(keyingMaterial.slice(64), (packet as any).authenticator, secret)]] [[16, encodeTunnelPW(keyingMaterial.slice(64), packet.authenticator, secret)]]
]); // MS-MPPE-Send-Key ]); // MS-MPPE-Send-Key
attributes.push([ attributes.push([
'Vendor-Specific', 'Vendor-Specific',
311, 311,
[[17, encodeTunnelPW(keyingMaterial.slice(0, 64), (packet as any).authenticator, secret)]] [[17, encodeTunnelPW(keyingMaterial.slice(0, 64), packet.authenticator, secret)]]
]); // MS-MPPE-Recv-Key ]); // MS-MPPE-Recv-Key
} else { } else {
console.error( console.error(
@ -279,7 +295,7 @@ export class EAPTTLS implements IEAPMethod {
identifier: number, identifier: number,
stateID: string, stateID: string,
msg: Buffer, msg: Buffer,
orgRadiusPacket: RadiusPacket packet: IPacket
): Promise<IPacketHandlerResult> { ): Promise<IPacketHandlerResult> {
const { data } = this.decodeTTLSMessage(msg); const { data } = this.decodeTTLSMessage(msg);
@ -313,12 +329,58 @@ export class EAPTTLS implements IEAPMethod {
ret.attributes = {}; ret.attributes = {};
ret.raw_attributes = []; ret.raw_attributes = [];
const { type, data: AVPdata, length: AVPlength } = this.decodeAVP(incomingData); const AVPs = this.decodeAVPs(incomingData);
console.log('AVP data', { AVPdata, AVPlength, AVPdataStr: AVPdata.toString() }); // build attributes for packet handler
const attributes: IPacketAttributes = {};
AVPs.forEach(avp => {
attributes[attr_id_to_name(avp.type)] = avp.data;
});
// const code = data.slice(4, 5).readUInt8(0); attributes.State = `${stateID}-inner`;
// handle incoming package via inner tunnel
const result = await this.innerTunnel.handlePacket(
{
attributes
},
this.getEAPType()
);
log('inner tunnel result', result);
if (
result.code === PacketResponseCode.AccessReject ||
result.code === PacketResponseCode.AccessAccept
) {
sendResponsePromise.resolve(
this.authResponse(
identifier,
result.code === PacketResponseCode.AccessAccept,
connection.tls,
{
...packet,
attributes: {
...packet.attributes,
...this.transformAttributesArrayToMap(result.attributes)
}
}
)
);
return;
}
const eapMessage = result.attributes?.find(attr => attr[0] === 'EAP-Message');
if (!eapMessage) {
throw new Error('no eap message found');
}
connection.events.emit(
'encrypt',
this.buildAVP(attr_name_to_id('EAP-Message'), eapMessage[1] as Buffer)
);
/*
switch (type) { switch (type) {
case 1: // PAP case 1: // PAP
try { try {
@ -381,7 +443,7 @@ export class EAPTTLS implements IEAPMethod {
this.buildAVP(79, this.buildEAPTTLS(identifier, 3, 0, stateID, Buffer.from([1]))) this.buildAVP(79, this.buildEAPTTLS(identifier, 3, 0, stateID, Buffer.from([1])))
); );
} }
} } */
}; };
const responseHandler = (encryptedResponseData: Buffer) => { const responseHandler = (encryptedResponseData: Buffer) => {
@ -407,7 +469,29 @@ export class EAPTTLS implements IEAPMethod {
return responseData; // this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, encryptedResponseData); return responseData; // this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, encryptedResponseData);
} }
private decodeAVP(buffer: Buffer) { private transformAttributesArrayToMap(attributes: [string, Buffer | string][] | undefined) {
const result = {};
attributes?.forEach(([key, value]) => {
result[key] = value;
});
return result;
}
private decodeAVPs(buffer: Buffer): IAVPEntry[] {
const results: {
type: number;
flags: string;
decodedFlags: {
V: boolean;
M: boolean;
};
length: number;
vendorId?: number;
data: Buffer;
}[] = [];
let currentBuffer = buffer;
do {
/** /**
* 4.1. AVP Header * 4.1. AVP Header
@ -426,8 +510,8 @@ export class EAPTTLS implements IEAPMethod {
| Data ... | Data ...
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
*/ */
const type = buffer.slice(0, 4).readUInt32BE(0); const type = currentBuffer.slice(0, 4).readUInt32BE(0);
const flags = buffer.slice(4, 5).readUInt8(0); const flags = currentBuffer.slice(4, 5).readUInt8(0);
const decodedFlags = { const decodedFlags = {
// L // L
V: !!(flags & 0b10000000), V: !!(flags & 0b10000000),
@ -436,26 +520,36 @@ export class EAPTTLS implements IEAPMethod {
}; };
// const length = buffer.slice(5, 8).readUInt16BE(0); // actually a Int24BE // const length = buffer.slice(5, 8).readUInt16BE(0); // actually a Int24BE
const length = buffer.slice(6, 8).readUInt16BE(0); // actually a Int24BE const length = currentBuffer.slice(6, 8).readUInt16BE(0); // actually a Int24BE
let vendorId; let vendorId;
let data; let data;
if (flags & 0b010000000) { if (flags & 0b010000000) {
// V flag set // V flag set
vendorId = buffer.slice(8, 12).readUInt32BE(0); vendorId = currentBuffer.slice(8, 12).readUInt32BE(0);
data = buffer.slice(8, 12); data = currentBuffer.slice(12, length);
} else { } else {
data = buffer.slice(8); data = currentBuffer.slice(8, length);
} }
return { results.push({
type, type,
flags: `00000000${flags.toString(2)}`.substr(-8), flags: `00000000${flags.toString(2)}`.substr(-8),
decodedFlags, decodedFlags,
length, length,
vendorId, vendorId,
data data
}; });
// ensure length is a multiple of 4 octect
let totalAVPSize = length;
while (totalAVPSize % 4 !== 0) {
totalAVPSize += 1;
}
currentBuffer = currentBuffer.slice(totalAVPSize);
} while (currentBuffer.length > 0);
return results;
} }
private buildAVP( private buildAVP(
@ -481,9 +575,9 @@ export class EAPTTLS implements IEAPMethod {
| Data ... | Data ...
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
*/ */
let b = Buffer.alloc(8); let AVP = Buffer.alloc(8);
b.writeInt32BE(code, 0); // EAP-Message AVP.writeInt32BE(code, 0); // EAP-Message
/** /**
* The 'V' (Vendor-Specific) bit indicates whether the optional * The 'V' (Vendor-Specific) bit indicates whether the optional
Vendor-ID field is present. When set to 1, the Vendor-ID field is Vendor-ID field is present. When set to 1, the Vendor-ID field is
@ -506,14 +600,19 @@ export class EAPTTLS implements IEAPMethod {
flagValue += 0b01000000; flagValue += 0b01000000;
} }
console.log('flagValue', flagValue, `00000000${flagValue.toString(2)}`.substr(-8)); // log('flagValue', flagValue, `00000000${flagValue.toString(2)}`.substr(-8));
b.writeInt8(flagValue, 4); // flags (set V..) AVP.writeInt8(flagValue, 4); // flags (set V..)
b = Buffer.concat([b, data]); // , Buffer.from('\0')]); AVP = Buffer.concat([AVP, data]); // , Buffer.from('\0')]);
b.writeInt16BE(b.byteLength, 6); // write size (actually we would need a Int24BE here, but it is good to go with 16bits) AVP.writeInt16BE(AVP.byteLength, 6); // write size (actually we would need a Int24BE here, but it is good to go with 16bits)
return b; // fill up with 0x00 till we have % 4
while (AVP.length % 4 !== 0) {
AVP = Buffer.concat([AVP, Buffer.from([0x00])]);
}
return AVP;
} }
} }

View File

@ -5,7 +5,6 @@ import * as crypto from 'crypto';
import * as DuplexPair from 'native-duplexpair'; import * as DuplexPair from 'native-duplexpair';
import * as constants from 'constants'; import * as constants from 'constants';
import debug from 'debug'; import debug from 'debug';
import { makeid } from '../helpers';
import * as config from '../../config'; import * as config from '../../config';
const log = debug('radius:tls'); const log = debug('radius:tls');
@ -136,11 +135,10 @@ export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: strin
contents of each Salt field in a given Access-Accept packet MUST contents of each Salt field in a given Access-Accept packet MUST
be unique. be unique.
*/ */
const salt = Buffer.concat([ const salt = crypto.randomBytes(2);
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
Buffer.from((Number(makeid(1)) & 0b10000000).toString()), // ensure left most bit is set (1) salt[0] |= 0b10000000; // ensure leftmost bit is set to 1
Buffer.from(makeid(1))
]);
/* /*
String String

View File

@ -1,5 +1,4 @@
import { RadiusPacket } from 'radius'; import { IPacket, IPacketHandlerResult } from './PacketHandler';
import { IPacketHandlerResult } from './PacketHandler';
export interface IEAPMethod { export interface IEAPMethod {
getEAPType(): number; getEAPType(): number;
@ -10,6 +9,7 @@ export interface IEAPMethod {
identifier: number, identifier: number,
stateID: string, stateID: string,
msg: Buffer, msg: Buffer,
orgRadiusPacket?: RadiusPacket packet?: IPacket,
identity?: string
): Promise<IPacketHandlerResult>; ): Promise<IPacketHandlerResult>;
} }

View File

@ -1,5 +1,3 @@
import { RadiusPacket } from 'radius';
export enum PacketResponseCode { export enum PacketResponseCode {
AccessChallenge = 'Access-Challenge', AccessChallenge = 'Access-Challenge',
AccessAccept = 'Access-Accept', AccessAccept = 'Access-Accept',
@ -8,12 +6,19 @@ export enum PacketResponseCode {
export interface IPacketHandlerResult { export interface IPacketHandlerResult {
code?: PacketResponseCode; code?: PacketResponseCode;
attributes?: [string, Buffer][]; attributes?: [string, Buffer | string][];
}
export interface IPacketAttributes {
[key: string]: string | Buffer;
}
export interface IPacket {
attributes: { [key: string]: string | Buffer };
authenticator?: Buffer;
} }
export interface IPacketHandler { export interface IPacketHandler {
handlePacket( /** handlingType is the attreibute ID of the currently processing type (e.g. TTLS, GTC, MD5,..) */
attributes: { [key: string]: Buffer }, handlePacket(packet: IPacket, handlingType?: number): Promise<IPacketHandlerResult>;
orgRadiusPacket: RadiusPacket
): Promise<IPacketHandlerResult>;
} }