From f762506bf453ab60e86b69cef80153b0ce4a896f Mon Sep 17 00:00:00 2001 From: Artem Titoulenko Date: Wed, 8 Sep 2021 12:33:57 -0400 Subject: [PATCH] user authentication, start building out services --- src/communicator.js | 106 +++++++++++++++++++++ src/consts.js | 7 ++ src/index.js | 67 ++----------- src/services/authorization-registration.js | 79 +++++++++++++++ src/services/base.js | 21 ++++ src/structures.js | 56 ++++++----- 6 files changed, 253 insertions(+), 83 deletions(-) create mode 100644 src/communicator.js create mode 100644 src/consts.js create mode 100644 src/services/authorization-registration.js create mode 100644 src/services/base.js diff --git a/src/communicator.js b/src/communicator.js new file mode 100644 index 0000000..e80f1de --- /dev/null +++ b/src/communicator.js @@ -0,0 +1,106 @@ +const { FLAP, SNAC, TLV } = require('./structures'); +const { logDataStream } = require('./util'); +const { FLAGS_EMPTY } = require('./consts'); + +const AuthorizationRegistrationService = require("./services/authorization-registration"); + +class Communicator { + constructor(socket) { + // Hold on to the socket + this.socket = socket; + + this.socket.on('data', (data) => { + console.log('DATA-----------------------'); + console.log('RECV', logDataStream(Buffer.from(data, 'hex'))); + const flap = FLAP.fromBuffer(Buffer.from(data, 'hex')); + console.log('RECV', flap.toString()); + this.handleMessage(flap); + }); + + this._sequenceNumber = 0; + + this.registerServices(); + + this.start(); + } + + start() { + // Start negotiating a connection + const hello = new FLAP(0x01, 0, Buffer.from([0x00, 0x00, 0x00, 0x01])); + this.send(hello); + } + + registerServices() { + const services = [ + new AuthorizationRegistrationService(this), + ]; + + this.services = {}; + services.forEach((service) => { + this.services[service.family] = service; + }); + } + + _getNewSequenceNumber() { + return ++this._sequenceNumber; + } + + send(message) { + console.log('SEND', message.toString()); + console.log('SEND', logDataStream(message.toBuffer())); + console.log('-----------------------DATA'); + this.socket.write(message.toBuffer()); + } + + handleMessage(message) { + switch (message.channel) { + case 1: + const protocol = message.payload.readUInt32BE(); + + if (protocol !== 1) { + console.log('Unsupported protocol:', protocol); + this.socket.end(); + return; + } + + if (message.payload.length <= 4) { + return; + } + + const tlv = TLV.fromBuffer(message.payload.slice(4)); + console.log(tlv.toString()); + + switch (tlv.type) { + case 0x06: // Requesting available services + const resp = new FLAP(2, this._getNewSequenceNumber(), new SNAC(0x01, 0x03, FLAGS_EMPTY, 0, [ + Buffer.from([0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, + 0x00, 0x06, 0x00, 0x08, 0x00, 0x09, 0x00, 0x0A, + 0x00, 0x0B, 0x00, 0x0C, 0x00, 0x13, 0x00, 0x15, + ]), // Buddy List management service + ])); + this.send(resp); + return; + } + + return; + case 2: + if (!message.payload) { + console.error('No SNAC'); + return; + } + + const familyService = this.services[message.payload.family]; + if (!familyService) { + console.warn('no handler for family', message.payload.family); + return; + } + + familyService.handleMessage(message); + default: + console.warn('No handlers for channel', message.channel); + return; + } + } +} + +module.exports = Communicator; diff --git a/src/consts.js b/src/consts.js new file mode 100644 index 0000000..10797b4 --- /dev/null +++ b/src/consts.js @@ -0,0 +1,7 @@ +const AIM_MD5_STRING = "AOL Instant Messenger (SM)"; +const FLAGS_EMPTY = Buffer.from([0x00, 0x00, 0x00, 0x00]); + +module.exports = { + AIM_MD5_STRING, + FLAGS_EMPTY, +}; diff --git a/src/index.js b/src/index.js index f9e06bb..e8829fb 100644 --- a/src/index.js +++ b/src/index.js @@ -1,78 +1,29 @@ const net = require('net'); -const { logDataStream } = require('./util'); -const { FLAP } = require('./structures'); +const Communicator = require('./communicator'); + +const communicators = {}; const server = net.createServer((socket) => { console.log('client connected...'); - socket.setTimeout(5000); + socket.setTimeout(30000); socket.on('error', (e) => { console.error('socket encountered an error:', e); - }); - - socket.on('data', (data) => { - const flap = FLAP.fromBuffer(Buffer.from(data, 'hex')); - console.log('RECV', flap.toString()); + delete communicators[socket]; }); socket.on('timeout', () => { console.log('socket timeout'); socket.end(); - }) + delete communicators[socket]; + }); socket.on('end', () => { console.log('client disconnected...'); }); - const hello = new FLAP(0x01, 0, Buffer.from([0x00, 0x00, 0x00, 0x01])); - socket.write(hello.toBuffer()); - - /* 1. on connection, server sends - - 2a FLAP - 01 channel 1 - 00 01 datagram #1 - 00 04 4 bytes of data - 00 00 00 1 - */ - - /* 2. client responds - 2a FLAP - 01 channel 1 - 51 11 datagram 11 - 00 04 4 bytes - 00 00 00 01 - */ - - /* 3. client sends username - 2a FLAP - 02 channel 2 (SNAC) - 51 12 datagram 12 - 00 12 18 bytes of data - 00 17 Service (Authorization/registration service) - 00 06 Family (Request md5 authkey) - 00 00 Flags - 00 00 00 00 SNAC request id - 00 01 TLV.Type(0x01) - screen name - 00 04 TLV.Length (4) - 74 6f 6f 66 toof - */ - - /* 4. server responds - 2a FLAP - 02 Channel 2 (SNAC) - 00 02 datagram 2 - 00 16 22 bytes - 00 17 Service (Authorization/registration service) - 00 07 Server md5 authkey response - This snac contain server generated auth key. Client should use it to crypt password. - 00 00 Flags - 00 00 00 00 SNAC request ID - 00 0a Length (10 bytes) - ... - */ - - + const c = new Communicator(socket); + communicators[socket] = c; }); server.on('error', (err) => { diff --git a/src/services/authorization-registration.js b/src/services/authorization-registration.js new file mode 100644 index 0000000..ec98465 --- /dev/null +++ b/src/services/authorization-registration.js @@ -0,0 +1,79 @@ +const crypto = require('crypto'); +const BaseService = require('./base'); +const { FLAP, SNAC, TLV } = require('../structures'); + +const { AIM_MD5_STRING, FLAGS_EMPTY } = require('../consts'); + +const users = { + toof: 'foo', +}; + +class AuthorizationRegistrationService extends BaseService { + constructor(communicator) { + super({ family: 0x17, version: 0x01 }, communicator); + this.cipher = "HARDY"; + } + + handleMessage(message) { + switch (message.payload.service) { + case 0x02: // Client login request (md5 login sequence) + const tlvs = message.payload.tlvs; + const clientNameTLV = tlvs.find((tlv) => tlv.type === 0x03); + console.log("Attempting connection from", clientNameTLV.payload.toString('ascii')); + + const userTLV = tlvs.find((tlv) => tlv.type === 0x01); + const username = userTLV.payload.toString('ascii'); + + if (!users[username]) { + const authResp = new FLAP(2, this._getNewSequenceNumber(), + new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [ + new TLV(0x0001, Buffer.from(username)), // username + new TLV(0x0008, Buffer.from([0x00, 0x04])) // incorrect nick/password + ])); + + this.send(authResp); + return; + } + + const passwordHashTLV = tlvs.find((tlv) => tlv.type === 0x25); + + const pwHash = crypto.createHash('md5'); + pwHash.update(this.cipher); + pwHash.update(users[username]); + pwHash.update(AIM_MD5_STRING); + const digest = pwHash.digest('hex'); + + if (digest !== passwordHashTLV.payload.toString('hex')) { + console.log('Invalid password for', username); + const authResp = new FLAP(2, this._getNewSequenceNumber(), + new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [ + new TLV(0x0001, Buffer.from(username)), // username + new TLV(0x0008, Buffer.from([0x00, 0x04])) // incorrect nick/password + ])); + this.send(authResp); + return; + } + + const authResp = new FLAP(2, this._getNewSequenceNumber(), + new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [ + new TLV(0x01, Buffer.from(username)), // username + new TLV(0x05, Buffer.from('10.0.1.29:5190')), // BOS address + new TLV(0x06, Buffer.from('im a cookie uwu')) // Authorization cookie + ])); + + this.send(authResp); + return; + case 0x06: // Request md5 authkey + const payload = Buffer.alloc(2, 0xFF, 'hex'); + payload.writeUInt16BE(this.cipher.length); + const md5ReqResp = new FLAP(2, this._getNewSequenceNumber(), + new SNAC(0x17, 0x07, FLAGS_EMPTY, 0, [ + Buffer.concat([payload, Buffer.from(this.cipher, 'binary')]), + ])); + this.send(md5ReqResp); + break; + } + } +} + +module.exports = AuthorizationRegistrationService; diff --git a/src/services/base.js b/src/services/base.js new file mode 100644 index 0000000..4951acd --- /dev/null +++ b/src/services/base.js @@ -0,0 +1,21 @@ +class BaseService { + constructor({family, version}, communicator) { + this.family = family; + this.version = version; + this.communicator = communicator; + } + + send(message) { + this.communicator.send(message); + } + + _getNewSequenceNumber() { + return this.communicator._getNewSequenceNumber(); + } + + handleMessage(message) { + return null; + } +} + +module.exports = BaseService; diff --git a/src/structures.js b/src/structures.js index 63bc5a7..2b135f3 100644 --- a/src/structures.js +++ b/src/structures.js @@ -18,7 +18,7 @@ class TLV { } toString() { - return `TLV(${this.type}, ${this.len}, ${this.payload.toString('ascii')})`; + return `TLV(0x${this.type.toString(16).padStart(2, '0')}, ${this.len}, ${this.payload.toString('ascii')})`; } toBuffer() { @@ -35,14 +35,14 @@ class SNAC { const service = buf.slice(2,4).readInt16BE(0); const flags = buf.slice(4, 6); const requestID = buf.slice(6, 10).readInt32BE(0); - const payload = []; // SNACs can have multiple TLVs + const tlvs = []; // SNACs can have multiple TLVs - let payloadIdx = 10; - let cb = 0, cbLimit = 10; //circuit breaker - while (payloadIdx < payloadLength && cb < cbLimit) { - const tlv = TLV.fromBuffer(buf.slice(payloadIdx)); - payload.push(tlv); - payloadIdx += tlv.len + 4; // 4 bytes for TLV type + payload length + let tlvsIdx = 10; + let cb = 0, cbLimit = 20; //circuit breaker + while (tlvsIdx < payloadLength && cb < cbLimit) { + const tlv = TLV.fromBuffer(buf.slice(tlvsIdx)); + tlvs.push(tlv); + tlvsIdx += tlv.len + 4; // 4 bytes for TLV type + tlvs length cb++; } if (cb === cbLimit) { @@ -50,19 +50,19 @@ class SNAC { process.exit(1); } - return new SNAC(family, service, flags, requestID, payload); + return new SNAC(family, service, flags, requestID, tlvs); } - constructor(family, service, flags, requestID, payload) { + constructor(family, service, flags, requestID, tlvs = []) { this.family = family; this.service = service; this.flags = flags; this.requestID = requestID; - this.payload = payload; + this.tlvs = tlvs; } toString() { - return `SNAC(${this.family.toString(16)},${this.service.toString(16)}) #${this.requestID}\n ${this.payload}`; + return `SNAC(${this.family.toString(16)},${this.service.toString(16)}) #${this.requestID}\n ${this.tlvs}`; } toBuffer() { @@ -72,7 +72,13 @@ class SNAC { SNACHeader.writeUInt16BE(this.flags, 4); SNACHeader.writeUInt32BE(this.requestID, 6); - const payload = this.payload.map((tlv) => tlv.toBuffer()); + const payload = this.tlvs.map((thing) => { + if (thing instanceof TLV) { + return thing.toBuffer(); + } + return thing; + }); + return Buffer.concat([SNACHeader, ...payload]); } } @@ -80,17 +86,21 @@ class SNAC { class FLAP { static fromBuffer(buf) { assert.equal(buf[0], 0x2a, 'Expected 0x2a FLAP header'); - const channel = parseInt(buf[1], 16); - const datagramNumber = buf.slice(2,4).readInt16BE(0); + const channel = buf.readInt8(1); + const sequenceNumber = buf.slice(2,4).readInt16BE(0); const payloadLength = buf.slice(4, 6).readInt16BE(0); - const payload = buf.slice(6, 6 + payloadLength); + let payload = buf.slice(6, 6 + payloadLength); - return new FLAP(channel, datagramNumber, payload) + if (channel === 2) { + payload = SNAC.fromBuffer(payload, payloadLength); + } + + return new FLAP(channel, sequenceNumber, payload) } - constructor(channel, datagramNumber, payload) { + constructor(channel, sequenceNumber, payload) { this.channel = channel; - this.datagramNumber = datagramNumber; + this.sequenceNumber = sequenceNumber; this.payload = payload; this.payloadLength = this.payload.length; @@ -98,23 +108,19 @@ class FLAP { if (payload instanceof SNAC) { this.payloadLength = payload.toBuffer().length; } - - if (channel === 2 && !(payload instanceof SNAC)) { - this.payload = SNAC.fromBuffer(this.payload, this.payloadLength); - } } toString() { const hasSnac = this.payload instanceof SNAC; const payload = hasSnac ? this.payload.toString() : logDataStream(this.payload).split('\n').join('\n '); - return `ch:${this.channel}, dn: ${this.datagramNumber}, len: ${this.payloadLength}, payload:\n ${payload}` + return `ch:${this.channel}, dn: ${this.sequenceNumber}, len: ${this.payloadLength}, payload:\n ${payload}` } toBuffer() { const FLAPHeader = Buffer.alloc(6, 0, 'hex'); FLAPHeader.writeInt8(0x2a, 0); FLAPHeader.writeInt8(this.channel, 1); - FLAPHeader.writeInt16BE(this.datagramNumber, 2); + FLAPHeader.writeInt16BE(this.sequenceNumber, 2); FLAPHeader.writeInt16BE(this.payloadLength, 4); let payload = this.payload;