瀏覽代碼

Merge branch 'master' into generator

painor 5 年之前
父節點
當前提交
be34801567
共有 20 個文件被更改,包括 1895 次插入23 次删除
  1. 1 0
      .gitignore
  2. 2 0
      .idea/.gitignore
  3. 0 21
      LICENSE
  4. 0 2
      README.md
  5. 4 0
      api/settings
  6. 85 0
      crypto/AES.js
  7. 21 0
      crypto/AuthKey.js
  8. 93 0
      crypto/Factorizator.js
  9. 249 0
      crypto/RSA.js
  10. 250 0
      errors.js
  11. 251 0
      network/authenticator.js
  12. 58 0
      network/mtprotoPlainSender.js
  13. 315 0
      network/mtprotoSender.js
  14. 74 0
      network/tcpClient.js
  15. 83 0
      network/tcpTransport.js
  16. 1 0
      package.json
  17. 45 0
      tl/mtprotoRequest.js
  18. 52 0
      tl/session.js
  19. 112 0
      tl/telegramClient.js
  20. 199 0
      utils/Helpers.js

+ 1 - 0
.gitignore

@@ -19,3 +19,4 @@ example.js
 
 # dotenv
 .env
+

+ 2 - 0
.idea/.gitignore

@@ -0,0 +1,2 @@
+# Default ignored files
+/workspace.xml

+ 0 - 21
LICENSE

@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2019 gram-js
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.

+ 0 - 2
README.md

@@ -1,2 +0,0 @@
-# gramjs
-NodeJS MTProto API Telegram client library,

+ 4 - 0
api/settings

@@ -0,0 +1,4 @@
+api_id=12345
+api_hash=0123456789abcdef0123456789abcdef
+user_phone=+34600000000
+session_name=anonymous

+ 85 - 0
crypto/AES.js

@@ -0,0 +1,85 @@
+const aesjs = require('aes-js');
+
+
+class AES {
+    /**
+     * Decrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector
+     * @param cipherText {Buffer}
+     * @param key {Buffer}
+     * @param iv {Buffer}
+     * @returns {Buffer}
+     */
+    static decryptIge(cipherText, key, iv) {
+        let iv1 = iv.slice(0, Math.floor(iv.length / 2));
+        let iv2 = iv.slice(Math.floor(iv.length / 2));
+        let plainText = new Array(cipherText.length).fill(0);
+        let aes = new aesjs.AES(key);
+        let blocksCount = Math.floor(plainText.length / 16);
+        let cipherTextBlock = new Array(16).fill(0);
+
+        for (let blockIndex = 0; blockIndex < blocksCount; blockIndex++) {
+            for (let i = 0; i < 16; i++) {
+                cipherTextBlock[i] = cipherText[blockIndex * 16 + i] ^ iv2[i];
+            }
+            let plainTextBlock = aes.decrypt(cipherTextBlock);
+            for (let i = 0; i < 16; i++) {
+                plainTextBlock[i] ^= iv1[i];
+            }
+
+            iv1 = cipherText.slice(blockIndex * 16, blockIndex * 16 + 16);
+            iv2 = plainTextBlock.slice(0, 16);
+            plainText = new Uint8Array([
+                ...plainText.slice(0, blockIndex * 16),
+                ...plainTextBlock.slice(0, 16),
+                ...plainText.slice(blockIndex * 16 + 16)
+            ]);
+
+        }
+        return Buffer.from(plainText);
+    }
+
+    /**
+     * Encrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector
+     * @param plainText {Buffer}
+     * @param key {Buffer}
+     * @param iv {Buffer}
+     * @returns {Buffer}
+     */
+    static encryptIge(plainText, key, iv) {
+        if (plainText.length % 16 !== 0) {
+            let padding = new Uint8Array(16 - plainText.length % 16);
+            plainText = new Uint8Array([
+                ...plainText,
+                ...padding,
+            ]);
+        }
+        let iv1 = iv.slice(0, Math.floor(iv.length / 2));
+        let iv2 = iv.slice(Math.floor(iv.length / 2));
+        let aes = new aesjs.AES(key);
+        let blocksCount = Math.floor(plainText.length / 16);
+        let cipherText = new Array(plainText.length).fill(0);
+        for (let blockIndex = 0; blockIndex < blocksCount; blockIndex++) {
+            let plainTextBlock = plainText.slice(blockIndex * 16, blockIndex * 16 + 16);
+
+            for (let i = 0; i < 16; i++) {
+                plainTextBlock[i] ^= iv1[i];
+            }
+            let cipherTextBlock = aes.encrypt(plainTextBlock);
+            for (let i = 0; i < 16; i++) {
+                cipherTextBlock[i] ^= iv2[i];
+            }
+
+            iv1 = cipherTextBlock.slice(0, 16);
+            iv2 = plainText.slice(blockIndex * 16, blockIndex * 16 + 16);
+            cipherText = new Uint8Array([
+                ...cipherText.slice(0, blockIndex * 16),
+                ...cipherTextBlock.slice(0, 16),
+                ...cipherText.slice(blockIndex * 16 + 16)
+            ]);
+        }
+        return Buffer.from(cipherText);
+    }
+
+}
+
+exports.AES = AES;

+ 21 - 0
crypto/AuthKey.js

@@ -0,0 +1,21 @@
+const helper = require("../utils/Helpers").helpers;
+
+class AuthKey {
+    constructor(data) {
+        this.data = data;
+
+        let buffer = Buffer.from(helper.sha1(data));
+
+        this.auxHash = buffer.slice(0, 8).readBigUInt64LE();
+        this.keyId = buffer.slice(12, 20).readBigUInt64LE();
+
+    }
+
+    calcNewNonceHash(new_nonce, number) {
+        let buffer = Buffer.concat([new_nonce, number, this.auxHash]);
+        return helper.calcMsgKey(buffer);
+    }
+
+}
+
+exports.AuthKey = AuthKey;

+ 93 - 0
crypto/Factorizator.js

@@ -0,0 +1,93 @@
+const helper = require("../utils/Helpers").helpers;
+
+class Factorizator {
+
+    /**
+     * Finds the small multiplier by using Lopatin's method
+     * @param what
+     * @return {number}
+     */
+    static findSmallMultiplierLopatin(what) {
+        let g = 0;
+        for (let i = 0; i < 3; i++) {
+            let q = 30 || (helper.getRandomInt(0, 127) & 15) + 17;
+            let x = 40 || helper.getRandomInt(0, 1000000000) + 1;
+
+
+            let y = x;
+            let lim = 1 << (i + 18);
+            for (let j = 1; j < lim; j++) {
+                let a = x;
+                let b = x;
+
+                let c = q;
+                while (b !== 0) {
+                    if ((b & 1) !== 0) {
+                        c += a;
+                        if (c >= what) {
+                            c -= what;
+                        }
+                    }
+                    a += a;
+                    if (a >= what) {
+                        a -= what;
+                    }
+                    b >>= 1;
+                }
+
+                x = c;
+                let z = ((x < y) ? (y - x) : (x - y));
+
+                g = this.gcd(z, what);
+                if (g !== 1) {
+                    break
+                }
+
+                if ((j & (j - 1)) === 0) {
+                    y = x;
+                }
+
+            }
+            if (g > 1) {
+                break;
+            }
+        }
+        let p = Math.floor(what / g);
+        return Math.min(p, g);
+    }
+
+    /**
+     * Calculates the greatest common divisor
+     * @param a
+     * @param b
+     * @returns {number}
+     */
+    static gcd(a, b) {
+        while (((a !== 0) && (b !== 0))) {
+            while (((b & 1) === 0)) {
+                b >>= 1;
+            }
+            while (((a & 1) === 0)) {
+                a >>= 1;
+            }
+            if ((a > b)) {
+                a -= b;
+            } else {
+                b -= a;
+            }
+        }
+        return ((b === 0) ? a : b);
+    }
+
+    /**
+     * Factorizes the given number and returns both the divisor and the number divided by the divisor
+     * @param pq
+     * @returns {{divisor: number, divided: number}}
+     */
+    static factorize(pq) {
+        let divisor = this.findSmallMultiplierLopatin(pq);
+        return {divisor: divisor, divided: Math.floor(pq / divisor)}
+    }
+}
+
+exports.Factorizator = Factorizator;

+ 249 - 0
crypto/RSA.js

@@ -0,0 +1,249 @@
+const helpers = require("../utils/Helpers").helpers;
+
+
+class RSA {
+    static _server_keys = {
+        '216be86c022bb4c3': new RSAServerKey("216be86c022bb4c3", parseInt('C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9' +
+            '1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E' +
+            '580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F' +
+            '9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934' +
+            'EF781D866B34F011FCE4D835A090196E9A5F0E4449AF7EB697DDB9076494CA5F' +
+            '81104A305B6DD27665722C46B60E5DF680FB16B210607EF217652E60236C255F' +
+            '6A28315F4083A96791D7214BF64C1DF4FD0DB1944FB26A2A57031B32EEE64AD1' +
+            '5A8BA68885CDE74A5BFC920F6ABF59BA5C75506373E7130F9042DA922179251F', 16), parseInt('010001', 16)),
+// -4344800451088585951
+        '-4344800451088585951': new RSAServerKey(  // Telegram servers //1
+// -----BEGIN RSA PUBLIC KEY-----
+            // MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
+            // lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
+            // an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw
+            // Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+
+            // 8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n
+            // Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB
+// -----END RSA PUBLIC KEY-----
+            "-4344800451088585951",
+            parseInt(
+                "C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9" +
+                "1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E" +
+                "580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F" +
+                "9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934" +
+                "EF781D866B34F011FCE4D835A090196E9A5F0E4449AF7EB697DDB9076494CA5F" +
+                "81104A305B6DD27665722C46B60E5DF680FB16B210607EF217652E60236C255F" +
+                "6A28315F4083A96791D7214BF64C1DF4FD0DB1944FB26A2A57031B32EEE64AD1" +
+                "5A8BA68885CDE74A5BFC920F6ABF59BA5C75506373E7130F9042DA922179251F", +
+                    16
+            ),  // Modulus
+            parseInt("010001", 16)  // Exponent
+        ),
+
+        // 847625836280919973
+        '847625836280919973': new RSAServerKey(  // Telegram servers //2
+// -----BEGIN PUBLIC KEY-----
+            // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAruw2yP/BCcsJliRoW5eB
+            // VBVle9dtjJw+OYED160Wybum9SXtBBLXriwt4rROd9csv0t0OHCaTmRqBcQ0J8fx
+            // hN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/j1dnVa/gVbCjdSxpbrfY2g2L4frzjJvd
+            // l84Kd9ORYjDEAyFnEA7dD556OptgLQQ2e2iVNq8NZLYTzLp5YpOdO1doK+ttrltg
+            // gTCy5SrKeLoCPPbOgGsdxJxyz5KKcZnSLj16yE5HvJQn0CNpRdENvRUXe6tBP78O
+            // 39oJ8BTHp9oIjd6XWXAsp2CvK45Ol8wFXGF710w9lwCGNbmNxNYhtIkdqfsEcwR5
+            // JwIDAQAB
+// -----END PUBLIC KEY-----
+            '847625836280919973',
+            parseInt(
+                "AEEC36C8FFC109CB099624685B97815415657BD76D8C9C3E398103D7AD16C9BB" +
+                "A6F525ED0412D7AE2C2DE2B44E77D72CBF4B7438709A4E646A05C43427C7F184" +
+                "DEBF72947519680E651500890C6832796DD11F772C25FF8F576755AFE055B0A3" +
+                "752C696EB7D8DA0D8BE1FAF38C9BDD97CE0A77D3916230C4032167100EDD0F9E" +
+                "7A3A9B602D04367B689536AF0D64B613CCBA7962939D3B57682BEB6DAE5B6081" +
+                "30B2E52ACA78BA023CF6CE806B1DC49C72CF928A7199D22E3D7AC84E47BC9427" +
+                "D0236945D10DBD15177BAB413FBF0EDFDA09F014C7A7DA088DDE9759702CA760" +
+                "AF2B8E4E97CC055C617BD74C3D97008635B98DC4D621B4891DA9FB0473047927",
+                16
+            ),  // Modulus
+            parseInt("010001", 16)  // Exponent
+        ),
+
+        // 1562291298945373506
+        '1562291298945373506': new RSAServerKey(  // Telegram servers //3
+// -----BEGIN PUBLIC KEY-----
+            // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvfLHfYH2r9R70w8prHbl
+            // Wt/nDkh+XkgpflqQVcnAfSuTtO05lNPspQmL8Y2XjVT4t8cT6xAkdgfmmvnvRPOO
+            // KPi0OfJXoRVylFzAQG/j83u5K3kRLbae7fLccVhKZhY46lvsueI1hQdLgNV9n1cQ
+            // 3TDS2pQOCtovG4eDl9wacrXOJTG2990VjgnIKNA0UMoP+KF03qzryqIt3oTvZq03
+            // DyWdGK+AZjgBLaDKSnC6qD2cFY81UryRWOab8zKkWAnhw2kFpcqhI0jdV5QaSCEx
+            // vnsjVaX0Y1N0870931/5Jb9ICe4nweZ9kSDF/gip3kWLG0o8XQpChDfyvsqB9OLV
+// /wIDAQAB
+// -----END PUBLIC KEY-----
+            '1562291298945373506',
+            parseInt(
+                "BDF2C77D81F6AFD47BD30F29AC76E55ADFE70E487E5E48297E5A9055C9C07D2B" +
+                "93B4ED3994D3ECA5098BF18D978D54F8B7C713EB10247607E69AF9EF44F38E28" +
+                "F8B439F257A11572945CC0406FE3F37BB92B79112DB69EEDF2DC71584A661638" +
+                "EA5BECB9E23585074B80D57D9F5710DD30D2DA940E0ADA2F1B878397DC1A72B5" +
+                "CE2531B6F7DD158E09C828D03450CA0FF8A174DEACEBCAA22DDE84EF66AD370F" +
+                "259D18AF806638012DA0CA4A70BAA83D9C158F3552BC9158E69BF332A45809E1" +
+                "C36905A5CAA12348DD57941A482131BE7B2355A5F4635374F3BD3DDF5FF925BF" +
+                "4809EE27C1E67D9120C5FE08A9DE458B1B4A3C5D0A428437F2BECA81F4E2D5FF",
+                16
+            ),  // Modulus
+            parseInt("010001", 16)  // Exponent
+        ),
+
+// -5859577972006586033
+        '-5859577972006586033': new RSAServerKey(  // Telegram servers //4
+// -----BEGIN PUBLIC KEY-----
+            // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ditzm+mPND6xkhzwFI
+            // z6J/968CtkcSE/7Z2qAJiXbmZ3UDJPGrzqTDHkO30R8VeRM/Kz2f4nR05GIFiITl
+            // 4bEjvpy7xqRDspJcCFIOcyXm8abVDhF+th6knSU0yLtNKuQVP6voMrnt9MV1X92L
+            // GZQLgdHZbPQz0Z5qIpaKhdyA8DEvWWvSUwwc+yi1/gGaybwlzZwqXYoPOhwMebzK
+            // Uk0xW14htcJrRrq+PXXQbRzTMynseCoPIoke0dtCodbA3qQxQovE16q9zz4Otv2k
+            // 4j63cz53J+mhkVWAeWxVGI0lltJmWtEYK6er8VqqWot3nqmWMXogrgRLggv/Nbbo
+            // oQIDAQAB
+// -----END PUBLIC KEY-----
+            '-5859577972006586033',
+            parseInt(
+                "B3F762B739BE98F343EB1921CF0148CFA27FF7AF02B6471213FED9DAA0098976" +
+                "E667750324F1ABCEA4C31E43B7D11F1579133F2B3D9FE27474E462058884E5E1" +
+                "B123BE9CBBC6A443B2925C08520E7325E6F1A6D50E117EB61EA49D2534C8BB4D" +
+                "2AE4153FABE832B9EDF4C5755FDD8B19940B81D1D96CF433D19E6A22968A85DC" +
+                "80F0312F596BD2530C1CFB28B5FE019AC9BC25CD9C2A5D8A0F3A1C0C79BCCA52" +
+                "4D315B5E21B5C26B46BABE3D75D06D1CD33329EC782A0F22891ED1DB42A1D6C0" +
+                "DEA431428BC4D7AABDCF3E0EB6FDA4E23EB7733E7727E9A1915580796C55188D" +
+                "2596D2665AD1182BA7ABF15AAA5A8B779EA996317A20AE044B820BFF35B6E8A1",
+                16
+            ),  // Modulus
+            parseInt("010001", 16)  // Exponent
+        ),
+
+        // 6491968696586960280
+        '6491968696586960280': new RSAServerKey(  // Telegram servers //5
+// -----BEGIN PUBLIC KEY-----
+            // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvmpxVY7ld/8DAjz6F6q0
+            // 5shjg8/4p6047bn6/m8yPy1RBsvIyvuDuGnP/RzPEhzXQ9UJ5Ynmh2XJZgHoE9xb
+            // nfxL5BXHplJhMtADXKM9bWB11PU1Eioc3+AXBB8QiNFBn2XI5UkO5hPhbb9mJpjA
+            // 9Uhw8EdfqJP8QetVsI/xrCEbwEXe0xvifRLJbY08/Gp66KpQvy7g8w7VB8wlgePe
+            // xW3pT13Ap6vuC+mQuJPyiHvSxjEKHgqePji9NP3tJUFQjcECqcm0yV7/2d0t/pbC
+            // m+ZH1sadZspQCEPPrtbkQBlvHb4OLiIWPGHKSMeRFvp3IWcmdJqXahxLCUS1Eh6M
+            // AQIDAQAB
+// -----END PUBLIC KEY-----
+            '6491968696586960280',
+            parseInt(
+                "BE6A71558EE577FF03023CFA17AAB4E6C86383CFF8A7AD38EDB9FAFE6F323F2D" +
+                "5106CBC8CAFB83B869CFFD1CCF121CD743D509E589E68765C96601E813DC5B9D" +
+                "FC4BE415C7A6526132D0035CA33D6D6075D4F535122A1CDFE017041F1088D141" +
+                "9F65C8E5490EE613E16DBF662698C0F54870F0475FA893FC41EB55B08FF1AC21" +
+                "1BC045DED31BE27D12C96D8D3CFC6A7AE8AA50BF2EE0F30ED507CC2581E3DEC5" +
+                "6DE94F5DC0A7ABEE0BE990B893F2887BD2C6310A1E0A9E3E38BD34FDED254150" +
+                "8DC102A9C9B4C95EFFD9DD2DFE96C29BE647D6C69D66CA500843CFAED6E44019" +
+                "6F1DBE0E2E22163C61CA48C79116FA77216726749A976A1C4B0944B5121E8C01",
+                16
+            ),  // Modulus
+            parseInt("010001", 16)  // Exponent
+        ),
+
+        // 6427105915145367799
+        '6427105915145367799': new RSAServerKey(  // CDN DC-121
+// -----BEGIN RSA PUBLIC KEY-----
+            // MIIBCgKCAQEA+Lf3PvgE1yxbJUCMaEAkV0QySTVpnaDjiednB5RbtNWjCeqSVakY
+            // HbqqGMIIv5WCGdFdrqOfMNcNSstPtSU6R9UmRw6tquOIykpSuUOje9H+4XVIKquj
+            // yL2ISdK+4ZOMl4hCMkqauw4bP1Sbr03vZRQbU6qEA04V4j879BAyBVhr3WG9+Zi+
+            // t5XfGSTgSExPYEl8rZNHYNV5RB+BuroVH2HLTOpT/mJVfikYpgjfWF5ldezV4Wo9
+            // LSH0cZGSFIaeJl8d0A8Eiy5B9gtBO8mL+XfQRKOOmr7a4BM4Ro2de5rr2i2od7hY
+            // Xd3DO9FRSl4y1zA8Am48Rfd95WHF3N/OmQIDAQAB
+// -----END RSA PUBLIC KEY-----
+            '6427105915145367799',
+            parseInt(
+                "F8B7F73EF804D72C5B25408C6840245744324935699DA0E389E76707945BB4D5" +
+                "A309EA9255A9181DBAAA18C208BF958219D15DAEA39F30D70D4ACB4FB5253A47" +
+                "D526470EADAAE388CA4A52B943A37BD1FEE175482AABA3C8BD8849D2BEE1938C" +
+                "978842324A9ABB0E1B3F549BAF4DEF65141B53AA84034E15E23F3BF410320558" +
+                "6BDD61BDF998BEB795DF1924E0484C4F60497CAD934760D579441F81BABA151F" +
+                "61CB4CEA53FE62557E2918A608DF585E6575ECD5E16A3D2D21F471919214869E" +
+                "265F1DD00F048B2E41F60B413BC98BF977D044A38E9ABEDAE01338468D9D7B9A" +
+                "EBDA2DA877B8585DDDC33BD1514A5E32D7303C026E3C45F77DE561C5DCDFCE99",
+                16
+            ),  // Modulus
+            parseInt("010001", 16)  // Exponent
+        ),
+
+        // 2685959930972952888
+        '2685959930972952888': new RSAServerKey(  // CDN DC-140
+// -----BEGIN RSA PUBLIC KEY-----
+            // MIIBCgKCAQEAzuHVC7sE50Kho/yDVZtWnlmA5Bf/aM8KZY3WzS16w6w1sBqipj8o
+            // gMGG7ULbGBtYmKEaI7IIJO6WM2m1MaXVnsqS8d7PaGAZiy8rSN3S7S2a8wp4RXZe
+            // hs0JAXvZeIz45iByCMBfycbJKmSweYkesRUI7hUO8eQhmm/UYUEpJY7VOt0Iemiu
+            // URSpqlRQ2FlcyHahYUNcvbICb4+/AP7coKBn6cB5FyzM7MCcKxbEKOx3Y3MUnbZq
+            // q5pN6/eRazkegyrlp4kuJ94KsbRFHFX5Dx8uzjrO9wi8LF7gIgZu5DRMcmjXJKq6
+            // rGZ2Z9cnrD8pVu1L2vcInd4K6ximZS2hbwIDAQAB
+// -----END RSA PUBLIC KEY-----
+            '2685959930972952888',
+            parseInt(
+                "CEE1D50BBB04E742A1A3FC83559B569E5980E417FF68CF0A658DD6CD2D7AC3AC" +
+                "35B01AA2A63F2880C186ED42DB181B5898A11A23B20824EE963369B531A5D59E" +
+                "CA92F1DECF6860198B2F2B48DDD2ED2D9AF30A7845765E86CD09017BD9788CF8" +
+                "E6207208C05FC9C6C92A64B079891EB11508EE150EF1E4219A6FD4614129258E" +
+                "D53ADD087A68AE5114A9AA5450D8595CC876A161435CBDB2026F8FBF00FEDCA0" +
+                "A067E9C079172CCCECC09C2B16C428EC776373149DB66AAB9A4DEBF7916B391E" +
+                "832AE5A7892E27DE0AB1B4451C55F90F1F2ECE3ACEF708BC2C5EE022066EE434" +
+                "4C7268D724AABAAC667667D727AC3F2956ED4BDAF7089DDE0AEB18A6652DA16F",
+                16
+            ),  // Modulus
+            parseInt("010001", 16)  // Exponent
+        )
+
+    };
+
+    /**
+     * Encrypts the given data given a fingerprint
+     * @param fingerprint
+     * @param data
+     * @param offset
+     * @param length
+     */
+    static encrypt(fingerprint, data, offset, length) {
+        if (!(fingerprint.toLowerCase() in RSA._server_keys)) {
+            return;
+        }
+        let key = RSA._server_keys[fingerprint.toLowerCase()];
+        return key.encrypt(data, offset, length);
+
+    }
+}
+
+class RSAServerKey {
+    constructor(fingerprint, m, e) {
+        this.fingerprint = fingerprint;
+        this.m = m;
+        this.e = e;
+
+    }
+
+    /**
+     * Encrypts the given data with the current key
+     * @param data
+     * @param offset
+     * @param length
+     */
+    encrypt(data, offset, length) {
+        if (offset === undefined) {
+            offset = 0;
+        }
+        if (length === undefined) {
+            length = data.length;
+        }
+        let dataToWrite = data.split(offset, offset + length);
+        let sha1Data = helpers.sha1(dataToWrite);
+        let writer = Buffer.concat([sha1Data, dataToWrite]);
+
+        if (length < 235) {
+            writer = Buffer.concat([writer, helpers.generateRandomBytes(235 - length)]);
+
+        }
+        let result = writer.readIntBE(0, writer.byteLength);
+        result = (result ** this.e) % this.m;
+        let buffer = Buffer.alloc(256);
+        buffer.writeUIntBE(result, 0, 256);
+        return buffer;
+    }
+}
+
+exports.RSA = RSA;

+ 250 - 0
errors.js

@@ -0,0 +1,250 @@
+/**
+ * Occurs when a read operation was cancelled
+ */
+class ReadCancelledError extends Error {
+    constructor() {
+        super("You must run `python3 tl_generator.py` first. #ReadTheDocs!")
+    }
+}
+
+/**
+ * Occurs when you should've ran `tl_generator.py`, but you haven't
+ */
+class TLGeneratorNotRan extends Error {
+    constructor() {
+        super("You must run `python3 tl_generator.py` first. #ReadTheDocs!")
+    }
+}
+
+/**
+ * Occurs when an invalid parameter is given, for example,
+ * when either A or B are required but none is given
+ */
+class InvalidParameterError extends Error {
+
+}
+
+/**
+ * Occurs when a type is not found, for example,
+ * when trying to read a TLObject with an invalid constructor code
+ */
+class TypeNotFoundError extends Error {
+    constructor(invalidConstructorId) {
+        super('Could not find a matching Constructor ID for the TLObject ' +
+            'that was supposed to be read with ID {}. Most likely, a TLObject ' +
+            'was trying to be read when it should not be read.'.replace("{}",
+                invalidConstructorId.toString("16")));
+        this.invalidConstructorId = invalidConstructorId;
+    }
+}
+
+/**
+ * Occurs when a read operation was cancelled
+ */
+class InvalidDCError extends Error {
+    constructor(newDC) {
+        super('Your phone number is registered to #{} DC. ' +
+            'This should have been handled automatically; ' +
+            'if it has not, please restart the app.'.replace("{}", newDC));
+        this.newDC = newDC;
+    }
+}
+
+/**
+ * Occurs when an invalid checksum is passed
+ */
+class InvalidChecksumError extends Error {
+    constructor(checksum, validChecksum) {
+        super('Invalid checksum ({0} when {1} was expected). This packet should be skipped.'
+            .replace("{0}", checksum).replace("{1}", validChecksum));
+        this.checksum = checksum;
+        this.validChecksum = validChecksum;
+
+    }
+}
+
+class RPCError extends Error {
+    static CodeMessages = {
+        303: Array('ERROR_SEE_OTHER', 'The request must be repeated, but directed to a different data center.'),
+
+        400: Array('BAD_REQUEST', 'The query contains errors. In the event that a request was created using a ' +
+            'form and contains user generated data, the user should be notified that the ' +
+            'data must be corrected before the query is repeated.'),
+
+        401: Array('UNAUTHORIZED', 'There was an unauthorized attempt to use functionality available only to ' +
+            'authorized users.'),
+
+        403: Array('FORBIDDEN', 'Privacy violation. For example, an attempt to write a message to someone who ' +
+            'has blacklisted the current user.'),
+
+        404: Array('NOT_FOUND', 'An attempt to invoke a non-existent object, such as a method.'),
+
+        420: Array('FLOOD', 'The maximum allowed number of attempts to invoke the given method with ' +
+            'the given input parameters has been exceeded. For example, in an attempt ' +
+            'to request a large number of text messages (SMS) for the same phone number.'),
+
+        500: Array('INTERNAL', 'An internal server error occurred while a request was being processed; ' +
+            'for example, there was a disruption while accessing a database or file storage.')
+    };
+    static ErrorMessages = {
+        // 303 ERROR_SEE_OTHER
+        'FILE_MIGRATE_(\\d+)': 'The file to be accessed is currently stored in a different data center (#{}).',
+
+        'PHONE_MIGRATE_(\\d+)': 'The phone number a user is trying to use for authorization is associated ' +
+            'with a different data center (#{}).',
+
+        'NETWORK_MIGRATE_(\\d+)': 'The source IP address is associated with a different data center (#{}, ' +
+            'for registration).',
+
+        'USER_MIGRATE_(\\d+)': 'The user whose identity is being used to execute queries is associated with ' +
+            'a different data center  (#{} for registration).',
+
+        // 400 BAD_REQUEST
+        'FIRSTNAME_INVALID': 'The first name is invalid.',
+
+        'LASTNAME_INVALID': 'The last name is invalid.',
+
+        'PHONE_NUMBER_INVALID': 'The phone number is invalid.',
+
+        'PHONE_CODE_HASH_EMPTY': 'phone_code_hash is missing.',
+
+        'PHONE_CODE_EMPTY': 'phone_code is missing.',
+
+        'PHONE_CODE_EXPIRED': 'The confirmation code has expired.',
+
+        'API_ID_INVALID': 'The api_id/api_hash combination is invalid.',
+
+        'PHONE_NUMBER_OCCUPIED': 'The phone number is already in use.',
+
+        'PHONE_NUMBER_UNOCCUPIED': 'The phone number is not yet being used.',
+
+        'USERS_TOO_FEW': 'Not enough users (to create a chat, for example).',
+
+        'USERS_TOO_MUCH': 'The maximum number of users has been exceeded (to create a chat, for example).',
+
+        'TYPE_CONSTRUCTOR_INVALID': 'The type constructor is invalid.',
+
+        'FILE_PART_INVALID': 'The file part number is invalid.',
+
+        'FILE_PARTS_INVALID': 'The number of file parts is invalid.',
+
+        'FILE_PART_(\\d+)_MISSING': 'Part {} of the file is missing from storage.',
+
+        'MD5_CHECKSUM_INVALID': 'The MD5 checksums do not match.',
+
+        'PHOTO_INVALID_DIMENSIONS': 'The photo dimensions are invalid.',
+
+        'FIELD_NAME_INVALID': 'The field with the name FIELD_NAME is invalid.',
+
+        'FIELD_NAME_EMPTY': 'The field with the name FIELD_NAME is missing.',
+
+        'MSG_WAIT_FAILED': 'A waiting call returned an error.',
+
+        'CHAT_ADMIN_REQUIRED': 'Chat admin privileges are required to do that in the specified chat ' +
+            '(for example, to send a message in a channel which is not yours).',
+
+        // 401 UNAUTHORIZED
+        'AUTH_KEY_UNREGISTERED': 'The key is not registered in the system.',
+
+        'AUTH_KEY_INVALID': 'The key is invalid.',
+
+        'USER_DEACTIVATED': 'The user has been deleted/deactivated.',
+
+        'SESSION_REVOKED': 'The authorization has been invalidated, because of the user terminating all sessions.',
+
+        'SESSION_EXPIRED': 'The authorization has expired.',
+
+        'ACTIVE_USER_REQUIRED': 'The method is only available to already activated users.',
+
+        'AUTH_KEY_PERM_EMPTY': 'The method is unavailable for temporary authorization key, not bound to permanent.',
+
+        // 420 FLOOD
+        'FLOOD_WAIT_(\\d+)': 'A wait of {} seconds is required.'
+    };
+    constructor(code, message) {
+        let codeMeaning = RPCError.CodeMessages[code];
+        let mustResend = code === 303; // ERROR_SEE_OTHER, "The request must be repeated"
+
+        let calledSuper = false;
+        for (let item in RPCError.ErrorMessages) {
+
+            let key = new RegExp(item);
+            let errorMsg = RPCError.ErrorMessages[item];
+
+            let match = message.match(key);
+
+            if (match) {
+            console.log(match[1]);
+                // Get additionalData if any
+                if (match.length === 2) {
+                    console.log(errorMsg);
+                    let additionalData = parseInt(match[1]);
+                    super(errorMsg.replace("{}", additionalData));
+
+                    this.additionalData = additionalData;
+                } else {
+                    super(errorMsg);
+                }
+                calledSuper = true;
+                break;
+            }
+
+        }
+        if (!calledSuper) {
+            super("Unknown error message with code {0}: {1}"
+                .replace("{0}", code).replace("{1}", message))
+        }
+        this.code = code;
+        this.errorMessage = message;
+        this.codeMeaning = codeMeaning;
+        this.mustResend = mustResend;
+
+    }
+}
+
+
+/**
+ * Occurs when handling a badMessageNotification
+ */
+class BadMessageError extends Error {
+    static ErrorMessages = {
+        16: 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' +
+            'synchronize it using msg_id notifications and re-send the original message ' +
+            'with the “correct” msg_id or wrap it in a container with a new msg_id if the ' +
+            'original message had waited too long on the client to be transmitted).',
+
+        17: 'msg_id too high (similar to the previous case, the client time has to be ' +
+            'synchronized, and the message re-sent with the correct msg_id).',
+
+        18: 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' +
+            'to be divisible by 4).',
+
+        19: 'Container msg_id is the same as msg_id of a previously received message ' +
+            '(this must never happen).',
+
+        20: 'Message too old, and it cannot be verified whether the server has received a ' +
+            'message with this msg_id or not.',
+
+        32: 'msg_seqno too low (the server has already received a message with a lower ' +
+            'msg_id but with either a higher or an equal and odd seqno).',
+
+        33: 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' +
+            'either a lower or an equal and odd seqno).',
+
+        34: 'An even msg_seqno expected (irrelevant message), but odd received.',
+
+        35: 'Odd msg_seqno expected (relevant message), but even received.',
+
+        48: 'Incorrect server salt (in this case, the bad_server_salt response is received with ' +
+            'the correct salt, and the message is to be re-sent with it).',
+
+        64: 'Invalid container.'
+    };
+
+    constructor(code) {
+        super(BadMessageError.ErrorMessages[code] || "Unknown error code (this should not happen): {}."
+            .replace("{}", code));
+    }
+
+}
+

+ 251 - 0
network/authenticator.js

@@ -0,0 +1,251 @@
+const AES = require("../crypto/AES").AES;
+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);
+
+    }
+}

+ 58 - 0
network/mtprotoPlainSender.js

@@ -0,0 +1,58 @@
+const helpers = require("../utils/Helpers").helpers;
+
+/**
+ * MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)
+ */
+class MtProtoPlainSender {
+    constructor(transport) {
+        this._sequence = 0;
+        this._timeOffset = 0;
+        this._lastMsgId = 0;
+        this._transport = transport;
+    }
+
+    /**
+     * Sends a plain packet (auth_key_id = 0) containing the given message body (data)
+     * @param data
+     */
+    send(data) {
+        let packet = Buffer.alloc(8, 0);
+        packet.writeBigInt64LE(this.getNewMsgId(), packet.byteLength);
+        packet.writeInt32LE(data.length, packet.byteLength);
+        packet.write(data, packet.byteLength);
+        this._transport.send(packet);
+    }
+
+    /**
+     * Receives a plain packet, returning the body of the response
+     * @returns {Buffer}
+     */
+    receive() {
+        let {seq, body} = this._transport.receive();
+        let message_length = body.readInt32LE(16);
+        return body.slice(20, message_length);
+
+    }
+
+    /**
+     * Generates a new message ID based on the current time (in ms) since epoch
+     * @returns {BigInt}
+     */
+    getNewMsgId() {
+        //See https://core.telegram.org/mtproto/description#message-identifier-msg-id
+        let msTime = Date.now();
+        let newMsgId = ((BigInt(Math.floor(msTime / 1000)) << BigInt(32)) | // "must approximately equal unixtime*2^32"
+            (BigInt(msTime % 1000) << BigInt(32)) | // "approximate moment in time the message was created"
+            BigInt(helpers.getRandomInt(0, 524288)) << BigInt(2));// "message identifiers are divisible by 4"
+        //Ensure that we always return a message ID which is higher than the previous one
+        if (this._lastMsgId >= newMsgId) {
+            newMsgId = this._lastMsgId + 4
+        }
+        this._lastMsgId = newMsgId;
+        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);
+
+        }
+    }
+}

+ 74 - 0
network/tcpClient.js

@@ -0,0 +1,74 @@
+const Socket = require("net").Socket;
+const TextEncoder = require("util").TextEncoder;
+const sleep = require("../utils/Helpers").helpers.sleep;
+
+class TcpClient {
+    constructor() {
+        this.connected = false;
+        this.socket = new Socket();
+        this.canceled = false;
+        this.delay = 100;
+    }
+
+    /**
+     * Connects to the specified IP and port number
+     * @param ip
+     * @param port
+     */
+    async connect(ip, port) {
+        this.socket.connect({host: ip, port: port});
+        this.connected = true;
+
+    }
+
+    /**
+     * Closes the connection
+     */
+    async close() {
+        this.socket.destroy();
+        this.connected = true;
+    }
+
+    /**
+     * Writes (sends) the specified bytes to the connected peer
+     * @param data
+     */
+    async write(data) {
+        this.socket.write(data);
+    }
+
+    /**
+     * Reads (receives) the specified bytes from the connected peer
+     * @param bufferSize
+     * @returns {Buffer}
+     */
+    async read(bufferSize) {
+        this.canceled = false;
+        let buffer = Buffer.alloc(0);
+
+        let writtenCount = 0;
+        while (writtenCount < bufferSize) {
+            let leftCount = bufferSize - writtenCount;
+            let partial = this.socket.read(leftCount);
+            if (partial == null) {
+                await sleep(this.delay);
+                continue;
+            }
+            buffer = Buffer.concat([buffer, partial]);
+            writtenCount += buffer.byteLength;
+        }
+
+        return buffer;
+    }
+
+    /**
+     * Cancels the read operation IF it hasn't yet
+     * started, raising a ReadCancelledError
+     */
+    cancelRead() {
+        this.canceled = true;
+    }
+
+}
+
+exports.TcpClient = TcpClient;

+ 83 - 0
network/tcpTransport.js

@@ -0,0 +1,83 @@
+const TcpClient = require("./tcpClient").TcpClient;
+const crc = require('crc');
+
+class TcpTransport {
+    constructor(ipAddress, port) {
+        this.tcpClient = new TcpClient();
+        this.sendCounter = 0;
+        this.ipAddress = ipAddress;
+        this.port = port;
+    }
+
+    async connect() {
+        await this.tcpClient.connect(this.ipAddress, this.port);
+    }
+
+    /**
+     * Sends the given packet (bytes array) to the connected peer
+     * Original reference: https://core.telegram.org/mtproto#tcp-transport
+     * The packets are encoded as: total length, sequence number, packet and checksum (CRC32)
+     * @param packet
+     */
+    send(packet) {
+        if (this.tcpClient.connected) {
+            throw Error("not connected");
+        }
+        let buffer = Buffer.alloc(4 + 4);
+        buffer.writeInt32LE(packet.length + 12, 0);
+        buffer.writeInt32LE(this.sendCounter, 4);
+        buffer = Buffer.concat([buffer, packet]);
+        let tempBuffer = Buffer.alloc(4);
+        tempBuffer.writeUInt32LE(crc.crc32(buffer), 0);
+        buffer = Buffer.concat([buffer, tempBuffer]);
+        this.tcpClient.write(buffer);
+        this.sendCounter++;
+    }
+
+    /**
+     * Receives a TCP message (tuple(sequence number, body)) from the connected peer
+     * @returns {{body: {Buffer}, seq: {Buffer}}}
+     */
+    receive() {
+        /**First read everything we need**/
+        let packetLengthBytes = this.tcpClient.read(4);
+        let packetLength = Buffer.from(packetLengthBytes).readInt32LE(0);
+        let seqBytes = this.tcpClient.read(4);
+        let seq = Buffer.from(seqBytes).readInt32LE(0);
+        let body = this.tcpClient.read(packetLength - 12);
+        let checksum = Buffer.from(this.tcpClient.read(4)).readUInt32LE(0);
+        /**Then perform the checks**/
+        let rv = Buffer.concat([packetLengthBytes, seqBytes, body]);
+        let validChecksum = crc.crc32(rv);
+        if (checksum !== validChecksum) {
+            throw Error("invalid checksum");
+        }
+        /** If we passed the tests, we can then return a valid TCP message**/
+
+        return {seq, body}
+    }
+
+    close() {
+        if (this.tcpClient.connected) {
+            this.tcpClient.close();
+        }
+    }
+
+    /**
+     * Cancels (stops) trying to receive from the
+     * remote peer and raises an {Error}
+     */
+    cancelReceive() {
+        this.tcpClient.cancelRead();
+    }
+
+    /**
+     * Gets the client read delay
+     * @returns {number}
+     */
+    getClientDelay() {
+        return this.tcpClient.delay;
+    }
+
+
+}

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
   },
   "homepage": "https://github.com/gram-js/gramjs#readme",
   "dependencies": {
+    "aes-js": "^3.1.2",
     "crc": "^3.8.0",
     "csv-parse": "^4.4.6",
     "fast-csv": "^3.4.0",

+ 45 - 0
tl/mtprotoRequest.js

@@ -0,0 +1,45 @@
+class MTProtoRequest {
+
+    constructor() {
+        this.sent = false;
+        this.msgId = 0; //long
+        this.sequence = 0;
+
+        this.dirty = false;
+        this.sendTime = 0;
+        this.confirmReceived = false;
+
+        // These should be overrode
+
+        this.constructorId = 0;
+        this.confirmed = false;
+        this.responded = false;
+    }
+
+    // these should not be overrode
+    onSendSuccess() {
+        this.sendTime = new Date().getTime();
+        this.sent = true;
+    }
+
+    onConfirm() {
+        this.confirmReceived = true;
+    }
+
+    needResend(){
+        return this.dirty || (this.confirmed && !this.confirmReceived && new Date().getTime() - this.sendTime > 3000);
+    }
+
+    // These should be overrode
+    onSend(buffer){
+
+    }
+
+    onResponse(buffer){
+
+    }
+
+    onException(exception){
+
+    }
+}

+ 52 - 0
tl/session.js

@@ -0,0 +1,52 @@
+const Helpers = require("../utils/Helpers");
+const fs = require("fs");
+
+class Session {
+    constructor(sessionUserId) {
+        this.sessionUserId = sessionUserId;
+        this.serverAddress = "149.154.167.40";
+        this.port = 80;
+        this.authKey = undefined;
+        this.id = Helpers.generateRandomLong(false);
+        this.sequence = 0;
+        this.salt = 0; // Unsigned long
+        this.timeOffset = 0;
+        this.lastMessageId = 0;
+        this.user = undefined;
+    }
+
+    /**
+     * Saves the current session object as session_user_id.session
+     */
+    async save() {
+        if (this.sessionUserId) {
+            await fs.writeFile(`${this.sessionUserId}.session`, JSON.stringify(this));
+        }
+    }
+
+    static tryLoadOrCreateNew(sessionUserId) {
+        if (sessionUserId === undefined) {
+            return new Session();
+        }
+        let filepath = `${sessionUserId}.session`;
+        if (fs.existsSync(filepath)) {
+            return JSON.parse(fs.readFileSync(filepath, "utf-8"));
+        } else {
+            return Session(sessionUserId);
+        }
+    }
+
+    getNewMsgId() {
+        let msTime = new Date().getTime();
+        let newMessageId = (BigInt(Math.floor(msTime / 1000) + this.timeOffset) << BigInt(32)) |
+            ((msTime % 1000) << 22) |
+            (Helpers.getRandomInt(0, 524288) << 2); // 2^19
+
+        if (this.lastMessageId >= newMessageId) {
+            newMessageId = this.lastMessageId + 4;
+        }
+        this.lastMessageId = newMessageId;
+        return newMessageId;
+    }
+}
+

+ 112 - 0
tl/telegramClient.js

@@ -0,0 +1,112 @@
+const Session = require("./Session");
+
+class TelegramClient {
+
+    async constructor(sessionUserId, layer, apiId, apiHash) {
+        if (apiId === undefined || apiHash === undefined) {
+            throw Error("Your API ID or Hash are invalid. Please read \"Requirements\" on README.md");
+        }
+        this.apiId = apiId;
+        this.apiHash = apiHash;
+
+        this.layer = layer;
+
+        this.session = Session.tryLoadOrCreateNew(sessionUserId);
+        this.transport = TcpTransport(this.session.serverAddress, this.session.port);
+
+        //These will be set later
+        this.dcOptions = undefined;
+        this.sender = undefined;
+        this.phoneCodeHashes = Array();
+
+    }
+
+    /**
+     * Connects to the Telegram servers, executing authentication if required.
+     * Note that authenticating to the Telegram servers is not the same as authenticating
+     * the app, which requires to send a code first.
+     * @param reconnect {Boolean}
+     * @returns {Promise<Boolean>}
+     */
+    async connect(reconnect = false) {
+        try {
+            if (!this.session.authKey || reconnect) {
+                let res = network.authenticator.doAuthentication(this.transport);
+                this.session.authKey = res.authKey;
+                this.session.timeOffset = res.timeOffset;
+                this.session.save();
+            }
+            this.sender = MtProtoSender(this.transport, this.session);
+
+            // Now it's time to send an InitConnectionRequest
+            // This must always be invoked with the layer we'll be using
+            let query = InitConnectionRequest({
+                apiId: apiId,
+                deviceModel: "PlaceHolder",
+                systemVersion: "PlaceHolder",
+                appVersion: "0.0.1",
+                langCode: "en",
+                query: GetConfigRequest()
+            });
+            let result = await this.invoke(InvokeWithLayerRequest({
+                layer: this.layer,
+                query: query
+            }));
+
+            // We're only interested in the DC options
+            // although many other options are available!
+            this.dcOptions = result.dcOptions;
+            return true;
+        } catch (error) {
+            console.log('Could not stabilise initial connection: {}'.replace("{}", error));
+            return false;
+        }
+    }
+
+    /**
+     * Reconnects to the specified DC ID. This is automatically called after an InvalidDCError is raised
+     * @param dc_id {number}
+     */
+    async reconnect_to_dc(dc_id) {
+
+        if (this.dcOptions === undefined || this.dcOptions.length === 0) {
+            throw new Error("Can't reconnect. Stabilise an initial connection first.");
+        }
+        let dc;
+        for (dc of this.dcOptions) {
+            if (dc.id === dc_id) {
+                break;
+            }
+        }
+        this.transport.close();
+        this.transport = new TcpTransport(dc.ipAddress, dc.port);
+        this.session.server_address = dc.ipAddress;
+        this.session.port = dc.port;
+        this.session.save();
+        await this.connect(true);
+    }
+
+    /**
+     * Disconnects from the Telegram server
+     * @returns {Promise<void>}
+     */
+    async disconnect() {
+        if (this.sender) {
+            await this.sender.disconnect();
+        }
+    }
+
+    /**
+     * Invokes a MTProtoRequest (sends and receives it) and returns its result
+     * @param request
+     * @returns {Promise}
+     */
+    async invoke(request) {
+        if (!MTProtoRequest.prototype.isPrototypeOf(Object.getPrototypeOf(request).prototype)) {
+            throw new Error("You can only invoke MtProtoRequests");
+        }
+        await this.sender.send(request);
+        await this.sender.receive(request);
+        return request.result;
+    }
+}

+ 199 - 0
utils/Helpers.js

@@ -0,0 +1,199 @@
+const crypto = require('crypto');
+const fs = require("fs").promises;
+
+class Helpers {
+
+
+    /**
+     * Generates a random long integer (8 bytes), which is optionally signed
+     * @returns {number}
+     */
+    static generateRandomLong(signed) {
+        let buf = Buffer.from(this.generateRandomBytes(8)); // 0x12345678 = 305419896
+        if (signed)
+            return buf.readInt32BE(0);
+        else
+            return buf.readUInt32LE(0);
+    }
+
+    /**
+     * Generates a random bytes array
+     * @param count
+     * @returns {Buffer}
+     */
+    static generateRandomBytes(count) {
+        return crypto.randomBytes(count);
+    }
+
+    /**
+     * Loads the user settings located under `api/`
+     * @param path
+     * @returns {Promise<void>}
+     */
+    static async loadSettings(path = "../api/settings") {
+        let settings = {};
+        let left, right, value_pair;
+
+        let data = await fs.readFile(path, 'utf-8');
+
+        for (let line of data.toString().split('\n')) {
+            value_pair = line.split("=");
+            if (value_pair.length !== 2) {
+                break;
+            }
+            left = value_pair[0].replace(/ \r?\n|\r/g, '');
+            right = value_pair[1].replace(/ \r?\n|\r/g, '');
+            if (!isNaN(right)) {
+                settings[left] = Number.parseInt(right);
+            } else {
+                settings[left] = right;
+            }
+        }
+
+
+        return settings;
+
+
+    }
+
+    /**
+     * Calculate the key based on Telegram guidelines, specifying whether it's the client or not
+     * @param shared_key
+     * @param msg_key
+     * @param client
+     * @returns {[*, *]}
+     */
+
+    static calcKey(shared_key, msg_key, client) {
+        let x = client !== null ? 0 : 8;
+        let iv, key, sha1a, sha1b, sha1c, sha1d;
+        sha1a = this.sha1((msg_key + shared_key.slice(x, (x + 32))));
+        sha1b = this.sha1(((shared_key.slice((x + 32), (x + 48)) + msg_key) + shared_key.slice((x + 48), (x + 64))));
+        sha1c = this.sha1((shared_key.slice((x + 64), (x + 96)) + msg_key));
+        sha1d = this.sha1((msg_key + shared_key.slice((x + 96), (x + 128))));
+        key = ((sha1a.slice(0, 8) + sha1b.slice(8, 20)) + sha1c.slice(4, 16));
+        iv = (((sha1a.slice(8, 20) + sha1b.slice(0, 8)) + sha1c.slice(16, 20)) + sha1d.slice(0, 8));
+        return [key, iv];
+
+
+    }
+
+    /**
+     * Calculates the message key from the given data
+     * @param data
+     * @returns {Buffer}
+     */
+    static calcMsgKey(data) {
+        return this.sha1(data).slice(4, 20);
+
+
+    }
+
+    /**
+     * Generates the key data corresponding to the given nonces
+     * @param serverNonce
+     * @param newNonce
+     * @returns {{ivBuffer: Buffer, keyBuffer: Buffer}}
+     */
+    static generateKeyDataFromNonces(serverNonce, newNonce) {
+        let hash1 = this.sha1(Buffer.concat([newNonce, serverNonce]));
+        let hash2 = this.sha1(Buffer.concat([serverNonce, newNonce]));
+        let hash3 = this.sha1(Buffer.concat([newNonce, newNonce]));
+        let keyBuffer = Buffer.concat([hash1, hash1.slice(0, 12)]);
+        let ivBuffer = Buffer.concat([hash2.slice(12, 20), hash3, newNonce.slice(0, 4)]);
+        return {keyBuffer: keyBuffer, ivBuffer: ivBuffer}
+    }
+
+    /**
+     * Calculates the SHA1 digest for the given data
+     * @param data
+     * @returns {Buffer}
+     */
+    static sha1(data) {
+        let shaSum = crypto.createHash('sha1');
+        shaSum.update(data);
+        return shaSum.digest();
+
+    }
+
+
+    /**
+     *
+     * @param buffer {Buffer}
+     * @param offset {Number}
+     * @returns {{data: {Buffer}, offset: {Number}}}
+     */
+    static tgReadByte(buffer, offset) {
+        let firstByte = buffer.readInt8(offset);
+        offset += 1;
+        let padding, length;
+        if (firstByte === 255) {
+            length = buffer.readInt8(offset) | (buffer.readInt8(offset) << 8) | (buffer.readInt8(offset) << 16);
+            offset += 1;
+            padding = length % 4;
+        } else {
+            length = firstByte;
+            padding = (length + 1) % 4;
+        }
+        let data = buffer.readInt8(offset);
+        offset += 1;
+        if (padding > 0) {
+            padding = 4 - padding;
+            offset += padding;
+        }
+        return {data, offset}
+    }
+
+    static tgWriteBytes(data) {
+        let buffer;
+        let padding;
+
+        if (data.length < 254) {
+            padding = (data.length + 1) % 4;
+            if (padding !== 0) {
+                padding = 4 - padding;
+            }
+            buffer = Buffer.from([data.length, data]);
+        } else {
+            padding = data.length % 4;
+            if (padding !== 0) {
+                padding = 4 - padding;
+            }
+            buffer = Buffer.concat([Buffer.from([254]),
+                Buffer.from([data.length % 256]),
+                Buffer.from([(data.length >> 8) % 256]),
+                Buffer.from([(data.length >> 16) % 256]),
+                Buffer.from([data]),
+                Buffer.from([padding])
+            ]);
+
+        }
+        return buffer;
+
+
+    }
+
+    /**
+     * returns a random int from min (inclusive) and max (inclusive)
+     * @param min
+     * @param max
+     * @returns {number}
+     */
+    static getRandomInt(min, max) {
+        min = Math.ceil(min);
+        max = Math.floor(max);
+        return Math.floor(Math.random() * (max - min + 1)) + min;
+    }
+
+    /**
+     * Sleeps a specified amount of time
+     * @param ms time in milliseconds
+     * @returns {Promise}
+     */
+    static sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+
+}
+
+let l = Buffer.from(0x83c95aec);
+console.log(l.length);
+