From 7e28c60d81abe4c2c5269babbf6ef5951d65d682 Mon Sep 17 00:00:00 2001 From: simon Date: Sun, 16 Feb 2020 19:17:03 +0100 Subject: [PATCH] fix: add MS-MPPE-Send-Key and MS-MPPE-Recv-Key --- src/app.ts | 78 ++++++----------------------- src/eap/eap-ttls.ts | 115 +++++++++++++++++++++++++++++++++++-------- src/tls/crypt.ts | 77 +++++++++++------------------ src/types/Handler.ts | 13 ++++- 4 files changed, 152 insertions(+), 131 deletions(-) diff --git a/src/app.ts b/src/app.ts index 19683e4..0c1d8b9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,17 +1,17 @@ import * as dgram from 'dgram'; import * as radius from 'radius'; // import * as dgram from "dgram"; -import * as fs from 'fs'; +// import * as fs from 'fs'; import { EAPHandler } from './eap'; -import { encodeTunnelPW } from './tls/crypt'; import { makeid } from './helpers'; import { LDAPAuth } from './ldap'; +import { AdditionalAuthHandler } from './types/Handler'; const server = dgram.createSocket('udp4'); // not used right now, using stunnel to connect to ldap -const tlsOptions = { +/* const tlsOptions = { key: fs.readFileSync('ldap.gsuite.hokify.com.40567.key'), cert: fs.readFileSync('ldap.gsuite.hokify.com.40567.crt'), @@ -20,7 +20,7 @@ const tlsOptions = { // This is necessary only if the client uses the self-signed certificate. ca: [fs.readFileSync('ldap.gsuite.hokify.com.40567.key')] -}; +}; */ const { argv } = require('yargs') .usage('Simple Google LDAP <> RADIUS Server\nUsage: $0') @@ -62,81 +62,33 @@ server.on('message', async function(msg, rinfo) { // console.log('rinfo', rinfo); - async function checkAuth(username: string, password: string, EAPMessageIdentifier?: number) { + async function checkAuth( + username: string, + password: string, + additionalAuthHandler?: AdditionalAuthHandler + ) { console.log(`Access-Request for ${username}`); - let code: 'Access-Accept' | 'Access-Reject'; - + let success = false; try { await ldap.authenticate(username, password); - code = 'Access-Accept'; + success = true; } catch (err) { console.error(err); - code = 'Access-Reject'; } const attributes: any[] = []; - if (EAPMessageIdentifier) { - const buffer = Buffer.from([ - code === 'Access-Accept' ? 3 : 4, // 3.. success, 4... failure - EAPMessageIdentifier, - 0, // length (1/2) - 4 // length (2/2) - ]); - - 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 (additionalAuthHandler) { + await additionalAuthHandler(success, { packet, attributes, secret: argv.secret }); } - /* - if (sess->eap_if->eapKeyDataLen > 64) { - len = 32; - } else { - len = sess->eap_if->eapKeyDataLen / 2; - } - */ - // eapKeyData + len - attributes.push([ - 'Vendor-Specific', - 311, - [ - [ - 16, - encodeTunnelPW( - (packet as any).authenticator, - packet.attributes['Message-Authenticator'], - argv.secret - ) - ] - ] - ]); // MS-MPPE-Send-Key - - // eapKeyData - attributes.push([ - 'Vendor-Specific', - 311, - [ - [ - 17, - encodeTunnelPW( - (packet as any).authenticator, - packet.attributes['Message-Authenticator'], - argv.secret - ) - ] - ] - ]); // MS-MPPE-Recv-Key - const response = radius.encode_response({ packet, - code, + code: success ? 'Access-Accept' : 'Access-Reject', secret: argv.secret, attributes }); - console.log(`Sending ${code} for user ${username}`); + console.log(`Sending ${success ? 'accept' : 'reject'} for user ${username}`); server.send(response, 0, response.length, rinfo.port, rinfo.address, function(err, _bytes) { if (err) { diff --git a/src/eap/eap-ttls.ts b/src/eap/eap-ttls.ts index bc2d5e3..61739ed 100644 --- a/src/eap/eap-ttls.ts +++ b/src/eap/eap-ttls.ts @@ -1,6 +1,7 @@ import * as events from 'events'; -import { openTLSSockets, startTLSServer } from '../tls/crypt'; -import { IResponseHandlers } from '../types/Handler'; +import * as tls from 'tls'; +import { encodeTunnelPW, openTLSSockets, startTLSServer } from '../tls/crypt'; +import { AdditionalAuthHandler, IResponseHandlers } from '../types/Handler'; import { PAPChallenge } from './challenges/pap'; import { IEAPType } from '../types/EAPType'; @@ -44,16 +45,20 @@ export class EAPTTLS implements IEAPType { dataStr: data.toString() }); - let sslLayer = openTLSSockets.get(state) as - | { socket: events.EventEmitter; currentHandlers: IResponseHandlers } + let currentConnection = openTLSSockets.get(state) as + | { events: events.EventEmitter; tls: tls.TLSSocket; currentHandlers: IResponseHandlers } | undefined; - if (!sslLayer) { - const newSocket = startTLSServer(); - sslLayer = { socket: newSocket, currentHandlers: handlers }; - openTLSSockets.set(state, sslLayer); + if (!currentConnection) { + const connection = startTLSServer(); + currentConnection = { + events: connection.events, + tls: connection.tls, + currentHandlers: handlers + }; + openTLSSockets.set(state, currentConnection); // register event listeners - newSocket.on('incoming', (incomingData: Buffer) => { + currentConnection.events.on('incoming', (incomingData: Buffer) => { const type = incomingData.slice(3, 4).readUInt8(0); // const code = data.slice(4, 5).readUInt8(0); @@ -61,13 +66,18 @@ export class EAPTTLS implements IEAPType { case 1: // PAP / CHAP try { const { username, password } = this.papChallenge.decode(incomingData); - sslLayer!.currentHandlers.checkAuth(username, password, identifier); + currentConnection!.currentHandlers.checkAuth(username, password); } catch (err) { // pwd not found.. console.error('pwd not found', err); // NAK - this.sendEAPResponse(sslLayer!.currentHandlers.response, identifier, undefined, 3); - newSocket.emit('end'); + this.sendEAPResponse( + currentConnection!.currentHandlers.response, + identifier, + undefined, + 3 + ); + currentConnection!.events.emit('end'); throw new Error(`pwd not found`); } break; @@ -75,20 +85,20 @@ export class EAPTTLS implements IEAPType { console.log('data', incomingData); console.log('data str', incomingData.toString()); - newSocket.emit('end'); + currentConnection!.events.emit('end'); throw new Error(`unsupported auth type${type}`); } }); - newSocket.on('response', (responseData: Buffer) => { + currentConnection.events.on('response', (responseData: Buffer) => { console.log('sending encrypted data back to client', responseData); // send back... - this.sendEAPResponse(sslLayer!.currentHandlers.response, identifier, responseData); + this.sendEAPResponse(currentConnection!.currentHandlers.response, identifier, responseData); // this.sendMessage(TYPE.PRELOGIN, data, false); }); - newSocket.on('end', () => { + currentConnection.events.on('end', () => { // cleanup socket console.log('ENDING SOCKET'); openTLSSockets.del(state); @@ -98,13 +108,78 @@ export class EAPTTLS implements IEAPType { } // update handlers - sslLayer.currentHandlers = { + currentConnection.currentHandlers = { ...handlers, - checkAuth: (username: string, password: string) => - handlers.checkAuth(username, password, identifier) + checkAuth: (username: string, password: string) => { + const additionalAuthHandler: AdditionalAuthHandler = (success, params) => { + const buffer = Buffer.from([ + success ? 3 : 4, // 3.. success, 4... failure + identifier, + 0, // length (1/2) + 4 // length (2/2) + ]); + + params.attributes.push(['EAP-Message', buffer]); + + if (params.packet.attributes && params.packet.attributes['User-Name']) { + // reappend username to response + params.attributes.push(['User-Name', params.packet.attributes['User-Name']]); + } + + /* + if (sess->eap_if->eapKeyDataLen > 64) { + len = 32; + } else { + len = sess->eap_if->eapKeyDataLen / 2; + } + */ + const keyingMaterial = (currentConnection?.tls as any).exportKeyingMaterial( + 128, + 'ttls keying material' + ); + + console.log('keyingMaterial', keyingMaterial); + + // eapKeyData + len + params.attributes.push([ + 'Vendor-Specific', + 311, + [ + [ + 16, + encodeTunnelPW( + keyingMaterial.slice(64), + (params.packet as any).authenticator, + // params.packet.attributes['Message-Authenticator'], + params.secret + ) + ] + ] + ]); // MS-MPPE-Send-Key + + // eapKeyData + params.attributes.push([ + 'Vendor-Specific', + 311, + [ + [ + 17, + encodeTunnelPW( + keyingMaterial.slice(0, 64), + (params.packet as any).authenticator, + // params.packet.attributes['Message-Authenticator'], + params.secret + ) + ] + ] + ]); // MS-MPPE-Recv-Key + }; + + return handlers.checkAuth(username, password, additionalAuthHandler); + } }; // emit data to tls server - sslLayer.socket.emit('send', data); + currentConnection.events.emit('send', data); } } diff --git a/src/tls/crypt.ts b/src/tls/crypt.ts index 9436bb6..cf88280 100644 --- a/src/tls/crypt.ts +++ b/src/tls/crypt.ts @@ -16,7 +16,7 @@ const tlsOptions = { const secureContext = createSecureContext(tlsOptions); export const openTLSSockets = new NodeCache({ useClones: false, stdTTL: 3600 }); // keep sockets for about one hour -export function startTLSServer(): events.EventEmitter { +export function startTLSServer(): { events: events.EventEmitter; tls: tls.TLSSocket } { const duplexpair = new DuplexPair(); const emitter = new events.EventEmitter(); @@ -58,7 +58,7 @@ export function startTLSServer(): events.EventEmitter { emitter.emit('incoming', data); }); - cleartext.once('close', (data: Buffer) => { + cleartext.once('close', (_data: Buffer) => { console.log('cleartext close'); emitter.emit('end'); }); @@ -82,7 +82,10 @@ export function startTLSServer(): events.EventEmitter { emitter.emit('end'); }); - return emitter; + return { + events: emitter, + tls: cleartext + }; } function md5Hex(buffer: Buffer): Buffer { @@ -91,18 +94,15 @@ function md5Hex(buffer: Buffer): Buffer { return hasher.digest(); // new Buffer(hasher.digest("binary"), "binary"); } -export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: string): Buffer { - // see freeradius TTLS implementation how to obtain "key"...... - - // key should be: - // https://www.openssl.org/docs/man1.0.2/man3/SSL_export_keying_material.html - // https://github.com/nodejs/ffi/blob/master/deps/openssl/openssl/doc/man3/SSL_export_keying_material.pod +// alloc_size - // but not available in NODE JS +// 0, +// EAP_TLS_KEY_LEN 64 +// EAP_EMSK_LEN 64 +// const buffer = tlsSocket.exportKeyingMaterial(128, 'ttls keying material'); - console.log('KEY', key); - console.log('authenticator', authenticator); - console.log('secret', secret); +export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: string): Buffer { + // see freeradius TTLS implementation how to obtain "key"...... // https://tools.ietf.org/html/rfc2548 /** @@ -120,9 +120,6 @@ export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: strin Buffer.from(makeid(1)) ]); - console.log('salt', salt); - // ensure left most bit is set to 1 - /* String The plaintext String field consists of three logical sub-fields: @@ -143,14 +140,13 @@ export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: strin used for padding. Call this plaintext P. */ - console.log('key', key.length, key); let P = Buffer.concat([new Uint8Array([key.length]), key]); // + key + padding; // fill up with 0x00 till we have % 16 while (P.length % 16 !== 0) { P = Buffer.concat([P, Buffer.from([0x00])]); } - // console.log('PLAINTEXT', P.length, P); + /* Call the shared secret S, the pseudo-random 128-bit Request Authenticator (from the corresponding Access-Request packet) R, @@ -160,6 +156,21 @@ export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: strin Intermediate values b(1), b(2)...c(i) are required. Encryption is performed in the following manner ('+' indicates concatenation): + + Zorn Informational [Page 21] + + RFC 2548 Microsoft Vendor-specific RADIUS Attributes March 1999 + + + b(1) = MD5(S + R + A) c(1) = p(1) xor b(1) C = c(1) + b(2) = MD5(S + c(1)) c(2) = p(2) xor b(2) C = C + c(2) + . . + . . + . . + b(i) = MD5(S + c(i-1)) c(i) = p(i) xor b(i) C = C + c(i) + + The resulting encrypted String field will contain + c(1)+c(2)+...+c(i). */ const p: Buffer[] = []; @@ -171,20 +182,11 @@ export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: strin const R = authenticator; const A = salt; - // console.log('S', S); - // console.log('R', R); - // console.log('A', A); - - // const P = Buffer.alloc(16); - let C; const c: { [key: number]: Buffer } = {}; const b: { [key: number]: Buffer } = {}; - // console.log('S + R + A', S + R + A); - for (let i = 0; i < p.length; i++) { - // one octet is 8.. therefore +=2 means next 16 if (!i) { b[i] = md5Hex(Buffer.concat([Buffer.from(S), R, A])); } else { @@ -197,29 +199,10 @@ export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: strin c[i][n] = p[i][n] ^ b[i][n]; } - // console.log('c['+i+']', c[i]); - // console.log('b['+i+']', b[i]); - C = C ? Buffer.concat([C, c[i]]) : c[i]; } const bufferC = Buffer.from(C); - console.log('BUFFER C', bufferC.length, bufferC); - return Buffer.concat([salt, bufferC]); - /* - Zorn Informational [Page 21] - - RFC 2548 Microsoft Vendor-specific RADIUS Attributes March 1999 - - - b(1) = MD5(S + R + A) c(1) = p(1) xor b(1) C = c(1) - b(2) = MD5(S + c(1)) c(2) = p(2) xor b(2) C = C + c(2) - . . - . . - . . - b(i) = MD5(S + c(i-1)) c(i) = p(i) xor b(i) C = C + c(i) - The resulting encrypted String field will contain - c(1)+c(2)+...+c(i). - */ + return Buffer.concat([salt, bufferC]); } diff --git a/src/types/Handler.ts b/src/types/Handler.ts index 5c3b929..f368e61 100644 --- a/src/types/Handler.ts +++ b/src/types/Handler.ts @@ -1,7 +1,18 @@ +import { RadiusPacket } from 'radius'; + export type ResponseHandler = (msg: Buffer) => void; -export type ResponseAuthHandler = (username: string, password: string, identifier: number) => void; +export type ResponseAuthHandler = ( + username: string, + password: string, + additionalAuthHandler?: AdditionalAuthHandler +) => void; export interface IResponseHandlers { response: ResponseHandler; checkAuth: ResponseAuthHandler; } + +export type AdditionalAuthHandler = ( + success: boolean, + params: { packet: RadiusPacket; attributes: any[]; secret: string } +) => void;