initial commit

This commit is contained in:
simon 2020-02-15 00:20:24 +01:00
commit 807e1508d7
19 changed files with 4830 additions and 0 deletions

41
README.md Normal file
View File

@ -0,0 +1,41 @@
Basic RADIUS Server for node.js for Google LDAP Service and WPA2 Enteprise WLAN Authentification.
* Only implements LDAP as Authentification Backend
* Only WPA TTLS implemented (as this is the only one that works with Google LDAP Service)
## Known Issues / Disclaimer
This is a first implementation draft, which is currently NOT WORKING:
There is still one major issue left to get things going:
https://github.com/nodejs/node/issues/31802
that's why it's currently not possible to calculate MS-MPPE-Send-Key and MS-MPPE-Recv-Key.
* PAP / CHAP RFC not found to implement this correctly
* Project needs more structure and interfaces to extend it more easily in the future (make a full radius server out of it ;)?)
* No package queuing or any kind of respsecting the MTU size
* a lot of bugs
CONTRIBUTIONS WELCOME!
## Installation
npm install
npm run build
## Introduction
This app provides a radius server to authenticate against google's SLDAP service. To get this running
you need:
1.) Running LDAP Service (E.g. Google Suite Enterprise or Gloud Identity Premium)
2.) Use stunnel to connect to the LDAP service and connect this app to the stunnel (I didn't get the client ldap authentication working in here yet)
3.) Install a SSL certificate (e.g. self signed via npm run create-certificate)
4.) Install und build server: npm install && npm run build
5.) Start server node dist/app.ts --secret {RADIUS secret} --baseDN dc=hokify,dc=com
## Usage
You need to specify at least a radius password and the base DN for LDAP:
node dist/app.ts --secret {RADIUS secret} --baseDN dc=hokify,dc=com

BIN
eapol_test Executable file

Binary file not shown.

3
notes Normal file
View File

@ -0,0 +1,3 @@
https://github.com/retailnext/node-radius/issues/29
https://stackoverflow.com/questions/60232165/ssl-export-keying-material-in-node-js

3797
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "radius-server",
"description": "radius server for google LDAP and TTLT",
"version": "0.0.1",
"scripts": {
"start": "node dist/app.js",
"build": "tsc",
"dev": "ts-node src/app.ts",
"test-ttls-pap": "eapol_test -c ./ttls-pap.conf -s testing123",
"create-certificate": "sh ./ssl/create.sh && sh ./ssl/sign.sh"
},
"dependencies": {
"native-duplexpair": "^1.0.0",
"ldapjs": "^1.0.2",
"node-cache": "^5.1.0",
"radius": "~1.1.4",
"ts-node": "^8.6.2",
"type-cacheable": "^4.0.0",
"yargs": "~15.1.0",
"md5": "^2.2.1"
},
"devDependencies": {
"@types/ldapjs": "^1.0.5",
"@types/radius": "0.0.28",
"@hokify/eslint-config": "^0.2.2",
"eslint": "^6.8.0",
"prettier": "^1.19.1",
"typescript": "^3.7.5"
}
}

190
src/app.ts Normal file
View File

@ -0,0 +1,190 @@
import * as dgram from 'dgram';
import * as radius from 'radius';
// import * as dgram from "dgram";
import * as fs from 'fs';
import { EAPHandler } from './eap';
import { encodeTunnelPW } from './tls/crypt';
import { makeid } from './helpers';
import { LDAPAuth } from './ldap';
const server = dgram.createSocket('udp4');
// not used right now, using stunnel to connect to ldap
const tlsOptions = {
key: fs.readFileSync('ldap.gsuite.hokify.com.40567.key'),
cert: fs.readFileSync('ldap.gsuite.hokify.com.40567.crt'),
// This is necessary only if using the client certificate authentication.
requestCert: true,
// 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')
.example('$0 --port 1812 -s radiussecret')
.default({
port: 1812,
s: 'testing123',
baseDN: 'dc=hokify,dc=com',
ldapServer: 'ldap://127.0.0.1:1636'
})
.describe('baseDN', 'LDAP Base DN')
.describe('ldapServer', 'LDAP Server')
.describe('port', 'RADIUS server listener port')
.alias('s', 'secret')
.describe('secret', 'RADIUS secret')
.string(['secret', 'baseDN'])
.demand('secret');
console.log(`Listener Port: ${argv.port}`);
console.log(`RADIUS Secret: ${argv.secret}`);
console.log(`LDAP Base DN: ${argv.baseDN}`);
console.log(`LDAP Server: ${argv.ldapServer}`);
// const ldap = new LDAPAuth({url: 'ldap://ldap.google.com', base: 'dc=hokify,dc=com', uid: 'uid', tlsOptions});
const ldap = new LDAPAuth(argv.ldapServer, argv.baseDN);
const eapHandler = new EAPHandler();
server.on('message', async function(msg, rinfo) {
const packet = radius.decode({ packet: msg, secret: argv.secret });
if (packet.code !== 'Access-Request') {
console.log('unknown packet type: ', packet.code);
return;
}
// console.log('packet.attributes', packet.attributes);
// console.log('rinfo', rinfo);
async function checkAuth(username: string, password: string, EAPMessageIdentifier?: number) {
console.log(`Access-Request for ${username}`);
let code: 'Access-Accept' | 'Access-Reject';
try {
await ldap.authenticate(username, password);
code = 'Access-Accept';
} 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 (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,
secret: argv.secret,
attributes
});
console.log(`Sending ${code} for user ${username}`);
server.send(response, 0, response.length, rinfo.port, rinfo.address, function(err, _bytes) {
if (err) {
console.log('Error sending response to ', rinfo);
}
});
}
if (packet.attributes['EAP-Message']) {
const state = (packet.attributes.State && packet.attributes.State.toString()) || makeid(16);
// EAP MESSAGE
eapHandler.handleEAPMessage(packet.attributes['EAP-Message'], state, {
response: (EAPMessage: Buffer) => {
const attributes: any = [['State', Buffer.from(state)]];
let sentDataSize = 0;
do {
if (EAPMessage.length > 0) {
attributes.push(['EAP-Message', EAPMessage.slice(sentDataSize, sentDataSize + 253)]);
sentDataSize += 253;
}
} while (sentDataSize < EAPMessage.length);
const response = radius.encode_response({
packet,
code: 'Access-Challenge',
secret: argv.secret,
attributes
});
server.send(response, 0, response.length, rinfo.port, rinfo.address, function(err, _bytes) {
if (err) {
console.log('Error sending response to ', rinfo);
}
});
},
checkAuth
});
} else {
const username = packet.attributes['User-Name'];
const password = packet.attributes['User-Password'];
checkAuth(username, password);
}
});
server.on('listening', function() {
const address = server.address();
console.log(`radius server listening ${address.address}:${address.port}`);
});
server.bind(argv.port);

203
src/eap.ts Normal file
View File

@ -0,0 +1,203 @@
// https://tools.ietf.org/html/rfc3748#section-4.1
// https://tools.ietf.org/html/rfc5281 TTLS v0
// https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented)
import { IResponseHandlers, ResponseHandler } from './types/Handler';
import { EAPTTLS } from './eap/eap-ttls';
export class EAPHandler {
maxFragmentSize = 1400; // @todo .. take framed-mtu into account from AVPs
eapTTLS: EAPTTLS;
constructor() {
this.eapTTLS = new EAPTTLS(this.sendEAPResponse);
}
/**
*
* @param data
* @param type 1 = identity, 21 = EAP-TTLS, 2 = notification, 4 = md5-challenge, 3 = NAK
*/
private sendEAPResponse(
response: ResponseHandler,
identifier: number,
data?: Buffer,
msgType = 21,
msgFlags = 0b00000000
) {
let i = 0;
do {
const fragmentMaxPart =
data && (i + 1) * this.maxFragmentSize > data.length
? undefined
: (i + 1) * this.maxFragmentSize;
const sslPart = data && data.slice(i * this.maxFragmentSize, fragmentMaxPart);
const includeLength =
data &&
i === 0 &&
fragmentMaxPart !== undefined; /* firsrt one and we have more, therefore include length */
// console.log('includeLength', includeLength, fragmentMaxPart, i)
i += 1;
/*
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
(fragmentMaxPart /* we have more */ ? 0b01000000 : 0); // set M bit
let buffer = Buffer.from([
1, // request
identifier + 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]);
}
const resBuffer = sslPart ? Buffer.concat([buffer, sslPart]) : buffer;
resBuffer.writeUInt16BE(resBuffer.byteLength, 2);
console.log('EAP RESPONSE', {
code: 1,
identifier: identifier + 1,
length: (includeLength && data && data.byteLength) || 0,
msgType,
flags,
data
});
// uffer.from([1,identifier, 0, 0, 21, 0]);
// buffer.writeUInt16BE(sslResponse.length, 2); // length
// buffer.writeInt8(21, 4); // eap-ttls
// buffer.writeInt8(0, 5); // flags
/*
@todo: this is wrong,
if there are more messages, add them to a queue
and process the next one when client has ack. (message without data)
*/
response(resBuffer);
} while (data && i * this.maxFragmentSize < data.length);
}
handleEAPMessage(msg: Buffer, state: string, handlers: IResponseHandlers) {
// const b = Buffer.from([2,0x242,0x0,0x18,0x1,0x115,0x105,0x109,0x111,0x110,0x46,0x116,0x114,0x101,0x116,0x116,0x101,0x114]);
// const msg = Buffer.from([2, 162, 0, 18, 1, 115, 105, 109, 111, 110, 46, 116, 114, 101, 116, 116, 101, 114])
/*
1 Request
2 Response
3 Success
4 Failure
*/
const code = msg.slice(0, 1).readUInt8(0);
const identifier = msg.slice(1, 2).readUInt8(0); // .toString('hex');
// const length = msg.slice(2, 4).readInt16BE(0); // .toString('binary');
const type = msg.slice(4, 5).readUInt8(0); // .slice(3,0x5).toString('hex');
/*
console.log("CODE", code);
console.log('ID', identifier);
console.log('length', length);
*/
switch (code) {
case 1: // for request
case 2: // for response
switch (type) {
case 1: // identifiy
/**
* 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...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
console.log('RESPONDING WITH IDENTIDY / START');
this.sendEAPResponse(handlers.response, identifier, undefined, 21, 0x20);
/*
handlers.response(
Buffer.from([
1, // request
identifier + 1,
0, // length (1/2)
6, // length (2/2)
21, // EAP-TTLS
0x20 // flags: 001000 start flag
])
); */
break;
case 21: // EAP TTLS
this.eapTTLS.handleMessage(msg, state, handlers, identifier);
return;
case 3: // nak
this.sendEAPResponse(handlers.response, identifier, undefined, 3);
break;
case 2: // notification
console.info('notification');
break;
case 4: // md5-challenge
console.info('md5-challenge');
break;
case 254: // expanded type
console.error('not implemented type', type);
break;
default:
console.error('unsupported type', type);
break;
}
break;
case 3:
console.log('Client Auth Success');
break;
case 4:
console.log('Client Auth FAILURE');
break;
default:
break;
// silently ignor;
}
}
}

44
src/eap/challenges/pap.ts Normal file
View File

@ -0,0 +1,44 @@
import { IAuthChallenge } from '../../types/AuthChallenge';
export class PAPChallenge implements IAuthChallenge {
// i couldn't find any documentation about it, therefore best guess how this is processed...
// http://www.networksorcery.com/enp/rfc/rfc1334.txt ?
decode(data: Buffer) {
const usrNameLength = data.slice(7, 8).readUInt8(0);
const user = data.slice(8, usrNameLength);
console.log('user', user, user.toString().trim());
let pwdStart = usrNameLength; // data.slice(usrNameLength);
const passwordDelimeter = Buffer.from([0x02, 0x40, 0x00, 0x00]);
let found = false;
let pwd: Buffer;
do {
const possibleDelimieter = data.slice(pwdStart, pwdStart + passwordDelimeter.length);
if (possibleDelimieter.equals(passwordDelimeter)) {
found = true;
}
if (!found) {
pwdStart++;
}
} while (!found && pwdStart < data.length);
if (!found) {
throw new Error("couldn't extract password");
}
// console.log('pwdStart+passwordDelimeter.length', pwdStart+passwordDelimeter.length);
// console.log('length', pwdStart + data.readUInt8(pwdStart+passwordDelimeter.length));
// first byte is a length property.. we ignore for now
pwd = data.slice(pwdStart + passwordDelimeter.length + 1); // , pwdStart+ data.readUInt8(pwdStart+passwordDelimeter.length));
// trim pwd
pwd = pwd.slice(0, pwd.indexOf(0x00));
console.log('pwd', pwd, pwd.toString().trim().length, pwd.toString());
return {
username: user.toString(),
password: pwd.toString()
};
}
}

110
src/eap/eap-ttls.ts Normal file
View File

@ -0,0 +1,110 @@
import * as events from 'events';
import { openTLSSockets, startTLSServer } from '../tls/crypt';
import { IResponseHandlers } from '../types/Handler';
import { PAPChallenge } from './challenges/pap';
import { IEAPType } from '../types/EAPType';
export class EAPTTLS implements IEAPType {
papChallenge: PAPChallenge;
constructor(private sendEAPResponse) {
this.papChallenge = new PAPChallenge();
}
handleMessage(msg: Buffer, state: string, handlers, identifier: number) {
const flags = msg.slice(5, 6); // .toString('hex');
// if (flags)
// @todo check if "L" flag is set in flags
const msglength = msg.slice(6, 10).readInt32BE(0); // .toString('hex');
const data = msg.slice(6, msg.length); // 10); //.toString('hex');
// check if no data package is there and we have something in the queue, if so.. empty the queue first
if (!data) {
// @todo: queue processing
console.warn('no data, just a confirmation!');
return;
}
console.log('incoming EAP TTLS', {
flags /*
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)
*/,
msglength,
data,
dataStr: data.toString()
});
let sslLayer = openTLSSockets.get(state) as
| { socket: events.EventEmitter; currentHandlers: IResponseHandlers }
| undefined;
if (!sslLayer) {
const newSocket = startTLSServer();
sslLayer = { socket: newSocket, currentHandlers: handlers };
openTLSSockets.set(state, sslLayer);
// register event listeners
newSocket.on('incoming', (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);
sslLayer!.currentHandlers.checkAuth(username, password, identifier);
} catch (err) {
// pwd not found..
console.error('pwd not found', err);
// NAK
this.sendEAPResponse(sslLayer!.currentHandlers.response, identifier, undefined, 3);
newSocket.emit('end');
throw new Error(`pwd not found`);
}
break;
default:
console.log('data', incomingData);
console.log('data str', incomingData.toString());
newSocket.emit('end');
throw new Error(`unsupported auth type${type}`);
}
});
newSocket.on('response', (responseData: Buffer) => {
console.log('sending encrypted data back to client', responseData);
// send back...
this.sendEAPResponse(sslLayer!.currentHandlers.response, identifier, responseData);
// this.sendMessage(TYPE.PRELOGIN, data, false);
});
newSocket.on('end', () => {
// cleanup socket
console.log('ENDING SOCKET');
openTLSSockets.del(state);
});
} else {
console.log('using existing socket');
}
// update handlers
sslLayer.currentHandlers = {
...handlers,
checkAuth: (username: string, password: string) =>
handlers.checkAuth(username, password, identifier)
};
// emit data to tls server
sslLayer.socket.emit('send', data);
}
}

9
src/helpers.ts Normal file
View File

@ -0,0 +1,9 @@
export function makeid(length) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

124
src/ldap.ts Normal file
View File

@ -0,0 +1,124 @@
import * as NodeCache from 'node-cache';
import { createClient, Client } from 'ldapjs';
const usernameFields = ['posixUid', 'mail'];
// TLS:
// https://github.com/ldapjs/node-ldapjs/issues/307
export class LDAPAuth {
cache = new NodeCache();
ldap: Client;
lastDNsFetch: Date;
allValidDNsCache: { [key: string]: string };
constructor(private url: string, private base: string, tlsOptions?) {
this.ldap = createClient({ url, tlsOptions }).on('error', error => {
console.error('Error in ldap', error);
});
this.fetchDNs();
}
async fetchDNs() {
const dns: { [key: string]: string } = {};
await new Promise((resolve, reject) => {
this.ldap.search(
this.base,
{
scope: 'sub'
},
(err, res) => {
if (err) {
reject(err);
return;
}
res.on('searchEntry', function(entry) {
// console.log('entry: ' + JSON.stringify(entry.object));
usernameFields.forEach(field => {
const index = entry.object[field] as string;
dns[index] = entry.object.dn;
});
});
res.on('searchReference', function(referral) {
console.log(`referral: ${referral.uris.join()}`);
});
res.on('error', function(ldapErr) {
console.error(`error: ${ldapErr.message}`);
reject();
});
res.on('end', result => {
console.log(`status: ${result?.status}`);
// replace with new dns
this.allValidDNsCache = dns;
// console.log('allValidDNsCache', this.allValidDNsCache);
resolve();
});
}
);
});
this.lastDNsFetch = new Date();
}
async authenticate(username: string, password: string, count = 0, forceFetching = false) {
const cacheKey = `usr:${username}|pwd:${password}`;
const fromCache = this.cache.get(cacheKey);
if (fromCache) {
return fromCache;
}
const cacheValidTime = new Date();
cacheValidTime.setHours(cacheValidTime.getHours() - 12);
let dnsFetched = false;
if (!this.lastDNsFetch || this.lastDNsFetch < cacheValidTime || forceFetching) {
console.log('fetching dns');
await this.fetchDNs();
dnsFetched = true;
}
if (count > 5) {
throw new Error('Failed to authenticate with LDAP!');
}
// const dn = ;
const dn = this.allValidDNsCache[username];
if (!dn) {
if (!dnsFetched && !forceFetching) {
return this.authenticate(username, password, count, true);
}
throw new Error(`invalid username, not found in DN: ${username}`);
}
await new Promise((resolve, reject) => {
this.ldap.bind(dn, password, (err, res) => {
if (err) {
if (err && (err as any).stack && (err as any).stack.includes(`${this.url} closed`)) {
count++;
// wait 1 second to give the ldap error handler time to reconnect
setTimeout(() => resolve(this.authenticate(dn, password)), 2000);
return;
}
console.error('ldap error', err);
reject(err);
}
if (res) resolve(res);
else reject();
});
});
this.cache.set(cacheKey, true, 86400);
return true;
}
}

225
src/tls/crypt.ts Normal file
View File

@ -0,0 +1,225 @@
import * as NodeCache from 'node-cache';
import * as events from 'events';
import * as tls from 'tls';
import { createSecureContext } from 'tls';
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as DuplexPair from 'native-duplexpair';
import { makeid } from '../helpers';
// https://nodejs.org/api/tls.html
const tlsOptions = {
cert: fs.readFileSync('./ssl/public-cert.pem'),
key: fs.readFileSync('./ssl/private-key.pem'),
ecdhCurve: 'auto'
};
const secureContext = createSecureContext(tlsOptions);
export const openTLSSockets = new NodeCache({ useClones: false, stdTTL: 3600 }); // keep sockets for about one hour
export function startTLSServer(): events.EventEmitter {
const duplexpair = new DuplexPair();
const emitter = new events.EventEmitter();
const cleartext = new tls.TLSSocket(duplexpair.socket1, {
secureContext,
isServer: true
});
const encrypted = duplexpair.socket2;
emitter.on('send', (data: Buffer) => {
encrypted.write(data);
// encrypted.sync();
});
encrypted.on('data', (data: Buffer) => {
// console.log('encrypted data', data, data.toString());
emitter.emit('response', data);
});
cleartext.on('secure', () => {
const cipher = cleartext.getCipher();
/*
console.log('Authorized', cleartext.authorized);
console.log('getTLSTicket', cleartext.getTLSTicket());
console.log('getEphemeralKeyInfo', cleartext.getEphemeralKeyInfo());
console.log('getPeerCertificate', cleartext.getPeerCertificate());
console.log('getSharedSigalgs', cleartext.getSharedSigalgs());
console.log('getCertificate', cleartext.getCertificate());
console.log('getSession', cleartext.getSession());
*/
if (cipher) {
console.log(`TLS negotiated (${cipher.name}, ${cipher.version})`);
}
cleartext.on('data', (data: Buffer) => {
// console.log('cleartext data', data, data.toString());
emitter.emit('incoming', data);
});
cleartext.once('close', (data: Buffer) => {
console.log('cleartext close');
emitter.emit('end');
});
cleartext.on('keylog', line => {
console.log('############ KEYLOG #############', line);
// cleartext.getTicketKeys()
});
console.log('*********** new client connection established / secured ********');
// this.emit('secure', securePair.cleartext);
// this.encryptAllFutureTraffic();
});
cleartext.on('error', (err?: Error) => {
console.log('cleartext error', err);
encrypted.destroy();
cleartext.destroy(err);
emitter.emit('end');
});
return emitter;
}
function md5Hex(buffer: Buffer): Buffer {
const hasher = crypto.createHash('md5');
hasher.update(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
// but not available in NODE JS
console.log('KEY', key);
console.log('authenticator', authenticator);
console.log('secret', secret);
// https://tools.ietf.org/html/rfc2548
/**
* Salt
The Salt field is two octets in length and is used to ensure the
uniqueness of the keys used to encrypt each of the encrypted
attributes occurring in a given Access-Accept packet. The most
significant bit (leftmost) of the Salt field MUST be set (1). The
contents of each Salt field in a given Access-Accept packet MUST
be unique.
*/
const salt = Buffer.concat([
// eslint-disable-next-line no-bitwise
Buffer.from((Number(makeid(1)) & 0b10000000).toString()), // ensure left most bit is set (1)
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:
the Key-Length and Key sub-fields (both of which are required),
and the optional Padding sub-field. The Key-Length sub-field is
one octet in length and contains the length of the unencrypted Key
sub-field. The Key sub-field contains the actual encryption key.
If the combined length (in octets) of the unencrypted Key-Length
and Key sub-fields is not an even multiple of 16, then the Padding
sub-field MUST be present. If it is present, the length of the
Padding sub-field is variable, between 1 and 15 octets. The
String field MUST be encrypted as follows, prior to transmission:
Construct a plaintext version of the String field by concate-
nating the Key-Length and Key sub-fields. If necessary, pad
the resulting string until its length (in octets) is an even
multiple of 16. It is recommended that zero octets (0x00) be
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,
and the contents of the Salt field A. Break P into 16 octet
chunks p(1), p(2)...p(i), where i = len(P)/16. Call the
ciphertext blocks c(1), c(2)...c(i) and the final ciphertext C.
Intermediate values b(1), b(2)...c(i) are required. Encryption
is performed in the following manner ('+' indicates
concatenation):
*/
const p: Buffer[] = [];
for (let i = 0; i < P.length; i += 16) {
p.push(P.slice(i, i + 16));
}
const S = secret;
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 {
b[i] = md5Hex(Buffer.concat([Buffer.from(S), c[i - 1]]));
}
c[i] = Buffer.alloc(16); // ''; //p[i];
for (let n = 0; n < p[i].length; n++) {
// eslint-disable-next-line no-bitwise
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).
*/
}

View File

@ -0,0 +1,3 @@
export interface IAuthChallenge {
decode(data: Buffer): { username: string; password: string };
}

3
src/types/EAPType.ts Normal file
View File

@ -0,0 +1,3 @@
export interface IEAPType {
handleMessage(msg: Buffer, state: string, handlers, identifier: number)
}

7
src/types/Handler.ts Normal file
View File

@ -0,0 +1,7 @@
export type ResponseHandler = (msg: Buffer) => void;
export type ResponseAuthHandler = (username: string, password: string, identifier: number) => void;
export interface IResponseHandlers {
response: ResponseHandler;
checkAuth: ResponseAuthHandler;
}

2
ssl/create.sh Executable file
View File

@ -0,0 +1,2 @@
openssl genrsa -out private-key.pem 1024
openssl req -new -key private-key.pem -out csr.pem

1
ssl/sign.sh Normal file
View File

@ -0,0 +1 @@
openssl x509 -req -in csr.pem -signkey private-key.pem -out public-cert.pem

11
tsconfig.eslint.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"*.js"
],
"compilerOptions": {
"allowJs": true,
"checkJs": true
}
}

27
tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
// target settings for node 10
"module": "commonjs",
"target": "es2018",
"lib": ["es2018"],
// other best practice configs
"moduleResolution": "node",
"strict": true,
"noImplicitAny": false, // <-- get rid of this!
"removeComments": false, // <-- do not remove comments (needed for @deprecated notices etc)
"emitDecoratorMetadata": true,
"composite": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"resolveJsonModule": true,
"sourceMap": true,
"isolatedModules": false,
"declaration": true
},
"exclude": ["node_modules", "**/__tests__"],
"include": ["./src"]
}