fix: add MS-MPPE-Send-Key and MS-MPPE-Recv-Key

master
simon 4 years ago
parent 13f10bae59
commit 7e28c60d81

@ -1,17 +1,17 @@
import * as dgram from 'dgram'; import * as dgram from 'dgram';
import * as radius from 'radius'; import * as radius from 'radius';
// import * as dgram from "dgram"; // import * as dgram from "dgram";
import * as fs from 'fs'; // import * as fs from 'fs';
import { EAPHandler } from './eap'; import { EAPHandler } from './eap';
import { encodeTunnelPW } from './tls/crypt';
import { makeid } from './helpers'; import { makeid } from './helpers';
import { LDAPAuth } from './ldap'; import { LDAPAuth } from './ldap';
import { AdditionalAuthHandler } from './types/Handler';
const server = dgram.createSocket('udp4'); const server = dgram.createSocket('udp4');
// not used right now, using stunnel to connect to ldap // not used right now, using stunnel to connect to ldap
const tlsOptions = { /* const tlsOptions = {
key: fs.readFileSync('ldap.gsuite.hokify.com.40567.key'), key: fs.readFileSync('ldap.gsuite.hokify.com.40567.key'),
cert: fs.readFileSync('ldap.gsuite.hokify.com.40567.crt'), 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. // This is necessary only if the client uses the self-signed certificate.
ca: [fs.readFileSync('ldap.gsuite.hokify.com.40567.key')] ca: [fs.readFileSync('ldap.gsuite.hokify.com.40567.key')]
}; }; */
const { argv } = require('yargs') const { argv } = require('yargs')
.usage('Simple Google LDAP <> RADIUS Server\nUsage: $0') .usage('Simple Google LDAP <> RADIUS Server\nUsage: $0')
@ -62,81 +62,33 @@ server.on('message', async function(msg, rinfo) {
// console.log('rinfo', 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}`); console.log(`Access-Request for ${username}`);
let code: 'Access-Accept' | 'Access-Reject'; let success = false;
try { try {
await ldap.authenticate(username, password); await ldap.authenticate(username, password);
code = 'Access-Accept'; success = true;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
code = 'Access-Reject';
} }
const attributes: any[] = []; 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']) { if (additionalAuthHandler) {
// reappend username to response await additionalAuthHandler(success, { packet, attributes, secret: argv.secret });
attributes.push(['User-Name', packet.attributes['User-Name']]);
} }
/*
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({ const response = radius.encode_response({
packet, packet,
code, code: success ? 'Access-Accept' : 'Access-Reject',
secret: argv.secret, secret: argv.secret,
attributes 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) { server.send(response, 0, response.length, rinfo.port, rinfo.address, function(err, _bytes) {
if (err) { if (err) {

@ -1,6 +1,7 @@
import * as events from 'events'; import * as events from 'events';
import { openTLSSockets, startTLSServer } from '../tls/crypt'; import * as tls from 'tls';
import { IResponseHandlers } from '../types/Handler'; import { encodeTunnelPW, openTLSSockets, startTLSServer } from '../tls/crypt';
import { AdditionalAuthHandler, IResponseHandlers } from '../types/Handler';
import { PAPChallenge } from './challenges/pap'; import { PAPChallenge } from './challenges/pap';
import { IEAPType } from '../types/EAPType'; import { IEAPType } from '../types/EAPType';
@ -44,16 +45,20 @@ export class EAPTTLS implements IEAPType {
dataStr: data.toString() dataStr: data.toString()
}); });
let sslLayer = openTLSSockets.get(state) as let currentConnection = openTLSSockets.get(state) as
| { socket: events.EventEmitter; currentHandlers: IResponseHandlers } | { events: events.EventEmitter; tls: tls.TLSSocket; currentHandlers: IResponseHandlers }
| undefined; | undefined;
if (!sslLayer) { if (!currentConnection) {
const newSocket = startTLSServer(); const connection = startTLSServer();
sslLayer = { socket: newSocket, currentHandlers: handlers }; currentConnection = {
openTLSSockets.set(state, sslLayer); events: connection.events,
tls: connection.tls,
currentHandlers: handlers
};
openTLSSockets.set(state, currentConnection);
// register event listeners // register event listeners
newSocket.on('incoming', (incomingData: Buffer) => { currentConnection.events.on('incoming', (incomingData: Buffer) => {
const type = incomingData.slice(3, 4).readUInt8(0); const type = incomingData.slice(3, 4).readUInt8(0);
// const code = data.slice(4, 5).readUInt8(0); // const code = data.slice(4, 5).readUInt8(0);
@ -61,13 +66,18 @@ export class EAPTTLS implements IEAPType {
case 1: // PAP / CHAP case 1: // PAP / CHAP
try { try {
const { username, password } = this.papChallenge.decode(incomingData); const { username, password } = this.papChallenge.decode(incomingData);
sslLayer!.currentHandlers.checkAuth(username, password, identifier); currentConnection!.currentHandlers.checkAuth(username, password);
} catch (err) { } catch (err) {
// pwd not found.. // pwd not found..
console.error('pwd not found', err); console.error('pwd not found', err);
// NAK // NAK
this.sendEAPResponse(sslLayer!.currentHandlers.response, identifier, undefined, 3); this.sendEAPResponse(
newSocket.emit('end'); currentConnection!.currentHandlers.response,
identifier,
undefined,
3
);
currentConnection!.events.emit('end');
throw new Error(`pwd not found`); throw new Error(`pwd not found`);
} }
break; break;
@ -75,20 +85,20 @@ export class EAPTTLS implements IEAPType {
console.log('data', incomingData); console.log('data', incomingData);
console.log('data str', incomingData.toString()); console.log('data str', incomingData.toString());
newSocket.emit('end'); currentConnection!.events.emit('end');
throw new Error(`unsupported auth type${type}`); 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); console.log('sending encrypted data back to client', responseData);
// send back... // send back...
this.sendEAPResponse(sslLayer!.currentHandlers.response, identifier, responseData); this.sendEAPResponse(currentConnection!.currentHandlers.response, identifier, responseData);
// this.sendMessage(TYPE.PRELOGIN, data, false); // this.sendMessage(TYPE.PRELOGIN, data, false);
}); });
newSocket.on('end', () => { currentConnection.events.on('end', () => {
// cleanup socket // cleanup socket
console.log('ENDING SOCKET'); console.log('ENDING SOCKET');
openTLSSockets.del(state); openTLSSockets.del(state);
@ -98,13 +108,78 @@ export class EAPTTLS implements IEAPType {
} }
// update handlers // update handlers
sslLayer.currentHandlers = { currentConnection.currentHandlers = {
...handlers, ...handlers,
checkAuth: (username: string, password: string) => checkAuth: (username: string, password: string) => {
handlers.checkAuth(username, password, identifier) 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 // emit data to tls server
sslLayer.socket.emit('send', data); currentConnection.events.emit('send', data);
} }
} }

@ -16,7 +16,7 @@ const tlsOptions = {
const secureContext = createSecureContext(tlsOptions); const secureContext = createSecureContext(tlsOptions);
export const openTLSSockets = new NodeCache({ useClones: false, stdTTL: 3600 }); // keep sockets for about one hour 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 duplexpair = new DuplexPair();
const emitter = new events.EventEmitter(); const emitter = new events.EventEmitter();
@ -58,7 +58,7 @@ export function startTLSServer(): events.EventEmitter {
emitter.emit('incoming', data); emitter.emit('incoming', data);
}); });
cleartext.once('close', (data: Buffer) => { cleartext.once('close', (_data: Buffer) => {
console.log('cleartext close'); console.log('cleartext close');
emitter.emit('end'); emitter.emit('end');
}); });
@ -82,7 +82,10 @@ export function startTLSServer(): events.EventEmitter {
emitter.emit('end'); emitter.emit('end');
}); });
return emitter; return {
events: emitter,
tls: cleartext
};
} }
function md5Hex(buffer: Buffer): Buffer { function md5Hex(buffer: Buffer): Buffer {
@ -91,18 +94,15 @@ function md5Hex(buffer: Buffer): Buffer {
return hasher.digest(); // new Buffer(hasher.digest("binary"), "binary"); return hasher.digest(); // new Buffer(hasher.digest("binary"), "binary");
} }
export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: string): Buffer { // alloc_size
// 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
// 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); export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: string): Buffer {
console.log('authenticator', authenticator); // see freeradius TTLS implementation how to obtain "key"......
console.log('secret', secret);
// https://tools.ietf.org/html/rfc2548 // https://tools.ietf.org/html/rfc2548
/** /**
@ -120,9 +120,6 @@ export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: strin
Buffer.from(makeid(1)) Buffer.from(makeid(1))
]); ]);
console.log('salt', salt);
// ensure left most bit is set to 1
/* /*
String String
The plaintext String field consists of three logical sub-fields: 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. used for padding. Call this plaintext P.
*/ */
console.log('key', key.length, key);
let P = Buffer.concat([new Uint8Array([key.length]), key]); // + key + padding; let P = Buffer.concat([new Uint8Array([key.length]), key]); // + key + padding;
// fill up with 0x00 till we have % 16 // fill up with 0x00 till we have % 16
while (P.length % 16 !== 0) { while (P.length % 16 !== 0) {
P = Buffer.concat([P, Buffer.from([0x00])]); P = Buffer.concat([P, Buffer.from([0x00])]);
} }
// console.log('PLAINTEXT', P.length, P);
/* /*
Call the shared secret S, the pseudo-random 128-bit Request Call the shared secret S, the pseudo-random 128-bit Request
Authenticator (from the corresponding Access-Request packet) R, 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 Intermediate values b(1), b(2)...c(i) are required. Encryption
is performed in the following manner ('+' indicates is performed in the following manner ('+' indicates
concatenation): 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[] = []; const p: Buffer[] = [];
@ -171,20 +182,11 @@ export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: strin
const R = authenticator; const R = authenticator;
const A = salt; const A = salt;
// console.log('S', S);
// console.log('R', R);
// console.log('A', A);
// const P = Buffer.alloc(16);
let C; let C;
const c: { [key: number]: Buffer } = {}; const c: { [key: number]: Buffer } = {};
const b: { [key: number]: Buffer } = {}; const b: { [key: number]: Buffer } = {};
// console.log('S + R + A', S + R + A);
for (let i = 0; i < p.length; i++) { for (let i = 0; i < p.length; i++) {
// one octet is 8.. therefore +=2 means next 16
if (!i) { if (!i) {
b[i] = md5Hex(Buffer.concat([Buffer.from(S), R, A])); b[i] = md5Hex(Buffer.concat([Buffer.from(S), R, A]));
} else { } else {
@ -197,29 +199,10 @@ export function encodeTunnelPW(key: Buffer, authenticator: Buffer, secret: strin
c[i][n] = p[i][n] ^ b[i][n]; 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]; C = C ? Buffer.concat([C, c[i]]) : c[i];
} }
const bufferC = Buffer.from(C); 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 return Buffer.concat([salt, bufferC]);
c(1)+c(2)+...+c(i).
*/
} }

@ -1,7 +1,18 @@
import { RadiusPacket } from 'radius';
export type ResponseHandler = (msg: Buffer) => void; 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 { export interface IResponseHandlers {
response: ResponseHandler; response: ResponseHandler;
checkAuth: ResponseAuthHandler; checkAuth: ResponseAuthHandler;
} }
export type AdditionalAuthHandler = (
success: boolean,
params: { packet: RadiusPacket; attributes: any[]; secret: string }
) => void;

Loading…
Cancel
Save