diff --git a/README.md b/README.md index ee64bd0..f0d148f 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,29 @@ level library to write bots. Hopefully eventually we can merge [mineflayer](https://github.com/superjoe30/mineflayer) with this project. + +## Try it out so far + +``` +$ MC_EMAIL=you@example.com MC_PASSWORD=your_pass node test.js +logging in to minecraft.net +logged in as user_name +connect +enc key request +write enc key response +confirmation enc key response +writing 205 packet with encryption +login request { id: 1, + entityId: 839, + levelType: 'default', + gameMode: 0, + dimension: 0, + difficulty: 1, + _notUsed1: 0, + maxPlayers: 20 } + +assert.js:102 + throw new assert.AssertionError({ + ^ +AssertionError: Unrecognized packetId: 6 +``` diff --git a/lib/parser.js b/lib/parser.js index 12473a3..71d4b9c 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -46,6 +46,10 @@ Parser.prototype.connect = function(port, host) { }); }; +Parser.prototype.end = function() { + this.client.end(); +}; + Parser.prototype.writePacket = function(packetId, params) { var buffer = createPacketBuffer(packetId, params); var out = this.encryptionEnabled ? new Buffer(this.cipher.update(buffer), 'binary') : buffer; @@ -64,6 +68,8 @@ var readers = { 'string': readString, 'byteArray': readByteArray, 'short': readShort, + 'int': readInt, + 'byte': readByte, }; function readString (buffer, offset) { @@ -74,7 +80,7 @@ function readString (buffer, offset) { var strLen = results.value; var strEnd = strBegin + strLen * 2; if (strEnd > buffer.length) return null; - var str = fromUcs2.convert(buffer.slice(strBegin, strEnd)).toString(); + var str = fromUcs2.convert(buffer.slice(strBegin, strEnd)).toString('utf8'); return { value: str, @@ -107,6 +113,24 @@ function readShort(buffer, offset) { }; } +function readInt(buffer, offset) { + if (offset + 4 > buffer.length) return null; + var value = buffer.readInt32BE(offset); + return { + value: value, + size: 4, + }; +} + +function readByte(buffer, offset) { + if (offset + 1 > buffer.length) return null; + var value = buffer.readInt8(offset); + return { + value: value, + size: 1, + }; +} + function StringWriter(value) { this.value = value; this.encoded = toUcs2.convert(value); diff --git a/packets.json b/packets.json index b9e54f9..1a69b9e 100644 --- a/packets.json +++ b/packets.json @@ -35,7 +35,7 @@ "type": "byte" }, { - "name": "userName", + "name": "username", "type": "string" }, { diff --git a/test.js b/test.js index 701e5e2..d5f35da 100644 --- a/test.js +++ b/test.js @@ -2,15 +2,23 @@ var Parser = require('./lib/parser') , ursa = require('ursa') , crypto = require('crypto') , assert = require('assert') + , superagent = require('superagent') + +var input = { + email: process.env.MC_EMAIL, + password: process.env.MC_PASSWORD, + serverHost: 'localhost', + serverPort: 25565, +}; var parser = new Parser(); parser.on('connect', function() { console.info("connect"); parser.writePacket(Parser.HANDSHAKE, { protocolVersion: 51, - userName: 'superjoe30', - serverHost: 'localhost', - serverPort: 25565, + username: loginSession.username, + serverHost: input.serverHost, + serverPort: input.serverPort, }); }); parser.on('packet', function(packet) { @@ -27,7 +35,44 @@ parser.on('error', function(err) { parser.on('end', function() { console.info("disconnect"); }); -parser.connect(25565, 'localhost'); + +var loginSession = null; +getLoginSession(function() { + parser.connect(input.serverPort, input.serverHost); +}); + +function getLoginSession(cb) { + console.log("logging in to minecraft.net"); + var req = superagent.post("https://login.minecraft.net"); + req.type('form'); + req.send({ + user: input.email, + password: input.password, + version: 13, + }); + req.end(function(err, resp) { + if (err) { + cb(err); + } else if (! resp.ok) { + cb(new Error("login.minecraft.net status " + resp.status + ": " + resp.text)); + } else { + var values = resp.text.split(':'); + var session = { + currentGameVersion: values[0], + username: values[2], + id: values[3], + uid: values[4], + }; + if (session.id) { + loginSession = session; + console.info("logged in as", session.username); + cb(); + } else { + cb(new Error("login.minecraft.net says " + session.currentGameVersion)); + } + } + }); +} var packetHandlers = { 0xFC: onEncryptionKeyResponse, @@ -46,20 +91,46 @@ function onLoginRequest(packet) { function onEncryptionKeyRequest(packet) { console.log("enc key request"); + var hash = crypto.createHash('sha1'); + hash.update(packet.serverId); crypto.randomBytes(16, function (err, sharedSecret) { assert.ifError(err); - var pubKey = mcPubKeyToURsa(packet.publicKey); - var encryptedSharedSecret = pubKey.encrypt(sharedSecret, 'binary', 'base64', ursa.RSA_PKCS1_PADDING); - var encryptedSharedSecretBuffer = new Buffer(encryptedSharedSecret, 'base64'); - var encryptedVerifyToken = pubKey.encrypt(packet.verifyToken, 'binary', 'base64', ursa.RSA_PKCS1_PADDING); - var encryptedVerifyTokenBuffer = new Buffer(encryptedVerifyToken, 'base64'); - parser.cipher = crypto.createCipheriv('aes-128-cfb8', sharedSecret, sharedSecret); - parser.decipher = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret); - console.log("write enc key response"); - parser.writePacket(Parser.ENCRYPTION_KEY_RESPONSE, { - sharedSecret: encryptedSharedSecretBuffer, - verifyToken: encryptedVerifyTokenBuffer, + hash.update(sharedSecret); + hash.update(packet.publicKey); + var digest = mcHexDigest(hash); + var request = superagent.get("http://session.minecraft.net/game/joinserver.jsp"); + + request.query({ + user: loginSession.username, + sessionId: loginSession.id, + serverId: digest, }); + request.end(function(err, resp) { + if (err) { + console.error("session.minecraft.net not available"); + // TODO emit error + } else if (! resp.ok) { + console.error("session.minecraft.net returned error:", resp.status, resp.text); + // TODO emit error + } else { + sendEncryptionKeyResponse(); + } + }); + + function sendEncryptionKeyResponse() { + var pubKey = mcPubKeyToURsa(packet.publicKey); + var encryptedSharedSecret = pubKey.encrypt(sharedSecret, 'binary', 'base64', ursa.RSA_PKCS1_PADDING); + var encryptedSharedSecretBuffer = new Buffer(encryptedSharedSecret, 'base64'); + var encryptedVerifyToken = pubKey.encrypt(packet.verifyToken, 'binary', 'base64', ursa.RSA_PKCS1_PADDING); + var encryptedVerifyTokenBuffer = new Buffer(encryptedVerifyToken, 'base64'); + parser.cipher = crypto.createCipheriv('aes-128-cfb8', sharedSecret, sharedSecret); + parser.decipher = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret); + console.log("write enc key response"); + parser.writePacket(Parser.ENCRYPTION_KEY_RESPONSE, { + sharedSecret: encryptedSharedSecretBuffer, + verifyToken: encryptedVerifyTokenBuffer, + }); + } }); } @@ -82,3 +153,31 @@ function mcPubKeyToURsa(mcPubKeyBuffer) { pem += "-----END PUBLIC KEY-----\n"; return ursa.createPublicKey(pem, 'utf8'); } + +function mcHexDigest(hash) { + var buffer = new Buffer(hash.digest(), 'binary'); + // check for negative hashes + var negative = buffer.readInt8(0) < 0; + if (negative) performTwosCompliment(buffer); + var digest = buffer.toString('hex'); + // trim leading zeroes + digest = digest.replace(/^0+/g, ''); + if (negative) digest = '-' + digest; + return digest; + + function performTwosCompliment(buffer) { + var carry = true; + var i, newByte, value; + for (i = buffer.length - 1; i >= 0; --i) { + value = buffer.readUInt8(i); + newByte = ~value & 0xff; + if (carry) { + carry = newByte === 0xff; + buffer.writeUInt8(newByte + 1, i); + } else { + buffer.writeUInt8(newByte, i); + } + } + } +} +