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, return supportedEAPMethods.includes(method.getEAPType()); // kick it out?
validMethods: currentState.validMethods.filter(method => {
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)
);
log('identify', parsedMsg, parsedMsg.toString());
this.loginData.set(stateID, parsedMsg); // use token til binary 0.);
} else {
log('no msg');
} }
return msg.slice(0, tillBinary0 + 1); // use token til binary 0.
}
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,9 +469,31 @@ 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 = {};
* 4.1. AVP Header 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
The fields in the AVP header MUST be sent in network byte order. The The fields in the AVP header MUST be sent in network byte order. The
format of the header is: format of the header is:
@ -425,37 +509,47 @@ 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),
// M // M
M: !!(flags & 0b01000000) M: !!(flags & 0b01000000)
}; };
// 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
Buffer.from((Number(makeid(1)) & 0b10000000).toString()), // ensure left most bit is set (1) // eslint-disable-next-line no-bitwise
Buffer.from(makeid(1)) salt[0] |= 0b10000000; // ensure leftmost bit is set to 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>;
} }