refactor: improve ttls tunnel stuff and fix some byte sizes for AVPs
This commit is contained in:
parent
6aa4b9f92e
commit
60ec84ae8e
10
README.md
10
README.md
@ -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.
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
40
src/radius/PacketHandler.ts
Normal file
40
src/radius/PacketHandler.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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}`);
|
||||||
|
@ -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]]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user