Browse Source

Add network MtProtoSenders and Authenticator

painor 5 years ago
parent
commit
86d0afc42d
3 changed files with 562 additions and 0 deletions
  1. 246 0
      network/authenticator.js
  2. 1 0
      network/mtprotoPlainSender.js
  3. 315 0
      network/mtprotoSender.js

+ 246 - 0
network/authenticator.js

@@ -3,3 +3,249 @@ const AuthKey = require("../crypto/AuthKey").AuthKey;
 const Factorizator = require("../crypto/Factorizator").Factorizator;
 const RSA = require("../crypto/RSA").RSA;
 const MtProtoPlainSender = require("./mtprotoPlainSender").MtProtoPlainSender;
+const Helpers = require("../utils/Helpers").helpers;
+
+function doAuthentication(transport) {
+    let sender = MtProtoPlainSender(transport);
+
+    // Step 1 sending: PQ request
+    let nonce = Helpers.generateRandomBytes(16);
+    let buffer = Buffer.alloc(32);
+    buffer.writeUInt32LE(0x60469778, 0);
+    buffer = Buffer.concat([buffer, nonce]);
+    sender.send(buffer);
+
+    // Step 1 response: PQ request
+    let pq = null;
+    let serverNonce = null;
+    let fingerprints = Array();
+    buffer = sender.receive();
+    let responseCode = buffer.readUInt32LE(0);
+    if (responseCode !== 0x05162463) {
+        throw Error("invalid response code");
+    }
+    let nonceFromServer = buffer.read(16, 8);
+    if (nonce !== nonceFromServer) {
+        throw Error("Invalid nonce from server");
+    }
+    serverNonce = buffer.read(16, 12);
+
+    let {pqBytes, newOffset} = Helpers.tgReadByte(buffer, 12);
+    pq = buffer.readBigInt64BE(newOffset);
+    newOffset += 8;
+    let vectorId = buffer.readInt8(newOffset);
+    newOffset += 1;
+    if (vectorId !== 0x1cb5c415) {
+        throw Error("vector error");
+    }
+    let fingerprints_count = buffer.readInt8(newOffset);
+    for (let i = 0; i < fingerprints_count; i++) {
+        fingerprints.push(buffer.readInt32LE(newOffset));
+        newOffset += 8;
+    }
+
+    // Step 2 sending: DH Exchange
+    let newNonce = Helpers.generateRandomBytes(32);
+    let {p, q} = Factorizator.factorize(pq);
+    let tempBuffer = Buffer.alloc(8);
+    tempBuffer.writeUIntLE(0x83c95aec, 0, 8);
+    let pqInnerData = Buffer.concat([
+        tempBuffer,
+        Helpers.tgWriteBytes(getByteArray(pq, false)),
+        Helpers.tgWriteBytes(getByteArray(Math.min(p, q), false)),
+        Helpers.tgWriteBytes(getByteArray(Math.max(p, q), false)),
+        nonce,
+        serverNonce,
+        newNonce,
+    ]);
+    let cipherText, targetFingerprint;
+    for (let fingerprint of fingerprints) {
+        cipherText = RSA.encrypt(getFingerprintText(fingerprint), pqInnerData);
+        if (cipherText !== undefined) {
+            targetFingerprint = fingerprint;
+            break;
+        }
+    }
+    if (cipherText === undefined) {
+        throw Error("Could not find a valid key for fingerprints");
+    }
+    tempBuffer = Buffer.alloc(8);
+    tempBuffer.writeUIntLE(0xd712e4be, 0, 8);
+
+    let reqDhParams = Buffer.concat([
+        tempBuffer,
+        nonce,
+        serverNonce,
+        Helpers.tgWriteBytes(getByteArray(Math.min(p, q), false)),
+        Helpers.tgWriteBytes(getByteArray(Math.max(p, q), false)),
+        targetFingerprint,
+        Helpers.tgWriteBytes(cipherText)
+    ]);
+    sender.send(reqDhParams);
+    // Step 2 response: DH Exchange
+    newOffset = 0;
+    let reader = sender.receive();
+    responseCode = reader.readInt32LE(newOffset);
+    newOffset += 4;
+    if (responseCode === 0x79cb045d) {
+        throw Error("Server DH params fail: TODO ");
+    }
+    if (responseCode !== 0xd0e8075c) {
+        throw Error("Invalid response code: TODO ");
+    }
+    nonceFromServer = reader.readIntLE(newOffset, 16);
+    newOffset += 16;
+    if (nonceFromServer !== nonce) {
+        throw Error("Invalid nonce from server");
+    }
+    let serverNonceFromServer = reader.readIntLE(newOffset, 16);
+    if (serverNonceFromServer !== nonceFromServer) {
+        throw Error("Invalid server nonce from server");
+    }
+    newOffset += 16;
+    let encryptedAnswer = Helpers.tgReadByte(reader, newOffset).data;
+
+    // Step 3 sending: Complete DH Exchange
+
+    let {key, iv} = Helpers.generateKeyDataFromNonces(serverNonce, newNonce);
+    let plainTextAnswer = AES.decryptIge(encryptedAnswer, key, iv);
+    let g, dhPrime, ga, timeOffset;
+    let dhInnerData = plainTextAnswer;
+    newOffset = 20;
+    let code = dhInnerData.readUInt32BE(newOffset);
+    if (code !== 0xb5890dba) {
+        throw Error("Invalid DH Inner Data code:")
+    }
+    newOffset += 4;
+    let nonceFromServer1 = dhInnerData.readIntLE(newOffset, 16);
+    if (nonceFromServer1 !== nonceFromServer) {
+        throw Error("Invalid nonce in encrypted answer");
+    }
+    newOffset += 16;
+    let serverNonceFromServer1 = dhInnerData.readIntLE(newOffset, 16);
+    if (serverNonceFromServer1 !== serverNonce) {
+        throw Error("Invalid server nonce in encrypted answer");
+    }
+    newOffset += 16;
+    g = dhInnerData.readInt32LE(newOffset);
+    newOffset += 4;
+    let temp = Helpers.tgReadByte(dhInnerData, newOffset);
+    newOffset += temp.offset;
+
+    dhPrime = temp.data.readUInt32BE(0);
+    temp = Helpers.tgReadByte(dhInnerData, newOffset);
+    newOffset += temp.offset;
+    ga = temp.data.readUInt32BE(0);
+    let serverTime = dhInnerData.readInt32LE(newOffset);
+    timeOffset = serverTime - Math.floor(new Date().getTime() / 1000);
+    let b = Helpers.generateRandomBytes(2048).readUInt32BE(0);
+    let gb = (g ** b) % dhPrime;
+    let gab = (ga * b) % dhPrime;
+
+    // Prepare client DH Inner Data
+
+    tempBuffer = Buffer.alloc(8);
+    tempBuffer.writeUIntLE(0x6643b654, 0, 8);
+    let clientDHInnerData = Buffer.concat([
+        tempBuffer,
+        nonce,
+        serverNonce,
+        Buffer.alloc(8).fill(0),
+        Helpers.tgWriteBytes(getByteArray(gb, false)),
+    ]);
+    let clientDhInnerData = Buffer.concat([
+        Helpers.sha1(clientDHInnerData),
+        clientDHInnerData
+    ]);
+
+    // Encryption
+    let clientDhInnerDataEncrypted = AES.encryptIge(clientDhInnerData, key, iv);
+
+    // Prepare Set client DH params
+    tempBuffer = Buffer.alloc(8);
+    tempBuffer.writeUIntLE(0xf5045f1f, 0, 8);
+    let setClientDhParams = Buffer.concat([
+        tempBuffer,
+        nonce,
+        serverNonce,
+        Helpers.tgWriteBytes(clientDhInnerDataEncrypted),
+    ]);
+    sender.send(setClientDhParams);
+
+    // Step 3 response: Complete DH Exchange
+    reader = sender.receive();
+    newOffset = 0;
+    code = reader.readUInt32LE(newOffset);
+    newOffset += 4;
+    if (code === 0x3bcbf734) { //  DH Gen OK
+        nonceFromServer = reader.readIntLE(newOffset, 16);
+        newOffset += 16;
+        if (nonceFromServer !== nonce) {
+            throw Error("Invalid nonce from server");
+        }
+        serverNonceFromServer = reader.readIntLE(newOffset, 16);
+        newOffset += 16;
+        if (serverNonceFromServer !== serverNonce) {
+            throw Error("Invalid server nonce from server");
+        }
+        let newNonceHash1 = reader.readIntLE(newOffset, 16);
+        let authKey = AuthKey(getByteArray(gab, false));
+        let newNonceHashCalculated = authKey.calcNewNonceHash(newNonce, 1);
+        if (newNonceHash1 !== newNonceHashCalculated) {
+            throw Error("Invalid new nonce hash");
+        }
+        return {authKey,timeOffset};
+    }
+    else if (code===0x46dc1fb9){
+        throw Error("dh_gen_retry");
+
+    }
+    else if (code===0x46dc1fb9){
+        throw Error("dh_gen_fail");
+
+    }else{
+        throw Error("DH Gen unknown");
+
+    }
+
+}
+
+function rightJustify(string, length, char) {
+    let fill = [];
+    while (fill.length + string.length < length) {
+        fill[fill.length] = char;
+    }
+    return fill.join('') + string;
+}
+
+/**
+ * Gets a fingerprint text in 01-23-45-67-89-AB-CD-EF format (no hyphens)
+ * @param fingerprint {Array}
+ * @returns {string}
+ */
+function getFingerprintText(fingerprint) {
+    let res = "";
+    for (let b of fingerprint) {
+        res += rightJustify(b.toString(16), 2, '0').toUpperCase();
+    }
+    return res;
+}
+
+/**
+ *
+ * @param integer {number,BigInt}
+ * @param signed {boolean}
+ * @returns {number}
+ */
+function getByteArray(integer, signed) {
+    let bits = integer.toString(2).length;
+    let byteLength = Math.floor((bits + 8 - 1) / 8);
+    let buffer = Buffer.alloc(byteLength);
+    if (signed) {
+        return buffer.readIntLE(0, byteLength);
+
+    } else {
+        return buffer.readUIntLE(0, byteLength);
+
+    }
+}

+ 1 - 0
network/mtprotoPlainSender.js

@@ -52,6 +52,7 @@ class MtProtoPlainSender {
         return BigInt(newMsgId);
 
     }
+
 }
 
 exports.MtProtoPlainSender = MtProtoPlainSender;

+ 315 - 0
network/mtprotoSender.js

@@ -0,0 +1,315 @@
+const MtProtoPlainSender = require("./mtprotoPlainSender").MtProtoPlainSender;
+const Helpers = require("../utils/Helpers");
+
+/**
+ * MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)
+ */
+class MtProtoSender {
+    constructor(transport, session) {
+        this.transport = transport;
+        this.session = session;
+        this.needConfirmation = Array(); // Message IDs that need confirmation
+        this.onUpdateHandlers = Array();
+
+    }
+
+    /**
+     * Disconnects
+     */
+    disconnect() {
+        this.setListenForUpdates(false);
+        this.transport.close();
+    }
+
+    /**
+     * Adds an update handler (a method with one argument, the received
+     * TLObject) that is fired when there are updates available
+     * @param handler {function}
+     */
+    addUpdateHandler(handler) {
+        let firstHandler = Boolean(this.onUpdateHandlers.length);
+        this.onUpdateHandlers.push(handler);
+        // If this is the first added handler,
+        // we must start receiving updates
+        if (firstHandler) {
+            this.setListenForUpdates(true);
+        }
+    }
+
+    /**
+     * Removes an update handler (a method with one argument, the received
+     * TLObject) that is fired when there are updates available
+     * @param handler {function}
+     */
+    removeUpdateHandler(handler) {
+        let index = this.onUpdateHandlers.indexOf(handler);
+        if (index !== -1) this.onUpdateHandlers.splice(index, 1);
+        if (!Boolean(this.onUpdateHandlers.length)) {
+            this.setListenForUpdates(false);
+
+        }
+    }
+
+    /**
+     *
+     * @param confirmed {boolean}
+     * @returns {number}
+     */
+    generateSequence(confirmed) {
+        if (confirmed) {
+            let result = this.session.sequence * 2 + 1;
+            this.session.sequence += 1;
+            return result;
+        } else {
+            return this.session.sequence * 2;
+        }
+    }
+
+    /**
+     * Sends the specified MTProtoRequest, previously sending any message
+     * which needed confirmation. This also pauses the updates thread
+     * @param request {MtProtoPlainSender}
+     * @param resend
+     */
+    send(request, resend = false) {
+        let buffer;
+        //If any message needs confirmation send an AckRequest first
+        if (Boolean(this.needConfirmation.length)) {
+            let msgsAck = MsgsAck(this.needConfirmation);
+
+            buffer = msgsAck.onSend();
+            this.sendPacket(buffer, msgsAck);
+            this.needConfirmation.length = 0;
+        }
+        //Finally send our packed request
+        buffer = request.on_send();
+        this.sendPacket(buffer, request);
+
+        //And update the saved session
+        this.session.save();
+
+    }
+
+    receive(request) {
+        try {
+            //Try until we get an update
+            while (!request.confirmReceive()) {
+                let {seq, body} = this.transport.receive();
+                let {message, remoteMsgId, remoteSequence} = this.decodeMsg(body);
+                this.processMsg(remoteMsgId, remoteSequence, message, request);
+            }
+        } catch (e) {
+
+        }
+    }
+
+    // region Low level processing
+    /**
+     * Sends the given packet bytes with the additional
+     * information of the original request.
+     * @param packet
+     * @param request
+     */
+    sendPacket(packet, request) {
+        request.msgId = this.session.getNewMsgId();
+
+        // First Calculate plainText to encrypt it
+        let first = Buffer.alloc(8);
+        let second = Buffer.alloc(8);
+        let third = Buffer.alloc(8);
+        let forth = Buffer.alloc(4);
+        let fifth = Buffer.alloc(4);
+        first.writeBigUInt64LE(this.session.salt, 0);
+        second.writeBigUInt64LE(this.session.id, 0);
+        third.writeBigUInt64LE(request.msgId, 0);
+        forth.writeInt32LE(this.generateSequence(request.confirmed), 0);
+        fifth.writeInt32LE(packet.length, 0);
+        let plain = Buffer.concat([
+            first,
+            second,
+            third,
+            forth,
+            fifth,
+            packet
+        ]);
+        let msgKey = Helpers.calcMsgKey(plain);
+        let {key, iv} = Helpers.calcKey(this.session.authKey.key, msgKey, true);
+        let cipherText = AES.encryptIge(plain, key, iv);
+
+        //And then finally send the encrypted packet
+
+        first = Buffer.alloc(8);
+        first.writeUInt32LE(this.session.authKey.keyId, 0);
+        let cipher = Buffer.concat([
+            first,
+            msgKey,
+            cipherText,
+        ]);
+        this.transport.send(cipher);
+    }
+
+    decodeMsg(body) {
+        if (body.length < 8) {
+            throw Error("Can't decode packet");
+        }
+        let offset = 8;
+        let msgKey = body.readIntLE(offset, 16);
+        offset += 16;
+        let {key, iv} = Helpers.calcKey(this.session.authKey.key, msgKey, false);
+        let plainText = AES.decryptIge(body.readIntLE(offset, body.length - offset), key, iv);
+        offset = 0;
+        let remoteSalt = plainText.readBigInt64LE(offset);
+        offset += 8;
+        let remoteSessionId = plainText.readBigInt64LE(offset);
+        offset += 8;
+        let remoteSequence = plainText.readBigInt64LE(offset);
+        offset += 8;
+        let remoteMsgId = plainText.readInt32LE(offset);
+        offset += 4;
+        let msgLen = plainText.readInt32LE(offset);
+        offset += 4;
+        let message = plainText.readIntLE(offset, msgLen);
+        return {message, remoteMsgId, remoteSequence}
+    }
+
+    processMsg(msgId, sequence, reader, offset, request = undefined) {
+        this.needConfirmation.push(msgId);
+        let code = reader.readUInt32LE(offset);
+        offset -= 4;
+
+        // The following codes are "parsed manually"
+        if (code === 0xf35c6d01) {  //rpc_result, (response of an RPC call, i.e., we sent a request)
+            return this.handleRpcResult(msgId, sequence, reader, request);
+        }
+
+        if (code === 0x73f1f8dc) {  //msg_container
+            return this.handlerContainer(msgId, sequence, reader, request);
+        }
+        if (code === 0x3072cfa1) {  //gzip_packed
+            return this.handlerGzipPacked(msgId, sequence, reader, request);
+        }
+        if (code === 0xedab447b) {  //bad_server_salt
+            return this.handleBadServerSalt(msgId, sequence, reader, request);
+        }
+        if (code === 0xa7eff811) {  //bad_msg_notification
+            return this.handleBadMsgNotification(msgId, sequence, reader);
+        }
+        /**
+         * If the code is not parsed manually, then it was parsed by the code generator!
+         * In this case, we will simply treat the incoming TLObject as an Update,
+         * if we can first find a matching TLObject
+         */
+        if (tlobjects.contains(code)) {
+            return this.handleUpdate(msgId, sequence, reader);
+        }
+        console.log("Unknown message");
+        return false;
+    }
+
+    // region Message handling
+
+    handleUpdate(msgId, sequence, reader) {
+        let tlobject = Helpers.tgReadObject(reader);
+        for (let handler of this.onUpdateHandlers) {
+            handler(tlobject);
+        }
+        return Float32Array
+    }
+
+    handleContainer(msgId, sequence, reader, offset, request) {
+        let code = reader.readUInt32LE(offset);
+        offset += 4;
+        let size = reader.readInt32LE(offset);
+        offset += 4;
+        for (let i = 0; i < size; i++) {
+            let innerMsgId = reader.readBigUInt64LE(offset);
+            offset += 8;
+            let innerSequence = reader.readBigInt64LE(offset);
+            offset += 8;
+            let innerLength = reader.readInt32LE(offset);
+            offset += 4;
+            if (!this.processMsg(innerMsgId, sequence, reader, request)) {
+                offset += innerLength;
+            }
+        }
+        return false;
+    }
+
+    handleBadServerSalt(msgId, sequence, reader, offset, request) {
+        let code = reader.readUInt32LE(offset);
+        offset += 4;
+        let badMsgId = reader.readUInt32LE(offset);
+        offset += 4;
+        let badMsgSeqNo = reader.readInt32LE(offset);
+        offset += 4;
+        let errorCode = reader.readInt32LE(offset);
+        offset += 4;
+        let newSalt = reader.readUInt32LE(offset);
+        offset += 4;
+        this.session.salt = newSalt;
+
+        if (!request) {
+            throw Error("Tried to handle a bad server salt with no request specified");
+        }
+
+        //Resend
+        this.send(request, true);
+        return true;
+    }
+
+    handleBadMsgNotification(msgId, sequence, reader, offset) {
+        let code = reader.readUInt32LE(offset);
+        offset += 4;
+        let requestId = reader.readUInt32LE(offset);
+        offset += 4;
+        let requestSequence = reader.readInt32LE(offset);
+        offset += 4;
+        let errorCode = reader.readInt32LE(offset);
+        return BadMessageError(errorCode);
+    }
+
+    handleRpcResult(msgId, sequence, reader, offset, request) {
+        if (!request) {
+            throw Error("RPC results should only happen after a request was sent");
+        }
+
+        let code = reader.readUInt32LE(offset);
+        offset += 4;
+        let requestId = reader.readUInt32LE(offset);
+        offset += 4;
+        let innerCode = reader.readUInt32LE(offset);
+        offset += 4;
+        if (requestId === request.msgId) {
+            request.confirmReceived = true;
+        }
+
+        if (innerCode === 0x2144ca19) {  // RPC Error
+            // TODO add rpc logic
+            throw Error("error");
+        } else {
+            // TODO
+        }
+    }
+
+    handleGzipPacked(msgId, sequence, reader, offset, request) {
+        // TODO
+    }
+
+    setListenForUpdates(enabled) {
+
+        if (enabled) {
+            console.log("Enabled updates");
+        } else {
+            console.log("Disabled updates");
+        }
+    }
+
+    updatesListenMethod() {
+        while (true) {
+            let {seq, body} = this.transport.receive();
+            let {message, remoteMsgId, remoteSequence} = this.decodeMsg(body);
+            this.processMsg(remoteMsgId, remoteSequence, message);
+
+        }
+    }
+}