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