4
\$\begingroup\$

I've written a Node js server that create, read, and update user data from a SQL server hosted in Azure. Clients connect to the server using a standard TCP socket, every message sent or received will be encrypted/decrypted using AES 256 CBC.

Communication protocol

TCP is used to create a simple communication mean without the over head of HTTP/HTTPS, and the client will try to persist that connection in order to eliminate the need to connect every time a message is sent. That will also give us the ability to know the number of connected players, and to be able to send messages from the server to all connected players.

Authentication

There are no authentication at the moment, anyone could connect to the server using telnet or any TCP based client. However any data sent will be decrypted if decryption or JSON parsing failed the connection will be closed. I am open for any suggestion in this area.

Message format

All messages exchanged between the server and client uses JSON format. Messages sent from client to server are formatted as the following JSON message:

{
    "ID": 0 // a number indicating which function to call 
    "msg": "{
            // embedded JSON contains the data
            ..... 
            }"
}

Messages sent from server to client

  • Number indicating if the operation was successful(1) or not(0).

Or

  • JSON containing data about the player, if the operation was about fetching player data.

Message process

All JSON messages received are parsed asynchronously and messages sent to the client are stringifyied asynchronously to avoid Nodejs main loop blocking.


Server.js

    /* Library to add date and time to any console.log */
    require('log-timestamp');
    /* Asynchronous JSON parse library */
    var parseJSON = require('json-parse-async');
    /* Asynchronous JSON stringify library */
    var asyncJSON = require('async-json');
    /* Keymetrics advance metrics */
    var pmx = require('pmx').init({
        http: false, // HTTP routes logging (default: true)
        ignore_routes: [], // Ignore http routes (Default: [])
        errors: true, // Exceptions loggin (default: true)
        custom_probes: false, // Auto expose JS Loop Latency and HTTP req/s
        network: true, // Network monitoring at the application level
        ports: false // Shows which ports your app is listening on (default: false)
    });
    var probe = pmx.probe();
    // The counter will start at 0
    var counter = probe.counter({
        name: 'Current connected players'
    });
    /* Encryption situp */
    var crypto = require('crypto');
    var key = new Buffer([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]);
    var iv = new Buffer([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
    var cipherAlgorithmUsed = 'aes-256-cbc';
    /* Encrypt buffer */
    function encrypt(textBuffer) {
        try {
            var cipher = crypto.createCipheriv(cipherAlgorithmUsed, key, iv);
            var crypted = Buffer.concat([cipher.update(textBuffer), cipher.final()]);
            return crypted;
        } catch (err) {
            // Handle the error here.
            console.error('Error trying to encrypt, ' + err);
            return '';
        }
        return '';
    }
    /* Decrypt buffer */
    function decrypt(encryptedBuffer) {
        try {
            var decipher = crypto.createDecipheriv(cipherAlgorithmUsed, key, iv);
            var dec = Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]);
            return dec;
        } catch (err) {
            // Handle the error here.
            console.error('Error trying to decrypt, ' + err);
            return '';
        }
        return '';
    }
    /* Database Connection */
    var ConnectionPool = require('tedious-connection-pool');
    var DBTDSConnection = require('tedious').Connection;
    var Request = require('tedious').Request;
    var poolConfig = {
        min: 10, // The minimun of connections there can be in the pool. Default = 10
        max: 1000, //The maximum number of connections there can be in the pool. Default = 50
        log: false
    };
    var dBConnectionconfig = {
        userName: 'kskskhr@939jdn',
        password: 'kwpfnekososken',
        server: 'jjsmcmnxjfhsn.database.windows.net',
        options: {
            requestTimeout: 0,
            debug: {
                packet: false,
                data: false,
                payload: false,
                token: false,
                log: false
            },
            encrypt: true,// If you are on Microsoft Azure, you need this:
            database: 'ylldkgnje_db'
        }
    };
    //create the pool
    var pool = new ConnectionPool(poolConfig, dBConnectionconfig);
    pool.on('error', function(err) {
        console.error('Error when trying to create DB connection pool' + err);
    });
    /* Login Server */
    var net = require('net');
    var HOST = '127.0.0.1';
    var PORT = 4000; // nginx is used infront as a reverse proxy
    var server = net.createServer(); // Create a server instance
    server.listen(PORT, HOST); 
    onClientConnected_ServerEvent = function(sock) {
            counter.inc(); // Increment the counter, metrics
            sock.on('data', function(data) { // data from client
                parseJSON(decrypt(data), function(err, content) {
                    if (err) {
                        console.error('Error when trying to parse json received from client, json received is ' + data + ', and the error is ' + err);
                        sock.end();
                    } else {
                        switch (content.reqID) {
                            case 0:
                                setPlayerData(sock, content.msg);
                                break;
                            case 1:
                                getPlayerData(sock, content.msg);
                                break;
                            case 2:
                                createUser(sock, content.msg);
                                break;
                            default:
                                console.log('request id sent doesnt match any of the cases, id sent is ' + content.reqID);
                                return;
                        }
                    }
                });
            });
            sock.on('close', function(data) {
                counter.dec(); // Decrement the counter, metrics
            }); 
            sock.on('timeout', function(data) {
            });
        }
        /*
        Emitted when an error occurs. The 'close' event will be called directly following this event. See example in discussion of server.listen.
        */
    onError_ServerEvent = function(err) {
        // handle errors here
        console.error('onError_ServerEvent occured, ' + err);
        throw err;
    }
    server.on('connection', onClientConnected_ServerEvent);
    server.on('error', onError_ServerEvent);
    var PlayerData = function() {
            this.appVersionAndroid = "2.7",
                this.appVersioniOS = "2.7",
                this.kickStatus = "true",
                this.maxNumOfPlayerInRoom = "10",
                this.userName = "potato",
                this.coin = 2311,
                this.ruby = 22,
                this.rep = 352,
                this.driftMax = 2133,
                this.dailyRepsGiveAway = 132,
                this.equipedProducts = "3-1-2-1,3-6-2-1,2-1-1-1",
                this.boughtProducts = "3-1-2-1,3-6-2-1,2-1-1-1";
        }
        // case 0
    function setPlayerData(sock, dataSent) { // update player data
        // parse json, then update player row in DB
        parseJSON(dataSent, function(err, playerDataSent) {
            if (err) {
                console.error('(setPlayerData) Error when trying to parse json received from switch statment, json received is ' + dataSent + ', and the error is ' + err);
            } else {
                pool.acquire(function(err, connection) { //acquire a db connection from connection pool
                    if (err) {
                        console.error('(setPlayerData) Error when trying to acquire a DB connection from pool,  ' + err);
                        return;
                    }
                    var sqlUpdatePlayerDataQuery = "UPDATE PlayerDataTable  SET rep=" + playerDataSent.rep + ", driftDistance=" + playerDataSent.driftMax + ", coin = " + playerDataSent.coin + ", ruby = " + playerDataSent.ruby + ", dailyReps = " + playerDataSent.dailyRepsGiveAway + ", boughtProducts = '" + playerDataSent.boughtProducts + "', equippedProducts = '" + playerDataSent.equipedProducts + "', lastUpdated = GETDATE() WHERE UserName ='" + playerDataSent.userName + "'";
                    var request = new Request(sqlUpdatePlayerDataQuery, function(err, rowCount) {
                        if (err) {
                            sock.write(encrypt('0'));
                            console.error('(setPlayerData) Error when trying to execute SQL update player data,  ' + err);
                            return;
                        }
                        if (rowCount == 1) {
                            // 1 row updated
                            sock.write(encrypt('1'));
                        } else {
                            sock.write(encrypt('0'));
                        }
                        //release the connection back to the pool when finished
                        connection.release();
                    });
                    connection.execSql(request);
                });
            }
        });
    }
    // case 1
    function getPlayerData(sock, dataSent) { // get player data when game starts
        // dataSent will have the username
        // get all player data, parse, then send
        // acquire a db connection from connection pool
        pool.acquire(function(err, connection) {
            if (err) {
                console.error('(getPlayerData) Error when trying to acquire a DB connection from pool,  ' + err);
                return;
            }
            //use the connection as normal
            var request = new Request("select * from PlayerDataTable  where UserName = '" + dataSent + "'", function(err, rowCount) {
                if (err) {
                    sock.write(encrypt('0'));
                    console.error('(getPlayerData) Error when trying to execute SQL select * from ,  ' + err);
                    return;
                }
                if (rowCount < 1) {
                    // error retrieving user
                    sock.write(encrypt('0'));
                }
                //release the connection back to the pool when finished
                connection.release();
            });
            request.on('row', function(columns) {
                var playerData = new PlayerData();
                columns.forEach(function(column) {
                    if (column.metadata.colName === "UserName") {
                        playerData.userName = column.value;
                    } else if (column.metadata.colName === "rep") {
                        playerData.rep = column.value;
                    } else if (column.metadata.colName === "driftDistance") {
                        playerData.driftMax = column.value;
                    } else if (column.metadata.colName === "coin") {
                        playerData.coin = column.value;
                    } else if (column.metadata.colName === "ruby") {
                        playerData.ruby = column.value;
                    } else if (column.metadata.colName === "boughtProducts") {
                        playerData.boughtProducts = column.value;
                    } else if (column.metadata.colName === "equippedProducts") {
                        playerData.equipedProducts = column.value;
                    } else if (column.metadata.colName === "dailyReps") {
                        playerData.dailyRepsGiveAway = column.value;
                    }
                });

                asyncJSON.stringify(playerData, function(err, jsonValue) {
                    if (err) {
                        console.error('(getPlayerData) Error when trying to stringify json, json is ' + playerData + ', and the error is ' + err);
                    } else {
                        sock.write(encrypt(jsonValue));
                    }
                });
            });
            connection.execSql(request);
        });
    }
    // case 2
    function createUser(sock, dataSent) { // create a new username for a player
        // dataSent will have the username
        // if query succeeded print 1 else 0
        //acquire a db connection from connection pool
        pool.acquire(function(err, connection) {
            if (err) {
                console.error('(createUser) Error when trying to acquire a DB connection from pool,  ' + err);
                sock.write(encrypt('0'));
                return;
            }
            //console.error('name sent is ' + name);
            //use the connection as normal
            var request = new Request("INSERT INTO PlayerDataTable  (username,coin,ruby) VALUES ('" + dataSent + "',150,20)", function(err, rowCount) { 
                if (err) {
                    console.error('(createUser) Error when trying to execute SQL INSERT INTO ,  ' + err);
                    sock.write(encrypt('0'));
                } else {
                    sock.write(encrypt('1'));
                }
                //release the connection back to the pool when finished
                connection.release();
            });
            connection.execSql(request);
        });
    }
\$\endgroup\$
  • \$\begingroup\$ Those aren't your real credentials right? Maybe make that clear and replace them with <username>/<password> or so. \$\endgroup\$ – ferada Oct 27 '16 at 13:16
  • 1
    \$\begingroup\$ @ferada No they are not, all the sensitive data were replaced. Key, Iv, Username, Password ..etc. thanks for mentioning that anyways. \$\endgroup\$ – B.I.A Oct 27 '16 at 13:26
2
\$\begingroup\$

The first concern with this type of question is why (is it not using TLS/HTTPS)? A theoretical overhead doesn't matter at all when the protocol is broken. E.g. the IV is constant when it should be randomised on every message, but that's just an example for why reinventing it is a bad idea.

Next, this is probably only working on the "good" path, if there's fragmentation and the messages arrive in multiple pieces (like one-byte pieces in the extreme case, but in general with a bad or congested network connection) the decoding will just fail. The right way is to keep updating your cipher object with more input till it has all been consumed.

Next, the SQL injection. Right now, if someone gets hold of the single shared key the database is compromised since the input (from the network) isn't escaped. Libraries should have calls to deal with this.

\$\endgroup\$
  • \$\begingroup\$ Thanks a lot for your input, but could you elaborate more on the first concern, I mean from what I understand is that Node will fill the buffer until it's ready then it will pass the data to the cipher to be decrypted, and if a message arrived in multiple pieces, decryption will fail and the client will have to send the message again. about the fragmented messages, how do they occur?, because in almost all the tests i ran the messages received are in one piece. For SQL injection, i will try to escape strings before appending them. \$\endgroup\$ – B.I.A Oct 29 '16 at 19:01
  • \$\begingroup\$ As far as I can see it's a raw TCP stream and if you look at the data event there's no mention that all data from a single send is going to be in a single buffer. That's consistent with other socket APIs in general and streams for that matter. Now, the code might work so far, but as soon your network gets worse a few bytes might arrive early. Retrying shouldn't be necessary with TCP though, the rest of the data will arrive, just in a later call. \$\endgroup\$ – ferada Oct 29 '16 at 19:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.